package executor import ( "context" "os" "sync" "time" ) // timeoutContext is a custom context that kills the process group when cancelled due to timeout. type timeoutContext struct { context.Context cancel context.CancelFunc logEntry *InternalLogEntry process *os.Process executor *Executor processMu sync.Mutex } // newTimeoutContext creates a context that will kill the process group when the timeout expires. // logEntry is the same InternalLogEntry as the running execution so Kill(logEntry) can use Attributes and Process. // Pass nil logEntry for subprocesses (e.g. shellAfterCompleted); then setProcess stores the process and it is killed on timeout. func newTimeoutContext(parent context.Context, timeout time.Duration, executor *Executor, logEntry *InternalLogEntry) (*timeoutContext, context.CancelFunc) { ctx, cancel := context.WithTimeout(parent, timeout) tc := &timeoutContext{ Context: ctx, cancel: cancel, logEntry: logEntry, executor: executor, } go func() { <-ctx.Done() if ctx.Err() != context.DeadlineExceeded { return } tc.processMu.Lock() entry := tc.logEntry process := tc.process tc.processMu.Unlock() if entry != nil { _ = executor.Kill(entry) } else if process != nil { _ = executor.Kill(&InternalLogEntry{Process: process}) } }() return tc, cancel } func (tc *timeoutContext) setProcess(process *os.Process) { tc.processMu.Lock() if tc.logEntry != nil { tc.logEntry.Process = process } else { tc.process = process } tc.processMu.Unlock() tc.killProcessIfAlreadyTimedOut(process) } func (tc *timeoutContext) killProcessIfAlreadyTimedOut(process *os.Process) { if tc.Context.Err() != context.DeadlineExceeded || process == nil { return } if tc.logEntry != nil { _ = tc.executor.Kill(tc.logEntry) return } _ = tc.executor.Kill(&InternalLogEntry{Process: process}) }