Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 40 additions & 6 deletions hivemind.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type hivemindConfig struct {
Timeout int
NoPrefix bool
PrintTimestamps bool
ExitWithHighest bool
AsJobRunner bool
}

type hivemind struct {
Expand All @@ -31,6 +33,7 @@ type hivemind struct {
done chan bool
interrupted chan os.Signal
timeout time.Duration
jobRunner bool
}

func newHivemind(conf hivemindConfig) (h *hivemind) {
Expand All @@ -43,6 +46,7 @@ func newHivemind(conf hivemindConfig) (h *hivemind) {
}

h.output = &multiOutput{printProcName: !conf.NoPrefix, printTimestamp: conf.PrintTimestamps}
h.jobRunner = conf.AsJobRunner

entries := parseProcfile(conf.Procfile, conf.PortBase, conf.PortStep)
h.procs = make([]*process, 0)
Expand All @@ -62,17 +66,32 @@ func (h *hivemind) runProcess(proc *process) {
h.procWg.Add(1)

go func() {
procSucceed := false

defer h.procWg.Done()
defer func() { h.done <- true }()
defer func() { h.done <- procSucceed }()

proc.Run()
procSucceed = proc.Run()
}()
}

func (h *hivemind) waitForDoneOrInterrupt() {
func (h *hivemind) waitForDoneOrInterrupt() bool {
select {
case <-h.done:
case done := <-h.done:
return done
case <-h.interrupted:
return false
}
}

func (h *hivemind) waitForJobsToCompleteOrInterrupt() {
jobsCount := len(h.procs)

for jobsCompleted := 0; jobsCompleted < jobsCount; jobsCompleted++ {
succeeded := h.waitForDoneOrInterrupt()
if !succeeded {
return
}
}
}

Expand All @@ -84,7 +103,11 @@ func (h *hivemind) waitForTimeoutOrInterrupt() {
}

func (h *hivemind) waitForExit() {
h.waitForDoneOrInterrupt()
if h.jobRunner {
h.waitForJobsToCompleteOrInterrupt()
} else {
h.waitForDoneOrInterrupt()
}

for _, proc := range h.procs {
go proc.Interrupt()
Expand All @@ -97,7 +120,7 @@ func (h *hivemind) waitForExit() {
}
}

func (h *hivemind) Run() {
func (h *hivemind) Run() int {
fmt.Printf("\033]0;%s | hivemind\007", h.title)

h.done = make(chan bool, len(h.procs))
Expand All @@ -112,4 +135,15 @@ func (h *hivemind) Run() {
go h.waitForExit()

h.procWg.Wait()

exitCode := 0

for _, proc := range h.procs {
code := proc.ProcessState.ExitCode()
if code > exitCode {
exitCode = code
}
}

return exitCode
}
11 changes: 8 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ func main() {
app.HideHelp = true

app.Flags = []cli.Flag{
cli.StringFlag{Name: "title, w", EnvVar: "HIVEMIND_TITLE", Usage: "Specify a title of the application", Destination: &conf.Title},
cli.StringFlag{Name: "processes, l", EnvVar: "HIVEMIND_PROCESSES", Usage: "Specify process names to launch. Divide names with comma", Destination: &conf.ProcNames},
cli.StringFlag{Name: "title, w", EnvVar: "HIVEMIND_TITLE", Usage: "specify a title of the application", Destination: &conf.Title},
cli.StringFlag{Name: "processes, l", EnvVar: "HIVEMIND_PROCESSES", Usage: "specify process names to launch. Divide names with comma", Destination: &conf.ProcNames},
cli.IntFlag{Name: "port, p", EnvVar: "HIVEMIND_PORT,PORT", Usage: "specify a port to use as the base", Value: 5000, Destination: &conf.PortBase},
cli.IntFlag{Name: "port-step, P", EnvVar: "HIVEMIND_PORT_STEP", Usage: "specify a step to increase port number", Value: 100, Destination: &conf.PortStep},
cli.StringFlag{Name: "root, d", EnvVar: "HIVEMIND_ROOT", Usage: "specify a working directory of application. Default: directory containing the Procfile", Destination: &conf.Root},
cli.IntFlag{Name: "timeout, t", EnvVar: "HIVEMIND_TIMEOUT", Usage: "specify the amount of time (in seconds) processes have to shut down gracefully before being brutally killed", Value: 5, Destination: &conf.Timeout},
cli.BoolFlag{Name: "no-prefix", EnvVar: "HIVEMIND_NO_PREFIX", Usage: "process names will not be printed if the flag is specified", Destination: &conf.NoPrefix},
cli.BoolFlag{Name: "print-timestamps, T", EnvVar: "HIVEMIND_PRINT_TIMESTAMPS", Usage: "timestamps will be printed if the flag is specified", Destination: &conf.PrintTimestamps},
cli.BoolFlag{Name: "exit-with-highest-exit-code, e", EnvVar: "HIVEMIND_EXIT_WITH_HIGHEST_EXIT_CODE", Usage: "exit hivemind with highest exit code from all processes", Destination: &conf.ExitWithHighest},
cli.BoolFlag{Name: "as-job-runner", EnvVar: "HIVEMIND_AS_JOB_RUNNER", Usage: "be a job runner instead: Will wait for all to finish with exit code 0, or fail.", Destination: &conf.AsJobRunner},
}

app.Action = func(c *cli.Context) error {
Expand Down Expand Up @@ -65,7 +67,10 @@ func main() {
conf.Root, err = filepath.Abs(conf.Root)
fatalOnErr(err)

newHivemind(conf).Run()
exitCode := newHivemind(conf).Run()
if exitCode > 0 && conf.ExitWithHighest {
return cli.NewExitError("At least one process failed", exitCode)
}

return nil
}
Expand Down
4 changes: 3 additions & 1 deletion process.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (p *process) Running() bool {
return p.Process != nil && p.ProcessState == nil
}

func (p *process) Run() {
func (p *process) Run() bool {
p.output.PipeOutput(p)
defer p.output.ClosePipe(p)

Expand All @@ -66,8 +66,10 @@ func (p *process) Run() {

if err := p.Cmd.Run(); err != nil {
p.writeErr(err)
return false
} else {
p.writeLine([]byte("\033[1mProcess exited\033[0m"))
return true
}
}

Expand Down
23 changes: 23 additions & 0 deletions test-as-job-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

echo
echo "As process manager"
dist/hivemind --exit-with-highest-exit-code - <<PROCFILE
job1: echo "job1 is running"; sleep 0.5; exit 13; echo "Done"
job2: echo "job2 is running"; sleep 2; echo "Done"
PROCFILE

echo
echo "As Job Runner, all completing"
dist/hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE
job1: echo "short job1 is running"; sleep 0.5; echo "Done"
job2: echo "short job2 is running"; sleep 1.0; echo "Done"
job3: echo "long job3 is running"; sleep 2; echo "Done"
PROCFILE

echo
echo "As Job Runner, one exits and fail fast"
dist/hivemind --as-job-runner --exit-with-highest-exit-code - <<PROCFILE
job1: echo "job1 is running"; sleep 0.5; echo "Fail"; exit 13
job2: echo "job2 is running"; sleep 0.5; echo 1; sleep 0.5; echo 2; sleep 0.5; echo 3; sleep 1; echo "Done"
PROCFILE