├── .gitignore ├── eg ├── .env ├── spawner ├── Procfile ├── error ├── spawnee ├── utf8 └── ticker ├── fixtures ├── envs │ ├── .env1 │ └── .env2 ├── multiline │ ├── Procfile │ └── stdout.rb ├── large_stdout │ ├── Procfile │ └── stdout.rb └── configs │ └── .forego ├── .dockerignore ├── Godeps ├── Readme └── Godeps.json ├── go.mod ├── error.go ├── util.go ├── config_test.go ├── version.go ├── Makefile ├── go.sum ├── version_test.go ├── env_test.go ├── config.go ├── windows.go ├── bin └── release ├── update.go ├── main.go ├── process.go ├── unix.go ├── README.md ├── run.go ├── command.go ├── barrier.go ├── help.go ├── env.go ├── procfile.go ├── .circleci └── config.yml ├── outlet.go ├── start_test.go └── start.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | /forego 3 | -------------------------------------------------------------------------------- /eg/.env: -------------------------------------------------------------------------------- 1 | FOO=bar 2 | 3 | #BAZ=buz 4 | -------------------------------------------------------------------------------- /fixtures/envs/.env1: -------------------------------------------------------------------------------- 1 | env1=present 2 | -------------------------------------------------------------------------------- /fixtures/envs/.env2: -------------------------------------------------------------------------------- 1 | env2=present 2 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .git 3 | /forego 4 | -------------------------------------------------------------------------------- /fixtures/multiline/Procfile: -------------------------------------------------------------------------------- 1 | stdout1: ruby ./stdout.rb 2 | stdout2: ruby ./stdout.rb 3 | -------------------------------------------------------------------------------- /fixtures/large_stdout/Procfile: -------------------------------------------------------------------------------- 1 | stdout1: ruby ./stdout.rb 2 | stdout2: ruby ./stdout.rb 3 | -------------------------------------------------------------------------------- /eg/spawner: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ./spawnee A & 4 | ./spawnee B & 5 | ./spawnee C & 6 | 7 | wait 8 | -------------------------------------------------------------------------------- /eg/Procfile: -------------------------------------------------------------------------------- 1 | ticker: ruby ./ticker $PORT 2 | error: ruby ./error 3 | utf8: ruby ./utf8 4 | spawner: ./spawner 5 | -------------------------------------------------------------------------------- /eg/error: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | 5 | puts "will error in 5s" 6 | sleep 5 7 | raise "Dying" 8 | -------------------------------------------------------------------------------- /fixtures/configs/.forego: -------------------------------------------------------------------------------- 1 | procfile: Procfile.dev 2 | concurrency: foo=2,bar=3 3 | port: 15000 4 | shutdown_grace_time: 30 5 | -------------------------------------------------------------------------------- /Godeps/Readme: -------------------------------------------------------------------------------- 1 | This directory tree is generated automatically by godep. 2 | 3 | Please do not edit. 4 | 5 | See https://github.com/tools/godep for more information. 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ddollar/forego 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/daviddengcn/go-colortext v0.0.0-20150719211842-3b18c8575a43 7 | github.com/subosito/gotenv v0.1.0 8 | ) 9 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | func handleError(err error) { 9 | if err != nil { 10 | fmt.Println("ERROR:", err) 11 | os.Exit(1) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /eg/spawnee: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | NAME="$1" 4 | 5 | sigterm() { 6 | echo "$NAME: got sigterm" 7 | } 8 | 9 | #trap sigterm SIGTERM 10 | 11 | while true; do 12 | echo "$NAME: ping $$" 13 | sleep 1 14 | done 15 | -------------------------------------------------------------------------------- /fixtures/large_stdout/stdout.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | 5 | 10.times do |i| 6 | puts "sample log message... " * rand(i*1000) + "ok - #{i}" 7 | sleep 1 8 | end 9 | 10 | puts "finish!" 11 | -------------------------------------------------------------------------------- /fixtures/multiline/stdout.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | 5 | puts "a" 6 | sleep 1 7 | puts "a\nb" 8 | sleep 1 9 | puts "a\nb\nc" 10 | sleep 1 11 | puts "a\nb\nc\nd" 12 | sleep 1 13 | puts "finish!" 14 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | ) 8 | 9 | var stdout io.Writer = os.Stdout 10 | 11 | func Println(a ...interface{}) (n int, err error) { 12 | return fmt.Fprintln(stdout, a...) 13 | } 14 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestReadOptionFile(t *testing.T) { 6 | config_file := "./fixtures/options/.forego" 7 | _, err := ReadConfig(config_file) 8 | if err != nil { 9 | t.Fatalf("Could not read config file: %s", err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /eg/utf8: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # encoding: BINARY 3 | 4 | $stdout.sync = true 5 | 6 | while true 7 | puts "\u65e5\u672c\u8a9e\u6587\u5b57\u5217" 8 | puts "\u0915\u0932\u094d\u0907\u0928\u0643\u0637\u0628\u041a\u0430\u043b\u0438\u043d\u0430" 9 | puts "\xff\x03" 10 | sleep 1 11 | end 12 | -------------------------------------------------------------------------------- /eg/ticker: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $stdout.sync = true 4 | 5 | %w( SIGINT SIGTERM ).each do |signal| 6 | trap(signal) do 7 | puts "received #{signal} but i'm ignoring it!" 8 | end 9 | end 10 | 11 | while true 12 | puts "tick: #{ARGV.inspect} -- FOO:#{ENV["FOO"]}" 13 | sleep 1 14 | end 15 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var Version = "dev" 4 | 5 | var cmdVersion = &Command{ 6 | Run: runVersion, 7 | Usage: "version", 8 | Short: "Display current version", 9 | Long: ` 10 | Display current version 11 | 12 | Examples: 13 | 14 | forego version 15 | `, 16 | } 17 | 18 | func runVersion(cmd *Command, args []string) { 19 | Println(Version) 20 | } 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = forego 2 | SRC = $(shell find . -name '*.go' -not -path './vendor/*') 3 | 4 | .PHONY: all build clean lint release test 5 | 6 | all: build 7 | 8 | build: $(BIN) 9 | 10 | clean: 11 | rm -f $(BIN) 12 | 13 | lint: $(SRC) 14 | go fmt 15 | 16 | release: 17 | bin/release 18 | 19 | test: lint build 20 | go test -v -race -cover ./... 21 | 22 | $(BIN): $(SRC) 23 | go build -o $@ 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/daviddengcn/go-colortext v0.0.0-20150719211842-3b18c8575a43 h1:WOjNHsegRa6W+i7+V0g9GlGTKj5c2CzvENZBOYLYxMQ= 2 | github.com/daviddengcn/go-colortext v0.0.0-20150719211842-3b18c8575a43/go.mod h1:dv4zxwHi5C/8AeI+4gX4dCWOIvNi7I6JCSX0HvlKPgE= 3 | github.com/subosito/gotenv v0.1.0 h1:qrPfMw+dkLrA77HtD6S3ppWB9JjV8jhKMmeSpnuvd3Q= 4 | github.com/subosito/gotenv v0.1.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 5 | -------------------------------------------------------------------------------- /version_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestVersion(t *testing.T) { 9 | var b bytes.Buffer 10 | stdout = &b 11 | cmdVersion.Run(cmdVersion, []string{}) 12 | output := b.String() 13 | assertEqual(t, output, "dev\n") 14 | } 15 | 16 | func assertEqual(t *testing.T, a, b interface{}) { 17 | if a != b { 18 | t.Fatalf(`Expected %#v to equal %#v`, a, b) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Godeps/Godeps.json: -------------------------------------------------------------------------------- 1 | { 2 | "ImportPath": "github.com/ddollar/forego", 3 | "GoVersion": "go1.6", 4 | "GodepVersion": "v61", 5 | "Packages": [ 6 | "./..." 7 | ], 8 | "Deps": [ 9 | { 10 | "ImportPath": "github.com/daviddengcn/go-colortext", 11 | "Rev": "3b18c8575a432453d41fdafb340099fff5bba2f7" 12 | }, 13 | { 14 | "ImportPath": "github.com/subosito/gotenv", 15 | "Comment": "v0.1.0", 16 | "Rev": "a37a0e8fb3298354bf97daad07b38feb2d0fa263" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /env_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func TestMultipleEnvironmentFiles(t *testing.T) { 6 | envs := []string{"fixtures/envs/.env1", "fixtures/envs/.env2"} 7 | env, err := loadEnvs(envs) 8 | 9 | if err != nil { 10 | t.Fatalf("Could not read environments: %s", err) 11 | } 12 | 13 | if env["env1"] == "" { 14 | t.Fatalf("$env1 should be present and is not") 15 | } 16 | 17 | if env["env2"] == "" { 18 | t.Fatalf("$env2 should be present and is not") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/subosito/gotenv" 7 | ) 8 | 9 | type Config map[string]string 10 | 11 | func ReadConfig(filename string) (Config, error) { 12 | if _, err := os.Stat(filename); os.IsNotExist(err) { 13 | return make(Config), nil 14 | } 15 | fd, err := os.Open(filename) 16 | if err != nil { 17 | return nil, err 18 | } 19 | defer fd.Close() 20 | config := make(Config) 21 | for key, val := range gotenv.Parse(fd) { 22 | config[key] = val 23 | } 24 | return config, nil 25 | } 26 | -------------------------------------------------------------------------------- /windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package main 4 | 5 | import ( 6 | "syscall" 7 | ) 8 | 9 | const osHaveSigTerm = false 10 | 11 | func ShellInvocationCommand(interactive bool, root, command string) []string { 12 | return []string{"cmd", "/C", command} 13 | } 14 | 15 | func (p *Process) PlatformSpecificInit() { 16 | // NOP on windows for now. 17 | return 18 | } 19 | 20 | func (p *Process) SendSigTerm() { 21 | panic("SendSigTerm() not implemented on this platform") 22 | } 23 | 24 | func (p *Process) SendSigKill() { 25 | p.Signal(syscall.SIGKILL) 26 | } 27 | -------------------------------------------------------------------------------- /bin/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | version=$(date +%Y%m%d%H%M%S) 4 | 5 | curl -s ${GITHUB_KEY_URL} -o /tmp/github.key 6 | chmod 0400 /tmp/github.key 7 | git tag ${version} 8 | GIT_SSH_COMMAND='ssh -i /tmp/github.key' git push origin ${version} 9 | 10 | curl -s https://bin.equinox.io/c/mBWdkfai63v/release-tool-stable-linux-amd64.tgz | sudo tar xz -C /usr/local/bin 11 | curl -s ${EQUINOX_KEY_URL} -o /tmp/equinox.key 12 | equinox release --version=${version} --platforms="darwin_386 darwin_amd64 linux_386 linux_amd64 linux_arm linux_arm64 windows_386 windows_amd64" --channel=stable --signing-key=/tmp/equinox.key --app=${EQUINOX_APP} --token=${EQUINOX_TOKEN} 13 | -------------------------------------------------------------------------------- /update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var cmdUpdate = &Command{ 4 | Run: runUpdate, 5 | Usage: "update", 6 | Short: "Update forego", 7 | Long: ` 8 | Update forego 9 | 10 | Examples: 11 | 12 | forego update 13 | `, 14 | } 15 | 16 | func init() { 17 | } 18 | 19 | func runUpdate(cmd *Command, args []string) { 20 | // if Version == "dev" { 21 | // fmt.Println("ERROR: can't update dev version") 22 | // return 23 | // } 24 | // d := dist.NewDist("ddollar/forego", Version) 25 | // to, err := d.Update() 26 | // if err != nil { 27 | // fmt.Printf("ERROR: %s\n", err) 28 | // } else { 29 | // fmt.Printf("updated to %s\n", to) 30 | // } 31 | } 32 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "os" 4 | 5 | var commands = []*Command{ 6 | cmdStart, 7 | cmdRun, 8 | // cmdUpdate, 9 | cmdVersion, 10 | cmdHelp, 11 | } 12 | 13 | var allowUpdate string = "true" 14 | 15 | func main() { 16 | args := os.Args[1:] 17 | if len(args) < 1 { 18 | usage() 19 | } 20 | 21 | if allowUpdate == "false" { 22 | cmdUpdate.Disabled = true 23 | } 24 | 25 | for _, cmd := range commands { 26 | if cmd.Name() == args[0] && cmd.Runnable() { 27 | cmd.Flag.Usage = func() { 28 | cmd.printUsage() 29 | } 30 | if err := cmd.Flag.Parse(args[1:]); err != nil { 31 | os.Exit(2) 32 | } 33 | cmd.Run(cmd, cmd.Flag.Args()) 34 | return 35 | } 36 | } 37 | usage() 38 | } 39 | -------------------------------------------------------------------------------- /process.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "syscall" 7 | ) 8 | 9 | type Process struct { 10 | Command string 11 | Env Env 12 | Interactive bool 13 | 14 | *exec.Cmd 15 | } 16 | 17 | func NewProcess(workdir, command string, env Env, interactive bool) (p *Process) { 18 | argv := ShellInvocationCommand(interactive, workdir, command) 19 | return &Process{ 20 | command, env, interactive, exec.Command(argv[0], argv[1:]...), 21 | } 22 | } 23 | 24 | func (p *Process) Start() error { 25 | p.Cmd.Env = p.Env.asArray() 26 | p.PlatformSpecificInit() 27 | return p.Cmd.Start() 28 | } 29 | 30 | func (p *Process) Signal(signal syscall.Signal) error { 31 | group, err := os.FindProcess(-1 * p.Process.Pid) 32 | if err == nil { 33 | err = group.Signal(signal) 34 | } 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /unix.go: -------------------------------------------------------------------------------- 1 | // +build darwin freebsd linux netbsd openbsd 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "syscall" 8 | ) 9 | 10 | const osHaveSigTerm = true 11 | 12 | func ShellInvocationCommand(interactive bool, root, command string) []string { 13 | shellArgument := "-c" 14 | if interactive { 15 | shellArgument = "-ic" 16 | } 17 | shellCommand := fmt.Sprintf("cd \"%s\"; source .profile 2>/dev/null; exec %s", root, command) 18 | return []string{"sh", shellArgument, shellCommand} 19 | } 20 | 21 | func (p *Process) PlatformSpecificInit() { 22 | if !p.Interactive { 23 | p.SysProcAttr = &syscall.SysProcAttr{} 24 | p.SysProcAttr.Setsid = true 25 | } 26 | return 27 | } 28 | 29 | func (p *Process) SendSigTerm() { 30 | p.Signal(syscall.SIGTERM) 31 | } 32 | 33 | func (p *Process) SendSigKill() { 34 | p.Signal(syscall.SIGKILL) 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## forego 2 | 3 | 4 | 5 | 6 | 7 | [Foreman](https://github.com/ddollar/foreman) in Go. 8 | 9 | ### Installation 10 | 11 | [Downloads](https://dl.equinox.io/ddollar/forego/stable) 12 | 13 | ##### Compile from Source 14 | 15 | $ go get -u github.com/ddollar/forego 16 | 17 | ### Usage 18 | 19 | $ cat Procfile 20 | web: bin/web start -p $PORT 21 | worker: bin/worker queue=FOO 22 | 23 | $ forego start 24 | web | listening on port 5000 25 | worker | listening to queue FOO 26 | 27 | Use `forego help` to get a list of available commands, and `forego help 28 | ` for more detailed help on a specific command. 29 | 30 | ### License 31 | 32 | Apache 2.0 © 2015 David Dollar 33 | -------------------------------------------------------------------------------- /run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | var cmdRun = &Command{ 9 | Run: runRun, 10 | Usage: "run [-e env] [-p port]", 11 | Short: "Run a one-off command", 12 | Long: ` 13 | Run a one-off command 14 | 15 | Examples: 16 | 17 | forego run bin/migrate 18 | `, 19 | } 20 | 21 | var runEnvs envFiles 22 | 23 | func init() { 24 | cmdRun.Flag.Var(&runEnvs, "e", "env") 25 | } 26 | 27 | func runRun(cmd *Command, args []string) { 28 | if len(args) < 1 { 29 | cmd.printUsage() 30 | os.Exit(1) 31 | } 32 | workDir, err := os.Getwd() 33 | if err != nil { 34 | handleError(err) 35 | } 36 | 37 | env, err := loadEnvs(runEnvs) 38 | handleError(err) 39 | 40 | const interactive = true 41 | ps := NewProcess(workDir, strings.Join(args, " "), env, interactive) 42 | ps.Stdin = os.Stdin 43 | ps.Stdout = os.Stdout 44 | ps.Stderr = os.Stderr 45 | 46 | err = ps.Start() 47 | handleError(err) 48 | 49 | err = ps.Wait() 50 | handleError(err) 51 | } 52 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | var flagEnv string 10 | var flagProcfile string 11 | 12 | type Command struct { 13 | // args does not include the command name 14 | Run func(cmd *Command, args []string) 15 | Flag flag.FlagSet 16 | 17 | Disabled bool 18 | Usage string // first word is the command name 19 | Short string // `forego help` output 20 | Long string // `forego help cmd` output 21 | } 22 | 23 | func (c *Command) printUsage() { 24 | if c.Runnable() { 25 | fmt.Printf("Usage: forego %s\n\n", c.Usage) 26 | } 27 | fmt.Println(strings.Trim(c.Long, "\n")) 28 | } 29 | 30 | func (c *Command) Name() string { 31 | name := c.Usage 32 | i := strings.Index(name, " ") 33 | if i >= 0 { 34 | name = name[:i] 35 | } 36 | return name 37 | } 38 | 39 | func (c *Command) Runnable() bool { 40 | return c.Run != nil && c.Disabled != true 41 | } 42 | 43 | func (c *Command) List() bool { 44 | return c.Short != "" 45 | } 46 | -------------------------------------------------------------------------------- /barrier.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Direct import of https://github.com/pwaller/barrier/blob/master/barrier.go 8 | 9 | // The zero of Barrier is a ready-to-use value 10 | type Barrier struct { 11 | channel chan struct{} 12 | fall, initialize sync.Once 13 | FallHook func() 14 | } 15 | 16 | func (b *Barrier) init() { 17 | b.initialize.Do(func() { b.channel = make(chan struct{}) }) 18 | } 19 | 20 | // `b.Fall()` can be called any number of times and causes the channel returned 21 | // by `b.Barrier()` to become closed (permanently available for immediate reading) 22 | func (b *Barrier) Fall() { 23 | b.init() 24 | b.fall.Do(func() { 25 | if b.FallHook != nil { 26 | b.FallHook() 27 | } 28 | close(b.channel) 29 | }) 30 | } 31 | 32 | // When `b.Fall()` is called, the channel returned by Barrier() is closed 33 | // (and becomes always readable) 34 | func (b *Barrier) Barrier() <-chan struct{} { 35 | b.init() 36 | return b.channel 37 | } 38 | -------------------------------------------------------------------------------- /help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "text/template" 8 | ) 9 | 10 | var cmdHelp = &Command{ 11 | Usage: "help [topic]", 12 | Short: "Show this help", 13 | Long: `Help shows usage for a command.`, 14 | } 15 | 16 | func init() { 17 | cmdHelp.Run = runHelp // break init loop 18 | } 19 | 20 | func runHelp(cmd *Command, args []string) { 21 | if len(args) == 0 { 22 | printUsage() 23 | return 24 | } 25 | if len(args) != 1 { 26 | log.Fatal("too many arguments") 27 | } 28 | 29 | for _, cmd := range commands { 30 | if cmd.Name() == args[0] { 31 | cmd.printUsage() 32 | return 33 | } 34 | } 35 | 36 | fmt.Fprintf(os.Stderr, "Unknown help topic: %q. Run 'forego help'.\n", args[0]) 37 | os.Exit(2) 38 | } 39 | 40 | var usageTemplate = template.Must(template.New("usage").Parse(` 41 | Usage: forego [] 42 | 43 | Available commands:{{range .Commands}}{{if .Runnable}}{{if .List}} 44 | {{.Name | printf "%-8s"}} {{.Short}}{{end}}{{end}}{{end}} 45 | 46 | Run 'forego help [command]' for details. 47 | `[1:])) 48 | 49 | func printUsage() { 50 | usageTemplate.Execute(os.Stdout, struct { 51 | Commands []*Command 52 | }{ 53 | commands, 54 | }) 55 | } 56 | 57 | func usage() { 58 | printUsage() 59 | os.Exit(2) 60 | } 61 | -------------------------------------------------------------------------------- /env.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | 8 | "github.com/subosito/gotenv" 9 | ) 10 | 11 | var envEntryRegexp = regexp.MustCompile("^([A-Za-z_0-9]+)=(.*)$") 12 | 13 | type Env map[string]string 14 | 15 | type envFiles []string 16 | 17 | func (e *envFiles) String() string { 18 | return fmt.Sprintf("%s", *e) 19 | } 20 | 21 | func (e *envFiles) Set(value string) error { 22 | *e = append(*e, value) 23 | return nil 24 | } 25 | 26 | func loadEnvs(files []string) (Env, error) { 27 | if len(files) == 0 { 28 | files = []string{".env"} 29 | } 30 | 31 | env := make(Env) 32 | 33 | for _, file := range files { 34 | tmpEnv, err := ReadEnv(file) 35 | 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | // Merge the file I just read into the env. 41 | for k, v := range tmpEnv { 42 | env[k] = v 43 | } 44 | } 45 | return env, nil 46 | } 47 | 48 | func ReadEnv(filename string) (Env, error) { 49 | if _, err := os.Stat(filename); os.IsNotExist(err) { 50 | return make(Env), nil 51 | } 52 | fd, err := os.Open(filename) 53 | if err != nil { 54 | return nil, err 55 | } 56 | defer fd.Close() 57 | env := make(Env) 58 | for key, val := range gotenv.Parse(fd) { 59 | env[key] = val 60 | } 61 | return env, nil 62 | } 63 | 64 | func (e *Env) asArray() (env []string) { 65 | for _, pair := range os.Environ() { 66 | env = append(env, pair) 67 | } 68 | for name, val := range *e { 69 | env = append(env, fmt.Sprintf("%s=%s", name, val)) 70 | } 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /procfile.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "math" 8 | "os" 9 | "regexp" 10 | ) 11 | 12 | var procfileEntryRegexp = regexp.MustCompile("^([A-Za-z0-9_-]+):\\s*(.+)$") 13 | 14 | type ProcfileEntry struct { 15 | Name string 16 | Command string 17 | } 18 | 19 | type Procfile struct { 20 | Entries []ProcfileEntry 21 | } 22 | 23 | func ReadProcfile(filename string) (*Procfile, error) { 24 | fd, err := os.Open(filename) 25 | if err != nil { 26 | return nil, err 27 | } 28 | defer fd.Close() 29 | return parseProcfile(fd) 30 | } 31 | 32 | func (pf *Procfile) HasProcess(name string) (exists bool) { 33 | for _, entry := range pf.Entries { 34 | if name == entry.Name { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | 41 | func (pf *Procfile) LongestProcessName(concurrency map[string]int) (longest int) { 42 | longest = 6 // length of forego 43 | for _, entry := range pf.Entries { 44 | thisLen := len(entry.Name) 45 | // The "." 46 | thisLen += 1 47 | if c, ok := concurrency[entry.Name]; ok { 48 | // Add the number of digits 49 | thisLen += int(math.Log10(float64(c))) + 1 50 | } else { 51 | // The index number after the dot. 52 | thisLen += 1 53 | } 54 | if thisLen > longest { 55 | longest = thisLen 56 | } 57 | } 58 | return 59 | } 60 | 61 | func parseProcfile(r io.Reader) (*Procfile, error) { 62 | pf := new(Procfile) 63 | scanner := bufio.NewScanner(r) 64 | for scanner.Scan() { 65 | parts := procfileEntryRegexp.FindStringSubmatch(scanner.Text()) 66 | if len(parts) > 0 { 67 | pf.Entries = append(pf.Entries, ProcfileEntry{parts[1], parts[2]}) 68 | } 69 | } 70 | if err := scanner.Err(); err != nil { 71 | return nil, fmt.Errorf("Reading Procfile: %s", err) 72 | } 73 | return pf, nil 74 | } 75 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | # specify the version 9 | - image: circleci/golang:1.9 10 | 11 | working_directory: /go/src/github.com/ddollar/forego 12 | 13 | environment: 14 | TEST_RESULTS: /tmp/test-results 15 | 16 | steps: 17 | - checkout 18 | - run: go get github.com/daviddengcn/go-colortext 19 | - run: go get github.com/subosito/gotenv 20 | 21 | # For capturing test results 22 | - run: go get github.com/jstemmer/go-junit-report 23 | - run: mkdir -p $TEST_RESULTS 24 | 25 | # specify any bash command here prefixed with `run: ` 26 | - run: 27 | name: Run unit tests 28 | command: | 29 | trap "go-junit-report <${TEST_RESULTS}/forego-tests.log > ${TEST_RESULTS}/forego-tests-report.xml" EXIT 30 | make test | tee ${TEST_RESULTS}/forego-tests.log 31 | 32 | - store_artifacts: 33 | path: /tmp/test-results 34 | destination: raw-test-output 35 | 36 | - store_test_results: 37 | path: /tmp/test-results 38 | release: 39 | docker: 40 | - image: circleci/golang:1.9 41 | 42 | working_directory: /go/src/github.com/ddollar/forego 43 | 44 | steps: 45 | - checkout 46 | - run: go get github.com/daviddengcn/go-colortext 47 | - run: go get github.com/subosito/gotenv 48 | 49 | - run: make release 50 | 51 | workflows: 52 | version: 2 53 | deployment: 54 | jobs: 55 | - build 56 | - release: 57 | requires: 58 | - build 59 | filters: 60 | branches: 61 | only: master 62 | -------------------------------------------------------------------------------- /outlet.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "sync" 10 | 11 | ct "github.com/daviddengcn/go-colortext" 12 | ) 13 | 14 | type OutletFactory struct { 15 | Padding int 16 | 17 | sync.Mutex 18 | } 19 | 20 | var colors = []ct.Color{ 21 | ct.Cyan, 22 | ct.Yellow, 23 | ct.Green, 24 | ct.Magenta, 25 | ct.Red, 26 | ct.Blue, 27 | } 28 | 29 | func NewOutletFactory() (of *OutletFactory) { 30 | return new(OutletFactory) 31 | } 32 | 33 | func (of *OutletFactory) LineReader(wg *sync.WaitGroup, name string, index int, r io.Reader, isError bool) { 34 | defer wg.Done() 35 | 36 | color := colors[index%len(colors)] 37 | 38 | reader := bufio.NewReader(r) 39 | 40 | var buffer bytes.Buffer 41 | 42 | for { 43 | buf := make([]byte, 1024) 44 | 45 | if n, err := reader.Read(buf); err != nil { 46 | return 47 | } else { 48 | buf = buf[:n] 49 | } 50 | 51 | for { 52 | i := bytes.IndexByte(buf, '\n') 53 | if i < 0 { 54 | break 55 | } 56 | buffer.Write(buf[0:i]) 57 | of.WriteLine(name, buffer.String(), color, ct.None, isError) 58 | buffer.Reset() 59 | buf = buf[i+1:] 60 | } 61 | 62 | buffer.Write(buf) 63 | } 64 | } 65 | 66 | func (of *OutletFactory) SystemOutput(str string) { 67 | of.WriteLine("forego", str, ct.White, ct.None, false) 68 | } 69 | 70 | func (of *OutletFactory) ErrorOutput(str string) { 71 | fmt.Printf("ERROR: %s\n", str) 72 | os.Exit(1) 73 | } 74 | 75 | // Write out a single coloured line 76 | func (of *OutletFactory) WriteLine(left, right string, leftC, rightC ct.Color, isError bool) { 77 | of.Lock() 78 | defer of.Unlock() 79 | 80 | ct.ChangeColor(leftC, true, ct.None, false) 81 | formatter := fmt.Sprintf("%%-%ds | ", of.Padding) 82 | fmt.Printf(formatter, left) 83 | 84 | if isError { 85 | ct.ChangeColor(ct.Red, true, ct.None, true) 86 | } else { 87 | ct.ResetColor() 88 | } 89 | fmt.Println(right) 90 | if isError { 91 | ct.ResetColor() 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /start_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestParseConcurrencyFlagEmpty(t *testing.T) { 9 | c, err := parseConcurrency("") 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | if len(c) > 0 { 14 | t.Fatal("expected no concurrency settings with ''") 15 | } 16 | } 17 | 18 | func TestParseConcurrencyFlagSimle(t *testing.T) { 19 | c, err := parseConcurrency("foo=2") 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | 24 | if len(c) != 1 { 25 | t.Fatal("expected 1 concurrency settings with 'foo=2'") 26 | } 27 | 28 | if c["foo"] != 2 { 29 | t.Fatal("expected concurrency settings of 2 with 'foo=2'") 30 | } 31 | } 32 | 33 | func TestParseConcurrencyFlagMultiple(t *testing.T) { 34 | c, err := parseConcurrency("foo=2,bar=3") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | 39 | if len(c) != 2 { 40 | t.Fatal("expected 1 concurrency settings with 'foo=2'") 41 | } 42 | 43 | if c["foo"] != 2 { 44 | t.Fatal("expected concurrency settings of 2 with 'foo=2'") 45 | } 46 | 47 | if c["bar"] != 3 { 48 | t.Fatal("expected concurrency settings of 3 with 'bar=3'") 49 | } 50 | } 51 | 52 | func TestParseConcurrencyFlagNonInt(t *testing.T) { 53 | _, err := parseConcurrency("foo=x") 54 | if err == nil { 55 | t.Fatal("foo=x should fail") 56 | } 57 | } 58 | 59 | func TestParseConcurrencyFlagWhitespace(t *testing.T) { 60 | c, err := parseConcurrency("foo = 2, bar = 3") 61 | if err != nil { 62 | t.Fatalf("foo = 2, bar = 4 should not fail:%s", err) 63 | } 64 | 65 | if len(c) != 2 { 66 | t.Fatal("expected 1 concurrency settings with 'foo=2'") 67 | } 68 | 69 | if c["foo"] != 2 { 70 | t.Fatal("expected concurrency settings of 2 with 'foo=2'") 71 | } 72 | 73 | if c["bar"] != 3 { 74 | t.Fatal("expected concurrency settings of 3 with 'bar=3'") 75 | } 76 | } 77 | 78 | func TestParseConcurrencyFlagMultipleEquals(t *testing.T) { 79 | _, err := parseConcurrency("foo===2") 80 | if err == nil { 81 | t.Fatalf("foo===2 should fail: %s", err) 82 | } 83 | } 84 | 85 | func TestParseConcurrencyFlagNoValue(t *testing.T) { 86 | _, err := parseConcurrency("foo=") 87 | if err == nil { 88 | t.Fatalf("foo= should fail: %s", err) 89 | } 90 | 91 | _, err = parseConcurrency("=") 92 | if err == nil { 93 | t.Fatalf("= should fail: %s", err) 94 | } 95 | 96 | _, err = parseConcurrency("=1") 97 | if err == nil { 98 | t.Fatalf("= should fail: %s", err) 99 | } 100 | 101 | _, err = parseConcurrency(",") 102 | if err == nil { 103 | t.Fatalf(", should fail: %s", err) 104 | } 105 | 106 | _, err = parseConcurrency(",,,") 107 | if err == nil { 108 | t.Fatalf(",,, should fail: %s", err) 109 | } 110 | 111 | } 112 | 113 | func TestPortFromEnv(t *testing.T) { 114 | env := make(Env) 115 | port, err := basePort(env) 116 | if err != nil { 117 | t.Fatalf("Can not get base port: %s", err) 118 | } 119 | if port != 5000 { 120 | t.Fatal("Base port should be 5000") 121 | } 122 | 123 | os.Setenv("PORT", "4000") 124 | port, err = basePort(env) 125 | if err != nil { 126 | t.Fatal("Can not get port: %s", err) 127 | } 128 | if port != 4000 { 129 | t.Fatal("Base port should be 4000") 130 | } 131 | 132 | env["PORT"] = "6000" 133 | port, err = basePort(env) 134 | if err != nil { 135 | t.Fatalf("Can not get base port: %s", err) 136 | } 137 | if port != 6000 { 138 | t.Fatal("Base port should be 6000") 139 | } 140 | 141 | env["PORT"] = "forego" 142 | port, err = basePort(env) 143 | if err == nil { 144 | t.Fatalf("Port 'forego' should fail: %s", err) 145 | } 146 | 147 | } 148 | 149 | func TestConfigBeOverrideByForegoFile(t *testing.T) { 150 | var procfile = "Profile" 151 | var port = 5000 152 | var concurrency string = "web=2" 153 | var gracetime int = 3 154 | err := readConfigFile("./fixtures/configs/.forego", &procfile, &port, &concurrency, &gracetime) 155 | 156 | if err != nil { 157 | t.Fatalf("Cannot set default values from forego config file") 158 | } 159 | 160 | if procfile != "Procfile.dev" { 161 | t.Fatal("Procfile should be Procfile.dev") 162 | } 163 | 164 | if port != 15000 { 165 | t.Fatal("port should be 15000, got %d", port) 166 | } 167 | 168 | if concurrency != "foo=2,bar=3" { 169 | t.Fatal("concurrency should be 'foo=2,bar=3', got %s", concurrency) 170 | } 171 | 172 | if gracetime != 30 { 173 | t.Fatal("gracetime should be 3, got %d", gracetime) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /start.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | const defaultPort = 5000 17 | const defaultShutdownGraceTime = 3 18 | 19 | var flagPort int 20 | var flagConcurrency string 21 | var flagRestart bool 22 | var flagShutdownGraceTime int 23 | var envs envFiles 24 | 25 | var cmdStart = &Command{ 26 | Run: runStart, 27 | Usage: "start [process name] [-f procfile] [-e env] [-p port] [-c concurrency] [-r] [-t shutdown_grace_time]", 28 | Short: "Start the application", 29 | Long: ` 30 | Start the application specified by a Procfile. The directory containing the 31 | Procfile is used as the working directory. 32 | 33 | The following options are available: 34 | 35 | -f procfile Set the Procfile. Defaults to './Procfile'. 36 | 37 | -e env Add an environment file, containing variables in 'KEY=value', or 38 | 'export KEY=value', form. These variables will be set in the 39 | environment of each process. If no environment files are 40 | specified, a file called .env is used if it exists. 41 | 42 | -p port Sets the base port number; each process will have a PORT variable 43 | in its environment set to a unique value based on this. This may 44 | also be set via a PORT variable in the environment, or in an 45 | environment file, and otherwise defaults to 5000. 46 | 47 | -c concurrency 48 | Start a specific number of instances of each process. The 49 | argument should be in the format 'foo=1,bar=2,baz=0'. Use the 50 | name 'all' to set the default number of instances. By default, 51 | one instance of each process is started. 52 | 53 | -r Restart a process which exits. Without this, if a process exits, 54 | forego will kill all other processes and exit. 55 | 56 | -t shutdown_grace_time 57 | Set the shutdown grace time that each process is given after 58 | being asked to stop. Once this grace time expires, the process is 59 | forcibly terminated. By default, it is 3 seconds. 60 | 61 | If there is a file named .forego in the current directory, it will be read in 62 | the same way as an environment file, and the values of variables procfile, port, 63 | concurrency, and shutdown_grace_time used to change the corresponding default 64 | values. 65 | 66 | Examples: 67 | 68 | # start every process 69 | forego start 70 | 71 | # start only the web process 72 | forego start web 73 | 74 | # start every process specified in Procfile.test, with the environment specified in .env.test 75 | forego start -f Procfile.test -e .env.test 76 | 77 | # start every process, with a timeout of 30 seconds 78 | forego start -t 30 79 | `, 80 | } 81 | 82 | func init() { 83 | cmdStart.Flag.StringVar(&flagProcfile, "f", "Procfile", "procfile") 84 | cmdStart.Flag.Var(&envs, "e", "env") 85 | cmdStart.Flag.IntVar(&flagPort, "p", defaultPort, "port") 86 | cmdStart.Flag.StringVar(&flagConcurrency, "c", "", "concurrency") 87 | cmdStart.Flag.BoolVar(&flagRestart, "r", false, "restart") 88 | cmdStart.Flag.IntVar(&flagShutdownGraceTime, "t", defaultShutdownGraceTime, "shutdown grace time") 89 | err := readConfigFile(".forego", &flagProcfile, &flagPort, &flagConcurrency, &flagShutdownGraceTime) 90 | handleError(err) 91 | } 92 | 93 | func readConfigFile(config_path string, flagProcfile *string, flagPort *int, flagConcurrency *string, flagShutdownGraceTime *int) error { 94 | config, err := ReadConfig(config_path) 95 | 96 | if config["procfile"] != "" { 97 | *flagProcfile = config["procfile"] 98 | } else { 99 | *flagProcfile = "Procfile" 100 | } 101 | if config["port"] != "" { 102 | *flagPort, err = strconv.Atoi(config["port"]) 103 | } else { 104 | *flagPort = defaultPort 105 | } 106 | if config["shutdown_grace_time"] != "" { 107 | *flagShutdownGraceTime, err = strconv.Atoi(config["shutdown_grace_time"]) 108 | } else { 109 | *flagShutdownGraceTime = defaultShutdownGraceTime 110 | } 111 | *flagConcurrency = config["concurrency"] 112 | return err 113 | } 114 | 115 | func parseConcurrency(value string) (map[string]int, error) { 116 | concurrency := map[string]int{} 117 | if strings.TrimSpace(value) == "" { 118 | return concurrency, nil 119 | } 120 | 121 | parts := strings.Split(value, ",") 122 | for _, part := range parts { 123 | if !strings.Contains(part, "=") { 124 | return concurrency, errors.New("Concurrency should be in the format: foo=1,bar=2") 125 | } 126 | 127 | nameValue := strings.Split(part, "=") 128 | n, v := strings.TrimSpace(nameValue[0]), strings.TrimSpace(nameValue[1]) 129 | if n == "" || v == "" { 130 | return concurrency, errors.New("Concurrency should be in the format: foo=1,bar=2") 131 | } 132 | 133 | numProcs, err := strconv.ParseInt(v, 10, 16) 134 | if err != nil { 135 | return concurrency, err 136 | } 137 | 138 | concurrency[n] = int(numProcs) 139 | } 140 | return concurrency, nil 141 | } 142 | 143 | type Forego struct { 144 | outletFactory *OutletFactory 145 | 146 | teardown, teardownNow Barrier // signal shutting down 147 | 148 | wg sync.WaitGroup 149 | } 150 | 151 | func (f *Forego) monitorInterrupt() { 152 | handler := make(chan os.Signal, 1) 153 | signal.Notify(handler, syscall.SIGALRM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) 154 | 155 | first := true 156 | 157 | for sig := range handler { 158 | switch sig { 159 | case syscall.SIGINT: 160 | fmt.Println(" | ctrl-c detected") 161 | fallthrough 162 | default: 163 | f.teardown.Fall() 164 | if !first { 165 | f.teardownNow.Fall() 166 | } 167 | first = false 168 | } 169 | } 170 | } 171 | 172 | func basePort(env Env) (int, error) { 173 | if flagPort != defaultPort { 174 | return flagPort, nil 175 | } else if env["PORT"] != "" { 176 | return strconv.Atoi(env["PORT"]) 177 | } else if os.Getenv("PORT") != "" { 178 | return strconv.Atoi(os.Getenv("PORT")) 179 | } 180 | return defaultPort, nil 181 | } 182 | 183 | func (f *Forego) startProcess(idx, procNum int, proc ProcfileEntry, env Env, of *OutletFactory) { 184 | port, err := basePort(env) 185 | if err != nil { 186 | panic(err) 187 | } 188 | 189 | port = port + (idx * 100) 190 | 191 | const interactive = false 192 | workDir := filepath.Dir(flagProcfile) 193 | ps := NewProcess(workDir, proc.Command, env, interactive) 194 | procName := fmt.Sprint(proc.Name, ".", procNum+1) 195 | ps.Env["PORT"] = strconv.Itoa(port) 196 | 197 | ps.Stdin = nil 198 | 199 | stdout, err := ps.StdoutPipe() 200 | if err != nil { 201 | panic(err) 202 | } 203 | stderr, err := ps.StderrPipe() 204 | if err != nil { 205 | panic(err) 206 | } 207 | 208 | pipeWait := new(sync.WaitGroup) 209 | pipeWait.Add(2) 210 | go of.LineReader(pipeWait, procName, idx, stdout, false) 211 | go of.LineReader(pipeWait, procName, idx, stderr, true) 212 | 213 | of.SystemOutput(fmt.Sprintf("starting %s on port %d", procName, port)) 214 | 215 | finished := make(chan struct{}) // closed on process exit 216 | 217 | err = ps.Start() 218 | if err != nil { 219 | f.teardown.Fall() 220 | of.SystemOutput(fmt.Sprint("Failed to start ", procName, ": ", err)) 221 | return 222 | } 223 | 224 | f.wg.Add(1) 225 | go func() { 226 | defer f.wg.Done() 227 | defer close(finished) 228 | pipeWait.Wait() 229 | ps.Wait() 230 | }() 231 | 232 | f.wg.Add(1) 233 | go func() { 234 | defer f.wg.Done() 235 | 236 | select { 237 | case <-finished: 238 | if flagRestart { 239 | f.startProcess(idx, procNum, proc, env, of) 240 | } else { 241 | f.teardown.Fall() 242 | } 243 | 244 | case <-f.teardown.Barrier(): 245 | // Forego tearing down 246 | 247 | if !osHaveSigTerm { 248 | of.SystemOutput(fmt.Sprintf("Killing %s", procName)) 249 | ps.Process.Kill() 250 | return 251 | } 252 | 253 | of.SystemOutput(fmt.Sprintf("sending SIGTERM to %s", procName)) 254 | ps.SendSigTerm() 255 | 256 | // Give the process a chance to exit, otherwise kill it. 257 | select { 258 | case <-f.teardownNow.Barrier(): 259 | of.SystemOutput(fmt.Sprintf("Killing %s", procName)) 260 | ps.SendSigKill() 261 | case <-finished: 262 | } 263 | } 264 | }() 265 | } 266 | 267 | func runStart(cmd *Command, args []string) { 268 | pf, err := ReadProcfile(flagProcfile) 269 | handleError(err) 270 | 271 | concurrency, err := parseConcurrency(flagConcurrency) 272 | handleError(err) 273 | 274 | env, err := loadEnvs(envs) 275 | handleError(err) 276 | 277 | of := NewOutletFactory() 278 | of.Padding = pf.LongestProcessName(concurrency) 279 | 280 | f := &Forego{ 281 | outletFactory: of, 282 | } 283 | 284 | go f.monitorInterrupt() 285 | 286 | // When teardown fires, start the grace timer 287 | f.teardown.FallHook = func() { 288 | go func() { 289 | time.Sleep(time.Duration(flagShutdownGraceTime) * time.Second) 290 | of.SystemOutput("Grace time expired") 291 | f.teardownNow.Fall() 292 | }() 293 | } 294 | 295 | var singleton string = "" 296 | if len(args) > 0 { 297 | singleton = args[0] 298 | if !pf.HasProcess(singleton) { 299 | of.ErrorOutput(fmt.Sprintf("no such process: %s", singleton)) 300 | } 301 | } 302 | 303 | defaultConcurrency := 1 304 | 305 | var all bool 306 | for name, num := range concurrency { 307 | if name == "all" { 308 | defaultConcurrency = num 309 | all = true 310 | } 311 | } 312 | 313 | for idx, proc := range pf.Entries { 314 | numProcs := defaultConcurrency 315 | if len(concurrency) > 0 { 316 | if value, ok := concurrency[proc.Name]; ok { 317 | numProcs = value 318 | } else if !all { 319 | continue 320 | } 321 | } 322 | for i := 0; i < numProcs; i++ { 323 | if (singleton == "") || (singleton == proc.Name) { 324 | f.startProcess(idx, i, proc, env, of) 325 | } 326 | } 327 | } 328 | 329 | <-f.teardown.Barrier() 330 | 331 | f.wg.Wait() 332 | } 333 | --------------------------------------------------------------------------------