diff --git a/.gitignore b/.gitignore index e5c32b0..ad1b798 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ coverage.out .vscode -bin/** +bin/process* *.log .env # local build output via go build diff --git a/README.md b/README.md index ae56c63..6bf2ae5 100755 --- a/README.md +++ b/README.md @@ -149,12 +149,33 @@ process2: ```yaml process2: depends_on: - process2: - condition: process_completed_successfully # or "process_started" (default) + process3: + condition: process_completed_successfully # or "process_started" (default) process3: condition: process_completed_successfully ``` +##### ✅ Termination Parameters + +```yaml +process1: + command: "pg_ctl start" + shutdown: + command: "pg_ctl stop" + timeout_seconds: 10 # default 10 + signal: 15 # default 15, but only if command is not defined or empty +``` + +`shutdown` is optional and can be omitted. The default behaviour in this case: `SIGTERM` is issued to the running process. + +In case only `shutdown.signal` is defined `[1..31] ` the running process will be terminated with its value. + +In case the the `shutdown.command` is defined: + +1. The `shutdown.command` is executed with all the Environment Variables of the main process +2. Wait `shutdown.timeout_seconds` for its completion (if not defined wait for 10 seconds) +3. In case of timeout the process will receive the `SIGKILL` signal + #### ✅ Output Handling ##### ✅ Show process name diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/process-compose.yaml b/process-compose.yaml index c3f5f49..81c103c 100644 --- a/process-compose.yaml +++ b/process-compose.yaml @@ -11,7 +11,7 @@ processes: restart: "on-failure" backoff_seconds: 2 depends_on: - process2: + _process2: condition: process_completed_successfully process3: condition: process_completed @@ -20,7 +20,7 @@ processes: environment: - 'EXIT_CODE=0' - process2: + _process2: command: "./test_loop.bash process2" log_location: ./pc.proc2.log availability: @@ -31,7 +31,11 @@ processes: environment: - 'ABC=2221' - 'PRINT_ERR=111' - - 'EXIT_CODE=0' + - 'EXIT_CODE=2' + shutdown: + command: "pkill -f process2" + signal: 15 + timeout_seconds: 2 process3: command: "./test_loop.bash process3" @@ -48,7 +52,7 @@ processes: # restart: on-failure environment: - 'ABC=2221' - - 'EXIT_CODE=1' + - 'EXIT_CODE=4' kcalc: command: "kcalc" diff --git a/src/app/config.go b/src/app/config.go index dcff264..c30806f 100644 --- a/src/app/config.go +++ b/src/app/config.go @@ -24,14 +24,15 @@ type Project struct { type Processes map[string]ProcessConfig type ProcessConfig struct { - Name string - Disabled bool `yaml:"disabled,omitempty"` - Command string `yaml:"command"` - LogLocation string `yaml:"log_location,omitempty"` - Environment []string `yaml:"environment,omitempty"` - RestartPolicy RestartPolicyConfig `yaml:"availability,omitempty"` - DependsOn DependsOnConfig `yaml:"depends_on,omitempty"` - Extensions map[string]interface{} `yaml:",inline"` + Name string + Disabled bool `yaml:"disabled,omitempty"` + Command string `yaml:"command"` + LogLocation string `yaml:"log_location,omitempty"` + Environment []string `yaml:"environment,omitempty"` + RestartPolicy RestartPolicyConfig `yaml:"availability,omitempty"` + DependsOn DependsOnConfig `yaml:"depends_on,omitempty"` + ShutDownParams ShutDownParams `yaml:"shutdown,omitempty"` + Extensions map[string]interface{} `yaml:",inline"` } type ProcessState struct { @@ -74,6 +75,12 @@ type RestartPolicyConfig struct { MaxRestarts int `yaml:"max_restarts,omitempty"` } +type ShutDownParams struct { + ShutDownCommand string `yaml:"command,omitempty"` + ShutDownTimeout int `yaml:"timeout_seconds,omitempty"` + Signal int `yaml:"signal,omitempty"` +} + const ( // ProcessConditionCompleted is the type for waiting until a process has completed (any exit code). ProcessConditionCompleted = "process_completed" diff --git a/src/app/process.go b/src/app/process.go index 2ced201..d793a1f 100644 --- a/src/app/process.go +++ b/src/app/process.go @@ -2,6 +2,7 @@ package app import ( "bufio" + "context" "fmt" "io" "math/rand" @@ -10,6 +11,7 @@ import ( "runtime" "strconv" "sync" + "syscall" "time" "github.com/f1bonacc1/process-compose/src/pclog" @@ -18,6 +20,10 @@ import ( "github.com/rs/zerolog/log" ) +const ( + DEFAULT_SHUTDOWN_TIMEOUT_SEC = 10 +) + type Process struct { globalEnv []string procConf ProcessConfig @@ -156,6 +162,33 @@ func (p *Process) WontRun() { } +func (p *Process) shutDown() error { + if isStringDefined(p.procConf.ShutDownParams.ShutDownCommand) { + return p.doConfiguredStop(p.procConf.ShutDownParams) + } + return p.stop(p.procConf.ShutDownParams.Signal) +} + +func (p *Process) doConfiguredStop(params ShutDownParams) error { + timeout := params.ShutDownTimeout + if timeout == 0 { + timeout = DEFAULT_SHUTDOWN_TIMEOUT_SEC + } + log.Debug().Msgf("killing %s with timeout %d ...", p.GetName(), timeout) + ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second) + defer cancel() + + cmd := exec.CommandContext(ctx, getRunnerShell(), getRunnerArg(), params.ShutDownCommand) + cmd.Env = p.getProcessEnvironment() + + if err := cmd.Run(); err != nil { + // the process termination timedout and it will be killed + log.Error().Msgf("killing %s with timeout %d failed", p.GetName(), timeout) + return p.stop(int(syscall.SIGKILL)) + } + return nil +} + func (p *Process) onProcessEnd() { if isStringDefined(p.procConf.LogLocation) { p.logger.Close() diff --git a/src/app/process_unix.go b/src/app/process_unix.go index b1386b2..71fe9fe 100644 --- a/src/app/process_unix.go +++ b/src/app/process_unix.go @@ -2,12 +2,22 @@ package app -import "syscall" +import ( + "syscall" +) -func (p *Process) stop() error { +const ( + min_sig = 1 + max_sig = 31 +) + +func (p *Process) stop(sig int) error { + if sig < min_sig || sig > max_sig { + sig = int(syscall.SIGTERM) + } pgid, err := syscall.Getpgid(p.cmd.Process.Pid) if err == nil { - return syscall.Kill(-pgid, syscall.SIGKILL) + return syscall.Kill(-pgid, syscall.Signal(sig)) } return err } diff --git a/src/app/process_windows.go b/src/app/process_windows.go index 2db6845..977c869 100644 --- a/src/app/process_windows.go +++ b/src/app/process_windows.go @@ -5,7 +5,7 @@ import ( "strconv" ) -func (p *Process) stop() error { +func (p *Process) stop(sig int) error { //p.cmd.Process.Kill() kill := exec.Command("TASKKILL", "/T", "/F", "/PID", strconv.Itoa(p.cmd.Process.Pid)) return kill.Run() diff --git a/src/app/project.go b/src/app/project.go index 44a6220..8a9a73b 100644 --- a/src/app/project.go +++ b/src/app/project.go @@ -182,7 +182,7 @@ func (p *Project) StopProcess(name string) error { log.Error().Msgf("Process %s is not running", name) return fmt.Errorf("process %s is not running", name) } - proc.stop() + proc.shutDown() return nil } diff --git a/test_loop.bash b/test_loop.bash index 7693fa2..b163c6b 100755 --- a/test_loop.bash +++ b/test_loop.bash @@ -1,9 +1,14 @@ #!/usr/bin/env bash + +#trap "echo ERROR: The program is terminated ; exit" SIGTERM +trap 'echo CODE: $?; exit $EXIT_CODE' 1 2 3 15 + LOOPS=30000 for (( i=1; i<=LOOPS; i++ )) do - sleep 0.01 + #sleep 0.01 + sleep 0.5 if [[ -z "${PRINT_ERR}" ]]; then echo "test loop $i loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop $1 $ABC"