├── .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 |
--------------------------------------------------------------------------------