Separate subprocess execution lifecycle from policy
This commit is contained in:
90
internal/adapters/subprocess/execution.go
Normal file
90
internal/adapters/subprocess/execution.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package subprocess
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
type executionReason uint8
|
||||
|
||||
const (
|
||||
executionNotStarted executionReason = iota
|
||||
executionNaturalExit
|
||||
executionCanceled
|
||||
executionDeadline
|
||||
executionAborted
|
||||
)
|
||||
|
||||
type executionOutcome struct {
|
||||
reason executionReason
|
||||
startedAt time.Time
|
||||
completedAt time.Time
|
||||
exitCode int
|
||||
startErr error
|
||||
waitErr error
|
||||
contextErr error
|
||||
abortErr error
|
||||
cleanupErr error
|
||||
}
|
||||
|
||||
func executeOwnedCommand(ctx context.Context, cmd *exec.Cmd, abort <-chan error) (outcome executionOutcome) {
|
||||
outcome = executionOutcome{startedAt: time.Now().UTC(), exitCode: -1}
|
||||
defer func() { outcome.completedAt = time.Now().UTC() }()
|
||||
if outcome.contextErr = ctx.Err(); outcome.contextErr != nil {
|
||||
outcome.reason = executionNotStarted
|
||||
return outcome
|
||||
}
|
||||
tree, err := newOwnedProcessTree()
|
||||
if err != nil {
|
||||
outcome.reason, outcome.startErr = executionNotStarted, err
|
||||
return outcome
|
||||
}
|
||||
defer func() { outcome.cleanupErr = joinErrors(outcome.cleanupErr, tree.Dispose()) }()
|
||||
cmd.WaitDelay = forcefulTerminationWait
|
||||
if outcome.startErr = tree.Start(cmd); outcome.startErr != nil {
|
||||
outcome.reason = executionNotStarted
|
||||
return outcome
|
||||
}
|
||||
waitCh := make(chan error, 1)
|
||||
go func() { waitCh <- cmd.Wait() }()
|
||||
select {
|
||||
case outcome.waitErr = <-waitCh:
|
||||
outcome.reason = executionNaturalExit
|
||||
case <-ctx.Done():
|
||||
outcome.contextErr = ctx.Err()
|
||||
if outcome.contextErr == context.DeadlineExceeded {
|
||||
outcome.reason = executionDeadline
|
||||
} else {
|
||||
outcome.reason = executionCanceled
|
||||
}
|
||||
outcome.cleanupErr = terminateAndReap(tree, waitCh)
|
||||
case outcome.abortErr = <-abort:
|
||||
outcome.reason = executionAborted
|
||||
outcome.cleanupErr = terminateAndReap(tree, waitCh)
|
||||
}
|
||||
if cmd.ProcessState != nil {
|
||||
outcome.exitCode = cmd.ProcessState.ExitCode()
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
func terminateAndReap(tree ownedProcessTree, waitCh <-chan error) error {
|
||||
cleanupErr := tree.TerminateGracefully()
|
||||
timer := time.NewTimer(gracefulTerminationWait)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-waitCh:
|
||||
return joinErrors(cleanupErr, tree.TerminateForcefully())
|
||||
case <-timer.C:
|
||||
}
|
||||
cleanupErr = joinErrors(cleanupErr, tree.TerminateForcefully())
|
||||
forcefulTimer := time.NewTimer(forcefulTerminationWait)
|
||||
defer forcefulTimer.Stop()
|
||||
select {
|
||||
case <-waitCh:
|
||||
return cleanupErr
|
||||
case <-forcefulTimer.C:
|
||||
return joinErrors(cleanupErr, context.DeadlineExceeded)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user