|
1 | 1 | package exec |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "os" |
4 | 5 | "os/exec" |
| 6 | + "path/filepath" |
5 | 7 | "regexp" |
6 | 8 | "syscall" |
7 | 9 | "testing" |
@@ -215,3 +217,51 @@ func TestRunCaptureStderr(t *testing.T) { |
215 | 217 | assert.Equal(t, "hello world\nmy-error", output) |
216 | 218 | assert.NoError(t, err) |
217 | 219 | } |
| 220 | + |
| 221 | +// This test demonstrates that when a process group is signaled, all child processes are also terminated and file locks released. |
| 222 | +func TestProcessGroupSignalRemovesChildLock(t *testing.T) { |
| 223 | + hook := test.NewGlobal() |
| 224 | + log.SetLevel(log.DebugLevel) |
| 225 | + defer log.SetLevel(log.InfoLevel) |
| 226 | + |
| 227 | + dir := t.TempDir() |
| 228 | + lockFile := filepath.Join(dir, "lockfile") |
| 229 | + childScript := filepath.Join(dir, "child.sh") |
| 230 | + parentScript := filepath.Join(dir, "parent.sh") |
| 231 | + |
| 232 | + // Child: create lock file; on SIGTERM remove it and exit |
| 233 | + child := "#!/bin/sh\n" + |
| 234 | + "trap 'rm -f lockfile; exit 0' TERM\n" + |
| 235 | + "touch lockfile\n" + |
| 236 | + "sleep 100\n" |
| 237 | + require.NoError(t, os.WriteFile(childScript, []byte(child), 0o755)) |
| 238 | + |
| 239 | + // Parent: start child in background and sleep |
| 240 | + parent := "#!/bin/sh\n" + |
| 241 | + "./child.sh &\n" + |
| 242 | + "sleep 100\n" |
| 243 | + require.NoError(t, os.WriteFile(parentScript, []byte(parent), 0o755)) |
| 244 | + |
| 245 | + // Run parent with a short timeout; our implementation signals the process group |
| 246 | + opts := CmdOpts{ |
| 247 | + Timeout: 500 * time.Millisecond, |
| 248 | + FatalTimeout: 500 * time.Millisecond, |
| 249 | + TimeoutBehavior: TimeoutBehavior{ |
| 250 | + Signal: syscall.SIGTERM, |
| 251 | + ShouldWait: true, |
| 252 | + }, |
| 253 | + } |
| 254 | + _, err := RunCommand("sh", opts, "-c", "cd "+dir+" && ./parent.sh") |
| 255 | + require.Error(t, err) |
| 256 | + |
| 257 | + // Give a bit of time for traps to run and for the process tree to settle |
| 258 | + time.Sleep(200 * time.Millisecond) |
| 259 | + |
| 260 | + // Because the process group was signaled, the child should have removed the lock |
| 261 | + _, statErr := os.Stat(lockFile) |
| 262 | + require.Error(t, statErr, "expected lock file to be removed when process group is signaled") |
| 263 | + assert.True(t, os.IsNotExist(statErr)) |
| 264 | + |
| 265 | + // basic sanity: logs produced |
| 266 | + require.GreaterOrEqual(t, len(hook.Entries), 1) |
| 267 | +} |
0 commit comments