Skip to content

Commit f75ffef

Browse files
committed
Added readycheck and depends_on keys that allow Panes and Windows to
depend on each other.
1 parent d39d6be commit f75ffef

File tree

4 files changed

+305
-27
lines changed

4 files changed

+305
-27
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11

2-
=== Similar projects
2+
=== Directly Inspired By:
33

44
* teamocil
55
* tmuxinator
6+
* tmuxp
67
* tmuxstart

main.go

Lines changed: 148 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,51 @@ import (
77
"log"
88
"os"
99
"os/exec"
10-
"strings"
10+
"time"
1111

1212
"gopkg.in/yaml.v2"
1313
)
1414

15+
type Pane struct {
16+
Object `yaml:",inline"`
17+
Dir string
18+
Focus bool
19+
Cmd string
20+
target string
21+
}
22+
23+
type Window struct {
24+
Object `yaml:",inline"`
25+
Dir string
26+
Focus bool
27+
Layout string
28+
Panes []*Pane
29+
}
30+
1531
type Project struct {
1632
Name string
1733
Dir string
18-
Windows []struct {
19-
Name string
20-
Dir string
21-
Layout string
22-
Focus bool
23-
Panes []string
24-
}
34+
PreCmd string `yaml:"pre_cmd"`
35+
PostCmd string `yaml:"post_cmd"`
36+
Windows []*Window
2537
}
2638

27-
func run(name string, args ...string) {
28-
cmd := exec.Command(name, args...)
39+
func run(format string, args ...interface{}) error {
40+
cmdStr := fmt.Sprintf(format, args...)
41+
cmd := exec.Command("/bin/sh", "-c", cmdStr)
42+
43+
fmt.Println(cmdStr)
44+
_, err := cmd.CombinedOutput()
45+
if err != nil {
46+
return err
47+
}
48+
49+
return nil
50+
}
2951

30-
fmt.Println(name, strings.Join(args[:], " "))
31-
out, err := cmd.CombinedOutput()
52+
func shell(format string, args ...interface{}) {
53+
err := run(format, args...)
3254
if err != nil {
33-
fmt.Printf("Output: %s", out)
3455
log.Fatal(err)
3556
}
3657
}
@@ -39,19 +60,19 @@ var sessionStarted bool
3960

4061
func NewWindow(session, dir string) {
4162
if sessionStarted {
42-
run("tmux", "new-window", "-d", "-t "+session, "-c "+dir)
63+
shell("tmux new-window -d -t %s -c %s", session, dir)
4364
} else {
44-
run("tmux", "new-session", "-d", "-s "+session, "-c "+dir)
65+
shell("tmux new-session -d -s %s -c %s", session, dir)
4566
sessionStarted = true
4667
}
4768
}
4869

4970
func NewPane(target, dir string) {
50-
run("tmux", "split-window", "-t "+target, "-c "+dir)
71+
shell("tmux split-window -t %s -c %s", target, dir)
5172
}
5273

5374
func SelectWindow(target string) {
54-
run("tmux", "select-window", "-t "+target)
75+
shell("tmux select-window -t %s", target)
5576
}
5677

5778
func SelectLayout(target, layout string) {
@@ -67,19 +88,23 @@ func SelectLayout(target, layout string) {
6788
default:
6889
log.Fatal("Bad layout: " + layout)
6990
}
70-
run("tmux", "select-layout", "-t "+target, layout)
91+
shell("tmux select-layout -t %s %s", target, layout)
7192
}
7293

7394
func SendLine(target, text string) {
7495
if text == "" {
7596
return
7697
}
77-
run("tmux", "send-keys", "-l", "-t "+target, text)
78-
run("tmux", "send-keys", "-R", "-t "+target, "Enter")
98+
shell("tmux send-keys -t %s '%s'", target, text)
99+
shell("tmux send-keys -R -t %s 'Enter'", target)
79100
}
80101

81102
func KillSession(session string) {
82-
run("tmux", "kill-session", "-t "+session)
103+
shell("tmux kill-session -t %s", session)
104+
}
105+
106+
func SetEnvironment(session, key, value string) {
107+
shell("tmux set-environment -t %s %s %s", session, key, value)
83108
}
84109

85110
func coalesce(args ...string) string {
@@ -91,42 +116,139 @@ func coalesce(args ...string) string {
91116
return ""
92117
}
93118

119+
func (project *Project) getDir(w *Window, paneIndex int) string {
120+
if paneIndex > len(w.Panes) {
121+
log.Fatal("Pane index out of bounds?!")
122+
}
123+
124+
if paneIndex == 0 && len(w.Panes) == 0 {
125+
// The window has no explicit panes
126+
return coalesce(w.Dir, project.Dir, ".")
127+
}
128+
129+
return coalesce(w.Panes[paneIndex].Dir, w.Dir, project.Dir, ".")
130+
}
131+
132+
func (p *Pane) Run() {
133+
SendLine(p.target, p.Cmd)
134+
}
135+
136+
func (w *Window) Run() {
137+
}
138+
139+
func (w *Window) DoReadyCheck() {
140+
for {
141+
ready := true
142+
143+
for _, p := range w.Panes {
144+
if p == nil {
145+
continue
146+
}
147+
if !p.IsReady() {
148+
ready = false
149+
time.Sleep(100 * time.Millisecond)
150+
break
151+
}
152+
}
153+
154+
if ready {
155+
return
156+
}
157+
}
158+
}
159+
160+
func (p *Pane) DoReadyCheck() {
161+
if p.ReadyCheck.Test == "" {
162+
return
163+
}
164+
165+
for {
166+
if err := run(p.ReadyCheck.Test); err == nil {
167+
break
168+
}
169+
170+
if p.ReadyCheck.Retries <= 0 {
171+
log.Fatal("Object test failed?!")
172+
} else {
173+
p.ReadyCheck.Retries--
174+
time.Sleep(p.ReadyCheck.Interval)
175+
}
176+
}
177+
}
178+
179+
func shellInDir(dir, cmd string) {
180+
shell("cd %s;%s", coalesce(dir, "."), cmd)
181+
}
182+
94183
func up(session string, project *Project) {
184+
if project.PreCmd != "" {
185+
shellInDir(project.Dir, project.PreCmd)
186+
}
187+
188+
// Spawn all the windows/panes
95189
for wi, w := range project.Windows {
190+
if w == nil {
191+
continue
192+
}
96193
target := fmt.Sprintf("%s:%d", session, wi)
97-
dir := coalesce(w.Dir, project.Dir, ".")
194+
dir := project.getDir(w, 0)
98195

99196
NewWindow(session, dir)
100197

101198
for pi, p := range w.Panes {
199+
if p == nil {
200+
continue
201+
}
202+
p.target = fmt.Sprintf("%s:%d.%d", session, wi, pi)
203+
dir := project.getDir(w, pi)
102204
if pi > 0 {
103205
NewPane(target, dir)
104206
}
105-
SendLine(target, p)
106207
}
107208

108209
SelectLayout(target, w.Layout)
109210
}
110211

212+
// Run the commands concurrently
213+
for _, w := range project.Windows {
214+
if w == nil {
215+
continue
216+
}
217+
addRunner(w)
218+
for _, p := range w.Panes {
219+
if p == nil {
220+
continue
221+
}
222+
addRunner(p)
223+
}
224+
}
225+
runAll()
226+
227+
// Set which window has focus
111228
for wi, w := range project.Windows {
229+
if w == nil {
230+
continue
231+
}
112232
if w.Focus {
113233
target := fmt.Sprintf("%s:%d", session, wi)
114234
SelectWindow(target)
115235
}
116236
}
237+
238+
if project.PostCmd != "" {
239+
shellInDir(project.Dir, project.PostCmd)
240+
}
117241
}
118242

119243
func down(session string, project *Project) {
120244
KillSession(session)
121245
}
122246

123-
var defaultFile = "tmux-compose.yml"
124-
125247
func main() {
126248
var overrideName string
127249
var composeFile string
128250
flag.StringVar(&overrideName, "s", "", "Override the config files session name.")
129-
flag.StringVar(&composeFile, "f", defaultFile, "Specify an alternate compose file")
251+
flag.StringVar(&composeFile, "f", "tmux-compose.yml", "Specify an alternate compose file")
130252
flag.Parse()
131253

132254
if flag.Arg(0) == "" {

runner.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"sync"
6+
"time"
7+
)
8+
9+
// A window is ready when all it's children are ready.
10+
// A pane is ready when the readycheck is successful.
11+
// A pane without a readycheck is always ready.
12+
13+
type Object struct {
14+
Name string
15+
ReadyCheck struct {
16+
Test string
17+
Interval time.Duration
18+
Retries int
19+
}
20+
DependsOn []string `yaml:"depends_on"`
21+
mutex sync.Mutex
22+
ready bool
23+
}
24+
25+
var allRunners []Runner
26+
var byName = make(map[string]Runner)
27+
28+
type Runner interface {
29+
GetObject() *Object
30+
DependenciesReady() bool
31+
IsReady() bool
32+
MarkReady()
33+
DoReadyCheck()
34+
Run()
35+
}
36+
37+
func (o *Object) GetObject() *Object {
38+
return o
39+
}
40+
41+
func (o *Object) DependenciesReady() bool {
42+
for _, name := range o.DependsOn {
43+
other := byName[name]
44+
if !other.IsReady() {
45+
return false
46+
}
47+
}
48+
49+
return true
50+
}
51+
52+
func (o *Object) IsReady() bool {
53+
o.mutex.Lock()
54+
defer o.mutex.Unlock()
55+
return o.ready
56+
}
57+
58+
func (o *Object) MarkReady() {
59+
o.mutex.Lock()
60+
o.ready = true
61+
o.mutex.Unlock()
62+
}
63+
64+
func addRunner(r Runner) {
65+
allRunners = append(allRunners, r)
66+
67+
name := r.GetObject().Name
68+
if name == "" {
69+
return
70+
}
71+
if _, ok := byName[name]; ok {
72+
log.Fatalf("Duplicate name: '%s'", name)
73+
}
74+
byName[name] = r
75+
}
76+
77+
func (o *Object) Validate() {
78+
for _, name := range o.DependsOn {
79+
if _, ok := byName[name]; !ok {
80+
log.Fatalf("Dependency does not exist: %s", name)
81+
}
82+
}
83+
}
84+
85+
func validateDependencies() {
86+
for _, r := range allRunners {
87+
r.GetObject().Validate()
88+
}
89+
}
90+
91+
func runAll() {
92+
validateDependencies()
93+
94+
var wg sync.WaitGroup
95+
96+
for _, r := range allRunners {
97+
// Don't use loop variables in goroutine
98+
wg.Add(1)
99+
100+
go func(r Runner) {
101+
for !r.DependenciesReady() {
102+
time.Sleep(10 * time.Millisecond)
103+
}
104+
r.Run()
105+
r.DoReadyCheck()
106+
r.MarkReady()
107+
108+
wg.Done()
109+
}(r)
110+
}
111+
112+
wg.Wait()
113+
}

0 commit comments

Comments
 (0)