Files
osmedeus/internal/executor/run_control_plane.go
T
j3ssie 8448791c3c feat: propagate run cancellation to tmux sessions and remote SSH processes
- Track tmux sessions in ActiveRun and kill them on run cancel via new
  TmuxHooks indirection; expose killed_tmux_sessions in CLI and API responses
- Add ExecuteSSHCommand with remote pidfile + process-group kill watcher so
  cancelling a run actually terminates remote scans (not just the local session)
- Route ssh_exec/ssh_rsync/sync_* through the run's cancellable context via
  new RunContextHooks
- Switch docker-publish to sequential per-arch buildx builds + imagetools
  manifest to avoid OOM on multi-arch builds; add docker-buildx-setup target
- Cross-compile Dockerfile via BUILDPLATFORM/TARGETOS/TARGETARCH and retry
  SAST binary installs to survive QEMU-flaky downloads
- Bump version to v5.0.3
2026-05-26 00:06:26 +08:00

255 lines
6.5 KiB
Go

package executor
import (
"context"
"fmt"
"os/exec"
"sync"
"syscall"
"time"
)
// ActiveRun represents a currently executing workflow run
type ActiveRun struct {
RunUUID string
Ctx context.Context // cancellable context bound to this run
Cancel context.CancelFunc
PIDs *sync.Map // map[int]struct{} - currently running PIDs
TmuxSessions *sync.Map // map[string]struct{} - tmux sessions spawned by tmux_run
StartedAt time.Time
}
// AddPID adds a process ID to this run's tracked PIDs
func (a *ActiveRun) AddPID(pid int) {
if a.PIDs != nil {
a.PIDs.Store(pid, struct{}{})
}
}
// RemovePID removes a process ID from this run's tracked PIDs
func (a *ActiveRun) RemovePID(pid int) {
if a.PIDs != nil {
a.PIDs.Delete(pid)
}
}
// AddTmuxSession records a tmux session name spawned during this run.
func (a *ActiveRun) AddTmuxSession(name string) {
if a.TmuxSessions != nil && name != "" {
a.TmuxSessions.Store(name, struct{}{})
}
}
// RemoveTmuxSession drops a tmux session name from tracking (e.g. on tmux_kill).
func (a *ActiveRun) RemoveTmuxSession(name string) {
if a.TmuxSessions != nil && name != "" {
a.TmuxSessions.Delete(name)
}
}
// KillAllTmuxSessions issues `tmux kill-session` for every tracked session and
// returns the list of session names it attempted to kill. Errors are swallowed
// because best-effort cleanup is preferred over aborting cancellation.
func (a *ActiveRun) KillAllTmuxSessions() []string {
var killed []string
if a.TmuxSessions == nil {
return killed
}
tmuxBin, lookErr := exec.LookPath("tmux")
a.TmuxSessions.Range(func(key, _ any) bool {
name, ok := key.(string)
if !ok || name == "" {
return true
}
if lookErr == nil {
_ = exec.Command(tmuxBin, "kill-session", "-t", name).Run()
}
killed = append(killed, name)
a.TmuxSessions.Delete(name)
return true
})
return killed
}
// KillAllPIDs sends SIGKILL to all tracked PIDs and returns the list of killed PIDs
func (a *ActiveRun) KillAllPIDs() []int {
var killed []int
if a.PIDs == nil {
return killed
}
a.PIDs.Range(func(key, _ any) bool {
pid, ok := key.(int)
if !ok {
return true
}
// Kill the process group (negative PID kills all processes in the group)
// This ensures child processes are also terminated
if err := syscall.Kill(-pid, syscall.SIGKILL); err != nil {
// Try killing just the process if process group kill fails
_ = syscall.Kill(pid, syscall.SIGKILL)
}
killed = append(killed, pid)
a.PIDs.Delete(pid)
return true
})
return killed
}
// RunControlPlane tracks active workflow runs for cancellation support
type RunControlPlane struct {
mu sync.RWMutex
runs map[string]*ActiveRun
}
var globalControlPlane *RunControlPlane
var controlPlaneOnce sync.Once
// GetRunControlPlane returns the singleton run control plane
func GetRunControlPlane() *RunControlPlane {
controlPlaneOnce.Do(func() {
globalControlPlane = &RunControlPlane{
runs: make(map[string]*ActiveRun),
}
})
return globalControlPlane
}
// Register adds a new run to the control plane.
// The provided context must be the cancellable context whose CancelFunc is
// `cancel`; storing it lets background helpers (e.g. ssh_exec) derive their
// own deadlines from the run's lifetime so cancellation propagates.
func (r *RunControlPlane) Register(runUUID string, cancel context.CancelFunc) *ActiveRun {
return r.RegisterWithContext(runUUID, nil, cancel)
}
// RegisterWithContext is like Register but also records the parent context.
// New callers should prefer this over Register so the stored Ctx is non-nil.
func (r *RunControlPlane) RegisterWithContext(runUUID string, ctx context.Context, cancel context.CancelFunc) *ActiveRun {
r.mu.Lock()
defer r.mu.Unlock()
activeRun := &ActiveRun{
RunUUID: runUUID,
Ctx: ctx,
Cancel: cancel,
PIDs: &sync.Map{},
TmuxSessions: &sync.Map{},
StartedAt: time.Now(),
}
r.runs[runUUID] = activeRun
return activeRun
}
// Unregister removes a run from the control plane
func (r *RunControlPlane) Unregister(runUUID string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.runs, runUUID)
}
// Get retrieves an active run by its UUID
func (r *RunControlPlane) Get(runUUID string) *ActiveRun {
r.mu.RLock()
defer r.mu.RUnlock()
return r.runs[runUUID]
}
// Cancel cancels a run by calling its cancel function and killing all tracked
// PIDs and tmux sessions. Returns the list of killed PIDs, killed tmux sessions,
// and any error.
func (r *RunControlPlane) Cancel(runUUID string) ([]int, []string, error) {
r.mu.Lock()
activeRun, exists := r.runs[runUUID]
r.mu.Unlock()
if !exists {
return nil, nil, fmt.Errorf("run %s not found in control plane", runUUID)
}
// First, cancel the context to stop any new operations
if activeRun.Cancel != nil {
activeRun.Cancel()
}
// Then tear down tracked side effects: tmux sessions outlive the parent
// process by design, so kill them before any tracked PIDs.
killedSessions := activeRun.KillAllTmuxSessions()
killedPIDs := activeRun.KillAllPIDs()
return killedPIDs, killedSessions, nil
}
// AddPID adds a PID to a run's tracked processes
func (r *RunControlPlane) AddPID(runUUID string, pid int) {
r.mu.RLock()
activeRun := r.runs[runUUID]
r.mu.RUnlock()
if activeRun != nil {
activeRun.AddPID(pid)
}
}
// RemovePID removes a PID from a run's tracked processes
func (r *RunControlPlane) RemovePID(runUUID string, pid int) {
r.mu.RLock()
activeRun := r.runs[runUUID]
r.mu.RUnlock()
if activeRun != nil {
activeRun.RemovePID(pid)
}
}
// AddTmuxSession registers a tmux session name against a run so the session
// can be torn down when the run is cancelled.
func (r *RunControlPlane) AddTmuxSession(runUUID, name string) {
if runUUID == "" || name == "" {
return
}
r.mu.RLock()
activeRun := r.runs[runUUID]
r.mu.RUnlock()
if activeRun != nil {
activeRun.AddTmuxSession(name)
}
}
// RemoveTmuxSession unregisters a tmux session name (e.g. after tmux_kill).
func (r *RunControlPlane) RemoveTmuxSession(runUUID, name string) {
if runUUID == "" || name == "" {
return
}
r.mu.RLock()
activeRun := r.runs[runUUID]
r.mu.RUnlock()
if activeRun != nil {
activeRun.RemoveTmuxSession(name)
}
}
// ListActive returns a list of all active run UUIDs
func (r *RunControlPlane) ListActive() []string {
r.mu.RLock()
defer r.mu.RUnlock()
uuids := make([]string, 0, len(r.runs))
for uuid := range r.runs {
uuids = append(uuids, uuid)
}
return uuids
}
// Count returns the number of active runs
func (r *RunControlPlane) Count() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.runs)
}