mirror of
https://github.com/F1bonacc1/process-compose.git
synced 2024-09-11 11:35:26 +03:00
Merge pull request #6 from F1bonacc1/feature/support_daemon_launching
Feature/support daemon launching #3
This commit is contained in:
commit
13de4ab1f7
38
README.md
38
README.md
@ -158,23 +158,43 @@ process2:
|
||||
##### ✅ Termination Parameters
|
||||
|
||||
```yaml
|
||||
process1:
|
||||
command: "pg_ctl start"
|
||||
nginx:
|
||||
command: "docker run --rm --name nginx_test nginx"
|
||||
shutdown:
|
||||
command: "pg_ctl stop"
|
||||
command: "docker stop nginx_test"
|
||||
timeout_seconds: 10 # default 10
|
||||
signal: 15 # default 15, but only if the 'command' is not defined or empty
|
||||
```
|
||||
|
||||
`shutdown` is optional and can be omitted. The default behavior 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 `shutdown.command` is defined:
|
||||
|
||||
1. The `shutdown.command` is executed with all the Environment Variables of the primary process
|
||||
2. Wait for `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
|
||||
|
||||
##### ✅ Background (detached) Processes
|
||||
|
||||
```yaml
|
||||
nginx:
|
||||
command: "docker run -d --rm --name nginx_test nginx" # note the '-d' for detached mode
|
||||
is_daemon: true # this flag is required for background processes (default false)
|
||||
shutdown:
|
||||
command: "docker stop nginx_test"
|
||||
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.
|
||||
1. For processes that start services / daemons in the background, please use the `is_daemon` flag set to `true`.
|
||||
|
||||
In case only `shutdown.signal` is defined `[1..31] ` the running process will be terminated with its value.
|
||||
2. In case a process is daemon it will be considered running until stopped.
|
||||
|
||||
In case the the `shutdown.command` is defined:
|
||||
3. Daemon processes can only be stopped with the `$PROCESSNAME.shutdown.command` as in the example above.
|
||||
|
||||
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
|
||||
|
||||
|
||||
#### ✅ <u>Output Handling</u>
|
||||
|
||||
|
@ -58,6 +58,16 @@ processes:
|
||||
- 'ABC=2221'
|
||||
- 'EXIT_CODE=4'
|
||||
|
||||
docker:
|
||||
command: "docker run -d --rm --name nginx_test nginx"
|
||||
# availability:
|
||||
# restart: on-failure
|
||||
is_daemon: true
|
||||
shutdown:
|
||||
command: "docker stop nginx_test"
|
||||
signal: 15
|
||||
timeout_seconds: 5
|
||||
|
||||
kcalc:
|
||||
command: "kcalc"
|
||||
disabled: true
|
||||
|
@ -26,6 +26,7 @@ type Processes map[string]ProcessConfig
|
||||
type ProcessConfig struct {
|
||||
Name string
|
||||
Disabled bool `yaml:"disabled,omitempty"`
|
||||
IsDaemon bool `yaml:"is_daemon,omitempty"`
|
||||
Command string `yaml:"command"`
|
||||
LogLocation string `yaml:"log_location,omitempty"`
|
||||
Environment []string `yaml:"environment,omitempty"`
|
||||
@ -65,6 +66,8 @@ const (
|
||||
ProcessStateDisabled = "Disabled"
|
||||
ProcessStatePending = "Pending"
|
||||
ProcessStateRunning = "Running"
|
||||
ProcessStateLaunching = "Launching"
|
||||
ProcessStateLaunched = "Launched"
|
||||
ProcessStateRestarting = "Restarting"
|
||||
ProcessStateTerminating = "Terminating"
|
||||
ProcessStateCompleted = "Completed"
|
||||
|
27
src/app/daemon.go
Normal file
27
src/app/daemon.go
Normal file
@ -0,0 +1,27 @@
|
||||
package app
|
||||
|
||||
func (p *Process) waitForDaemonCompletion() {
|
||||
if !p.isDaemonLaunched() {
|
||||
return
|
||||
}
|
||||
|
||||
loop:
|
||||
for {
|
||||
status := <-p.procStateChan
|
||||
switch status {
|
||||
case ProcessStateCompleted:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (p *Process) notifyDaemonStopped() {
|
||||
if p.isDaemonLaunched() {
|
||||
p.procStateChan <- ProcessStateCompleted
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Process) isDaemonLaunched() bool {
|
||||
return p.procConf.IsDaemon && p.procState.ExitCode == 0
|
||||
}
|
@ -21,25 +21,28 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
DEFAULT_SHUTDOWN_TIMEOUT_SEC = 10
|
||||
UNDEFINED_SHUTDOWN_TIMEOUT_SEC = 0
|
||||
DEFAULT_SHUTDOWN_TIMEOUT_SEC = 10
|
||||
)
|
||||
|
||||
type Process struct {
|
||||
globalEnv []string
|
||||
procConf ProcessConfig
|
||||
procState *ProcessState
|
||||
sync.Mutex
|
||||
stateMtx sync.Mutex
|
||||
procCond sync.Cond
|
||||
procColor func(a ...interface{}) string
|
||||
noColor func(a ...interface{}) string
|
||||
redColor func(a ...interface{}) string
|
||||
logBuffer *pclog.ProcessLogBuffer
|
||||
logger pclog.PcLogger
|
||||
cmd *exec.Cmd
|
||||
done bool
|
||||
replica int
|
||||
startTime time.Time
|
||||
|
||||
globalEnv []string
|
||||
procConf ProcessConfig
|
||||
procState *ProcessState
|
||||
stateMtx sync.Mutex
|
||||
procCond sync.Cond
|
||||
procStateChan chan string
|
||||
procColor func(a ...interface{}) string
|
||||
noColor func(a ...interface{}) string
|
||||
redColor func(a ...interface{}) string
|
||||
logBuffer *pclog.ProcessLogBuffer
|
||||
logger pclog.PcLogger
|
||||
cmd *exec.Cmd
|
||||
done bool
|
||||
replica int
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func NewProcess(
|
||||
@ -50,20 +53,21 @@ func NewProcess(
|
||||
procLog *pclog.ProcessLogBuffer,
|
||||
replica int) *Process {
|
||||
colNumeric := rand.Intn(int(color.FgHiWhite)-int(color.FgHiBlack)) + int(color.FgHiBlack)
|
||||
//logger, _ := zap.NewProduction()
|
||||
|
||||
proc := &Process{
|
||||
globalEnv: globalEnv,
|
||||
procConf: procConf,
|
||||
procColor: color.New(color.Attribute(colNumeric), color.Bold).SprintFunc(),
|
||||
redColor: color.New(color.FgHiRed).SprintFunc(),
|
||||
noColor: color.New(color.Reset).SprintFunc(),
|
||||
logger: logger,
|
||||
procState: procState,
|
||||
done: false,
|
||||
replica: replica,
|
||||
logBuffer: procLog,
|
||||
globalEnv: globalEnv,
|
||||
procConf: procConf,
|
||||
procColor: color.New(color.Attribute(colNumeric), color.Bold).SprintFunc(),
|
||||
redColor: color.New(color.FgHiRed).SprintFunc(),
|
||||
noColor: color.New(color.Reset).SprintFunc(),
|
||||
logger: logger,
|
||||
procState: procState,
|
||||
done: false,
|
||||
replica: replica,
|
||||
logBuffer: procLog,
|
||||
procStateChan: make(chan string, 1),
|
||||
}
|
||||
|
||||
proc.procCond = *sync.NewCond(proc)
|
||||
return proc
|
||||
}
|
||||
@ -83,7 +87,7 @@ func (p *Process) run() error {
|
||||
go p.handleOutput(stderr, p.handleError)
|
||||
return p.cmd.Start()
|
||||
}
|
||||
p.setStateAndRun(ProcessStateRunning, starter)
|
||||
p.setStateAndRun(p.getStartingStateName(), starter)
|
||||
|
||||
p.startTime = time.Now()
|
||||
p.procState.Pid = p.cmd.Process.Pid
|
||||
@ -96,7 +100,12 @@ func (p *Process) run() error {
|
||||
p.Lock()
|
||||
p.procState.ExitCode = p.cmd.ProcessState.ExitCode()
|
||||
p.Unlock()
|
||||
log.Info().Msgf("%s exited with status %d", p.procConf.Name, p.procState.ExitCode)
|
||||
log.Info().Msgf("%s exited with status %d", p.getNameWithReplica(), p.procState.ExitCode)
|
||||
|
||||
if p.isDaemonLaunched() {
|
||||
p.setState(ProcessStateLaunched)
|
||||
p.waitForDaemonCompletion()
|
||||
}
|
||||
|
||||
if !p.isRestartable(p.procState.ExitCode) {
|
||||
break
|
||||
@ -144,6 +153,7 @@ func (p *Process) isRestartable(exitCode int) bool {
|
||||
return p.procState.Restarts < p.procConf.RestartPolicy.MaxRestarts
|
||||
}
|
||||
|
||||
// TODO consider if forking daemon should disable RestartPolicyAlways
|
||||
if p.procConf.RestartPolicy.Restart == RestartPolicyAlways {
|
||||
if p.procConf.RestartPolicy.MaxRestarts == 0 {
|
||||
return true
|
||||
@ -171,7 +181,7 @@ func (p *Process) wontRun() {
|
||||
|
||||
// perform gracefull process shutdown if defined in configuration
|
||||
func (p *Process) shutDown() error {
|
||||
if !p.isState(ProcessStateRunning) {
|
||||
if !p.isRunning() {
|
||||
log.Debug().Msgf("process %s is in state %s not shutting down", p.getName(), p.procState.Status)
|
||||
// prevent pending process from running
|
||||
p.setState(ProcessStateTerminating)
|
||||
@ -186,12 +196,13 @@ func (p *Process) shutDown() error {
|
||||
|
||||
func (p *Process) doConfiguredStop(params ShutDownParams) error {
|
||||
timeout := params.ShutDownTimeout
|
||||
if timeout == 0 {
|
||||
if timeout == UNDEFINED_SHUTDOWN_TIMEOUT_SEC {
|
||||
timeout = DEFAULT_SHUTDOWN_TIMEOUT_SEC
|
||||
}
|
||||
log.Debug().Msgf("terminating %s with timeout %d ...", p.getName(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
||||
defer cancel()
|
||||
defer p.notifyDaemonStopped()
|
||||
|
||||
cmd := exec.CommandContext(ctx, getRunnerShell(), getRunnerArg(), params.ShutDownCommand)
|
||||
cmd.Env = p.getProcessEnvironment()
|
||||
@ -204,6 +215,10 @@ func (p *Process) doConfiguredStop(params ShutDownParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Process) isRunning() bool {
|
||||
return p.isOneOfStates(ProcessStateRunning, ProcessStateLaunched)
|
||||
}
|
||||
|
||||
func (p *Process) prepareForShutDown() {
|
||||
// prevent restart during global shutdown
|
||||
p.procConf.RestartPolicy.Restart = RestartPolicyNo
|
||||
@ -234,7 +249,7 @@ func (p *Process) getCommand() string {
|
||||
}
|
||||
|
||||
func (p *Process) updateProcState() {
|
||||
if p.isState(ProcessStateRunning) {
|
||||
if p.isRunning() {
|
||||
dur := time.Since(p.startTime)
|
||||
p.procState.SystemTime = durationToString(dur)
|
||||
}
|
||||
@ -251,13 +266,13 @@ func (p *Process) handleOutput(pipe io.ReadCloser,
|
||||
|
||||
func (p *Process) handleInfo(message string) {
|
||||
p.logger.Info(message, p.getName(), p.replica)
|
||||
fmt.Printf("[%s]\t%s\n", p.procColor(p.getNameWithReplica()), message)
|
||||
fmt.Printf("[%s\t] %s\n", p.procColor(p.getNameWithReplica()), message)
|
||||
p.logBuffer.Write(message)
|
||||
}
|
||||
|
||||
func (p *Process) handleError(message string) {
|
||||
p.logger.Error(message, p.getName(), p.replica)
|
||||
fmt.Printf("[%s]\t%s\n", p.procColor(p.getNameWithReplica()), p.redColor(message))
|
||||
fmt.Printf("[%s\t] %s\n", p.procColor(p.getNameWithReplica()), p.redColor(message))
|
||||
p.logBuffer.Write(message)
|
||||
}
|
||||
|
||||
@ -267,6 +282,17 @@ func (p *Process) isState(state string) bool {
|
||||
return p.procState.Status == state
|
||||
}
|
||||
|
||||
func (p *Process) isOneOfStates(states ...string) bool {
|
||||
p.stateMtx.Lock()
|
||||
defer p.stateMtx.Unlock()
|
||||
for _, state := range states {
|
||||
if p.procState.Status == state {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Process) setState(state string) {
|
||||
p.stateMtx.Lock()
|
||||
defer p.stateMtx.Unlock()
|
||||
|
8
src/app/process_states.go
Normal file
8
src/app/process_states.go
Normal file
@ -0,0 +1,8 @@
|
||||
package app
|
||||
|
||||
func (p *Process) getStartingStateName() string {
|
||||
if p.procConf.IsDaemon {
|
||||
return ProcessStateLaunching
|
||||
}
|
||||
return ProcessStateRunning
|
||||
}
|
Loading…
Reference in New Issue
Block a user