Files
OliveTin/service/internal/executor/timeout_context.go
2026-05-19 22:25:56 +01:00

73 lines
1.8 KiB
Go

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})
}