From 1cf10874a83ea67d601ed250d9963f2e0c2d64e6 Mon Sep 17 00:00:00 2001 From: booleanmaybe Date: Sun, 5 Apr 2026 12:37:25 -0400 Subject: [PATCH] fix unit run --- service/cmdutil_unix.go | 18 ++++++++++++++++++ service/cmdutil_windows.go | 10 ++++++++++ service/trigger_engine.go | 2 ++ service/trigger_engine_test.go | 12 ++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 service/cmdutil_unix.go create mode 100644 service/cmdutil_windows.go diff --git a/service/cmdutil_unix.go b/service/cmdutil_unix.go new file mode 100644 index 0000000..691c100 --- /dev/null +++ b/service/cmdutil_unix.go @@ -0,0 +1,18 @@ +//go:build !windows + +package service + +import ( + "os/exec" + "syscall" +) + +// setProcessGroup configures the command to run in its own process group +// and overrides Cancel to kill the entire group (parent + children). +func setProcessGroup(cmd *exec.Cmd) { + cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + cmd.Cancel = func() error { + // negative pid → kill the whole process group + return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) + } +} diff --git a/service/cmdutil_windows.go b/service/cmdutil_windows.go new file mode 100644 index 0000000..f1cfeb2 --- /dev/null +++ b/service/cmdutil_windows.go @@ -0,0 +1,10 @@ +//go:build windows + +package service + +import "os/exec" + +// setProcessGroup is a no-op on Windows. +// Windows has no process-group kill equivalent to Unix's kill(-pgid). +// cmd.WaitDelay (set by the caller) bounds the pipe drain if children outlive the parent. +func setProcessGroup(_ *exec.Cmd) {} diff --git a/service/trigger_engine.go b/service/trigger_engine.go index a37c743..e1210cd 100644 --- a/service/trigger_engine.go +++ b/service/trigger_engine.go @@ -192,6 +192,8 @@ func (te *TriggerEngine) execRun(ctx context.Context, entry triggerEntry, tc *ru defer cancel() cmd := exec.CommandContext(runCtx, "sh", "-c", cmdStr) //nolint:gosec // cmdStr is a user-configured trigger action, intentionally dynamic + setProcessGroup(cmd) + cmd.WaitDelay = 3 * time.Second output, err := cmd.CombinedOutput() if err != nil { slog.Error("trigger run() command failed", diff --git a/service/trigger_engine_test.go b/service/trigger_engine_test.go index 22c3b54..de077e0 100644 --- a/service/trigger_engine_test.go +++ b/service/trigger_engine_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "runtime" "strings" "testing" "time" @@ -374,8 +375,17 @@ func TestTriggerEngine_DepthExceededAtGateLevel(t *testing.T) { } // --- run() trigger --- +// These tests invoke sh -c which requires a Unix shell. + +func skipOnWindows(t *testing.T) { + t.Helper() + if runtime.GOOS == "windows" { + t.Skip("run() triggers use sh -c, skipping on Windows") + } +} func TestTriggerEngine_RunCommand(t *testing.T) { + skipOnWindows(t) entry := parseTriggerEntry(t, "echo trigger", `after update where new.status = "done" run("echo " + old.id)`) @@ -394,6 +404,7 @@ func TestTriggerEngine_RunCommand(t *testing.T) { } func TestTriggerEngine_RunCommandFailure(t *testing.T) { + skipOnWindows(t) entry := parseTriggerEntry(t, "failing command", `after update where new.status = "done" run("exit 1")`) @@ -412,6 +423,7 @@ func TestTriggerEngine_RunCommandFailure(t *testing.T) { } func TestTriggerEngine_RunCommandTimeout(t *testing.T) { + skipOnWindows(t) // use a run() trigger whose command outlives the parent context's deadline entry := parseTriggerEntry(t, "slow command", `after update where new.status = "done" run("sleep 30")`)