mirror of
https://github.com/wahyd4/forego.git
synced 2026-08-09 05:07:05 +10:00
292 lines
6.3 KiB
Go
292 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
const defaultPort = 5000
|
|
const defaultShutdownGraceTime = 3
|
|
|
|
var flagPort int
|
|
var flagConcurrency string
|
|
var flagRestart bool
|
|
var flagShutdownGraceTime int
|
|
var envs envFiles
|
|
|
|
var cmdStart = &Command{
|
|
Run: runStart,
|
|
Usage: "start [process name] [-f procfile] [-e env] [-c concurrency] [-p port] [-t timeout] [-r]",
|
|
Short: "Start the application",
|
|
Long: `
|
|
Start the application specified by a Procfile (defaults to ./Procfile)
|
|
|
|
Examples:
|
|
|
|
forego start
|
|
forego start web
|
|
forego start -f Procfile.test -e .env.test
|
|
forego start -t 30
|
|
`,
|
|
}
|
|
|
|
func init() {
|
|
cmdStart.Flag.StringVar(&flagProcfile, "f", "Procfile", "procfile")
|
|
cmdStart.Flag.Var(&envs, "e", "env")
|
|
cmdStart.Flag.IntVar(&flagPort, "p", defaultPort, "port")
|
|
cmdStart.Flag.StringVar(&flagConcurrency, "c", "", "concurrency")
|
|
cmdStart.Flag.BoolVar(&flagRestart, "r", false, "restart")
|
|
cmdStart.Flag.IntVar(&flagShutdownGraceTime, "t", defaultShutdownGraceTime, "shutdown grace time")
|
|
err := readConfigFile(".forego", &flagProcfile, &flagPort, &flagConcurrency, &flagShutdownGraceTime)
|
|
handleError(err)
|
|
}
|
|
|
|
func readConfigFile(config_path string, flagProcfile *string, flagPort *int, flagConcurrency *string, flagShutdownGraceTime *int) error {
|
|
config, err := ReadConfig(config_path)
|
|
|
|
if config["procfile"] != "" {
|
|
*flagProcfile = config["procfile"]
|
|
} else {
|
|
*flagProcfile = "Procfile"
|
|
}
|
|
if config["port"] != "" {
|
|
*flagPort, err = strconv.Atoi(config["port"])
|
|
} else {
|
|
*flagPort = defaultPort
|
|
}
|
|
if config["shutdown_grace_time"] != "" {
|
|
*flagShutdownGraceTime, err = strconv.Atoi(config["shutdown_grace_time"])
|
|
} else {
|
|
*flagShutdownGraceTime = defaultShutdownGraceTime
|
|
}
|
|
*flagConcurrency = config["concurrency"]
|
|
return err
|
|
}
|
|
|
|
func parseConcurrency(value string) (map[string]int, error) {
|
|
concurrency := map[string]int{}
|
|
if strings.TrimSpace(value) == "" {
|
|
return concurrency, nil
|
|
}
|
|
|
|
parts := strings.Split(value, ",")
|
|
for _, part := range parts {
|
|
if !strings.Contains(part, "=") {
|
|
return concurrency, errors.New("Concurrency should be in the format: foo=1,bar=2")
|
|
}
|
|
|
|
nameValue := strings.Split(part, "=")
|
|
n, v := strings.TrimSpace(nameValue[0]), strings.TrimSpace(nameValue[1])
|
|
if n == "" || v == "" {
|
|
return concurrency, errors.New("Concurrency should be in the format: foo=1,bar=2")
|
|
}
|
|
|
|
numProcs, err := strconv.ParseInt(v, 10, 16)
|
|
if err != nil {
|
|
return concurrency, err
|
|
}
|
|
|
|
concurrency[n] = int(numProcs)
|
|
}
|
|
return concurrency, nil
|
|
}
|
|
|
|
type Forego struct {
|
|
outletFactory *OutletFactory
|
|
|
|
teardown, teardownNow Barrier // signal shutting down
|
|
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
func (f *Forego) monitorInterrupt() {
|
|
handler := make(chan os.Signal, 1)
|
|
signal.Notify(handler, syscall.SIGALRM, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
first := true
|
|
|
|
for sig := range handler {
|
|
switch sig {
|
|
case syscall.SIGINT:
|
|
fmt.Println(" | ctrl-c detected")
|
|
fallthrough
|
|
default:
|
|
f.teardown.Fall()
|
|
if !first {
|
|
f.teardownNow.Fall()
|
|
}
|
|
first = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func basePort(env Env) (int, error) {
|
|
if flagPort != defaultPort {
|
|
return flagPort, nil
|
|
} else if env["PORT"] != "" {
|
|
return strconv.Atoi(env["PORT"])
|
|
} else if os.Getenv("PORT") != "" {
|
|
return strconv.Atoi(os.Getenv("PORT"))
|
|
}
|
|
return defaultPort, nil
|
|
}
|
|
|
|
func (f *Forego) startProcess(idx, procNum int, proc ProcfileEntry, env Env, of *OutletFactory) {
|
|
port, err := basePort(env)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
port = port + (idx * 100)
|
|
|
|
const interactive = false
|
|
workDir := filepath.Dir(flagProcfile)
|
|
ps := NewProcess(workDir, proc.Command, env, interactive)
|
|
procName := fmt.Sprint(proc.Name, ".", procNum+1)
|
|
ps.Env["PORT"] = strconv.Itoa(port)
|
|
|
|
ps.Stdin = nil
|
|
|
|
stdout, err := ps.StdoutPipe()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
stderr, err := ps.StderrPipe()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
pipeWait := new(sync.WaitGroup)
|
|
pipeWait.Add(2)
|
|
go of.LineReader(pipeWait, procName, idx, stdout, false)
|
|
go of.LineReader(pipeWait, procName, idx, stderr, true)
|
|
|
|
of.SystemOutput(fmt.Sprintf("starting %s on port %d", procName, port))
|
|
|
|
finished := make(chan struct{}) // closed on process exit
|
|
|
|
err = ps.Start()
|
|
if err != nil {
|
|
f.teardown.Fall()
|
|
of.SystemOutput(fmt.Sprint("Failed to start ", procName, ": ", err))
|
|
return
|
|
}
|
|
|
|
f.wg.Add(1)
|
|
go func() {
|
|
defer f.wg.Done()
|
|
defer close(finished)
|
|
pipeWait.Wait()
|
|
ps.Wait()
|
|
}()
|
|
|
|
f.wg.Add(1)
|
|
go func() {
|
|
defer f.wg.Done()
|
|
|
|
select {
|
|
case <-finished:
|
|
if flagRestart {
|
|
f.startProcess(idx, procNum, proc, env, of)
|
|
} else {
|
|
f.teardown.Fall()
|
|
}
|
|
|
|
case <-f.teardown.Barrier():
|
|
// Forego tearing down
|
|
|
|
if !osHaveSigTerm {
|
|
of.SystemOutput(fmt.Sprintf("Killing %s", procName))
|
|
ps.Process.Kill()
|
|
return
|
|
}
|
|
|
|
of.SystemOutput(fmt.Sprintf("sending SIGTERM to %s", procName))
|
|
ps.SendSigTerm()
|
|
|
|
// Give the process a chance to exit, otherwise kill it.
|
|
select {
|
|
case <-f.teardownNow.Barrier():
|
|
of.SystemOutput(fmt.Sprintf("Killing %s", procName))
|
|
ps.SendSigKill()
|
|
case <-finished:
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func runStart(cmd *Command, args []string) {
|
|
pf, err := ReadProcfile(flagProcfile)
|
|
handleError(err)
|
|
|
|
concurrency, err := parseConcurrency(flagConcurrency)
|
|
handleError(err)
|
|
|
|
env, err := loadEnvs(envs)
|
|
handleError(err)
|
|
|
|
of := NewOutletFactory()
|
|
of.Padding = pf.LongestProcessName(concurrency)
|
|
|
|
f := &Forego{
|
|
outletFactory: of,
|
|
}
|
|
|
|
go f.monitorInterrupt()
|
|
|
|
// When teardown fires, start the grace timer
|
|
f.teardown.FallHook = func() {
|
|
go func() {
|
|
time.Sleep(time.Duration(flagShutdownGraceTime) * time.Second)
|
|
of.SystemOutput("Grace time expired")
|
|
f.teardownNow.Fall()
|
|
}()
|
|
}
|
|
|
|
var singleton string = ""
|
|
if len(args) > 0 {
|
|
singleton = args[0]
|
|
if !pf.HasProcess(singleton) {
|
|
of.ErrorOutput(fmt.Sprintf("no such process: %s", singleton))
|
|
}
|
|
}
|
|
|
|
defaultConcurrency := 1
|
|
|
|
var all bool
|
|
for name, num := range concurrency {
|
|
if name == "all" {
|
|
defaultConcurrency = num
|
|
all = true
|
|
}
|
|
}
|
|
|
|
for idx, proc := range pf.Entries {
|
|
numProcs := defaultConcurrency
|
|
if len(concurrency) > 0 {
|
|
if value, ok := concurrency[proc.Name]; ok {
|
|
numProcs = value
|
|
} else if !all {
|
|
continue
|
|
}
|
|
}
|
|
for i := 0; i < numProcs; i++ {
|
|
if (singleton == "") || (singleton == proc.Name) {
|
|
f.startProcess(idx, i, proc, env, of)
|
|
}
|
|
}
|
|
}
|
|
|
|
<-f.teardown.Barrier()
|
|
|
|
f.wg.Wait()
|
|
}
|