Skip to content

Commit 1eb4dc9

Browse files
authored
fix: isolate parent exit watcher environment (#2256)
1 parent 12fd2d7 commit 1eb4dc9

2 files changed

Lines changed: 85 additions & 0 deletions

File tree

internal/cmn/cmdutil/parent_exit_watcher_unix.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func StartParentExitWatcher(cmd *exec.Cmd) (func(), error) {
3131
const script = `if IFS= read -r line <&3 && [ "$line" = "` + parentExitWatcherOK + `" ]; then exit 0; fi; kill -KILL -"$1" 2>/dev/null; exit 0`
3232
watcher := exec.Command("/bin/sh", "-c", script, "dagu-parent-exit-watcher", strconv.Itoa(cmd.Process.Pid)) //nolint:gosec
3333
watcher.ExtraFiles = []*os.File{readPipe}
34+
// Keep shell control variables from changing the watcher's blocking read.
35+
watcher.Env = []string{"PATH=/usr/bin:/bin"}
3436
setupCommand(watcher)
3537

3638
if err := watcher.Start(); err != nil {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (C) 2026 Yota Hamada
2+
// SPDX-License-Identifier: GPL-3.0-or-later
3+
4+
//go:build !windows
5+
6+
package cmdutil_test
7+
8+
import (
9+
"bufio"
10+
"errors"
11+
"fmt"
12+
"os"
13+
"os/exec"
14+
"strconv"
15+
"strings"
16+
"syscall"
17+
"testing"
18+
"time"
19+
20+
"github.com/dagucloud/dagu/internal/cmn/cmdutil"
21+
"github.com/stretchr/testify/require"
22+
)
23+
24+
const parentExitWatcherHelperEnv = "DAGU_PARENT_EXIT_WATCHER_HELPER"
25+
26+
func TestParentExitWatcherIgnoresInheritedShellTimeout(t *testing.T) {
27+
t.Setenv("TMOUT", "1")
28+
29+
cmd := exec.Command("/bin/sh", "-c", "sleep 2") //nolint:gosec
30+
cmd.Env = []string{"PATH=/usr/bin:/bin"}
31+
32+
startedAt := time.Now()
33+
proc, err := cmdutil.StartManagedProcess(cmd)
34+
require.NoError(t, err)
35+
defer func() { _ = proc.Release() }()
36+
37+
require.NoError(t, proc.Wait())
38+
require.GreaterOrEqual(t, time.Since(startedAt), 2*time.Second)
39+
}
40+
41+
func TestParentExitWatcherTerminatesChildWhenParentExits(t *testing.T) {
42+
executable, err := os.Executable()
43+
require.NoError(t, err)
44+
45+
helper := exec.Command(executable, "-test.run=^TestParentExitWatcherHelper$") //nolint:gosec
46+
helper.Env = append(os.Environ(), parentExitWatcherHelperEnv+"=1")
47+
48+
stdout, err := helper.StdoutPipe()
49+
require.NoError(t, err)
50+
require.NoError(t, helper.Start())
51+
52+
scanner := bufio.NewScanner(stdout)
53+
require.True(t, scanner.Scan(), "helper did not print child pid")
54+
childPID, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
55+
require.NoError(t, err)
56+
57+
require.NoError(t, helper.Wait())
58+
require.Eventually(t, func() bool {
59+
return !processExists(childPID)
60+
}, 3*time.Second, 50*time.Millisecond)
61+
}
62+
63+
func TestParentExitWatcherHelper(t *testing.T) {
64+
if os.Getenv(parentExitWatcherHelperEnv) != "1" {
65+
t.Skip("helper only")
66+
}
67+
68+
cmd := exec.Command("/bin/sh", "-c", "sleep 30") //nolint:gosec
69+
cmd.Env = []string{"PATH=/usr/bin:/bin"}
70+
proc, err := cmdutil.StartManagedProcess(cmd)
71+
require.NoError(t, err)
72+
73+
fmt.Println(proc.PID())
74+
os.Exit(0)
75+
}
76+
77+
func processExists(pid int) bool {
78+
err := syscall.Kill(pid, 0)
79+
if err == nil {
80+
return true
81+
}
82+
return !errors.Is(err, syscall.ESRCH)
83+
}

0 commit comments

Comments
 (0)