1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Shared browser-process lifecycle helpers.
//!
//! Chrome/Firefox spawn a pool of subprocesses (renderer, GPU, utility,
//! zygote). When the parent dies via SIGKILL — as happens when a test
//! harness panics or `kill_on_drop(true)` fires — those helpers are
//! supposed to notice the parent IPC pipe closing and exit on their own.
//! In practice, on macOS this is flaky: helpers can linger for seconds
//! or get stuck, showing up as "automation Chrome zombies" in tools like
//! `box-dev-gate browser zombies` that pgrep `--remote-debugging` etc.
//!
//! Defence: every browser spawn calls `setsid()` in `pre_exec`, making
//! the parent its own session-and-process-group leader. Every helper
//! the parent forks inherits that group. On teardown we explicitly
//! `killpg(-pgid, SIGKILL)` so the whole group dies together — no
//! lingering helpers, regardless of how the parent itself died.
//!
//! Combine with `tokio::process::Command::kill_on_drop(true)`:
//! - `kill_on_drop` covers the *Rust* side (SIGKILL to the parent PID
//! when the `Child` handle drops).
//! - `killpg` covers the *OS* side (all helpers in the same group die
//! too, even if Chrome itself crashed or spun off sandboxed children).
/// `pre_exec` closure suitable for every browser `Command` in this crate.
///
/// Runs inside the forked child before `exec`, putting the child in its
/// own session and process group. Any error is silently ignored —
/// failing `setsid` only matters when the current process is already a
/// session leader, which is fine for tests.
///
/// # Safety
///
/// `setsid()` is async-signal-safe per POSIX.1-2017, so it is safe to
/// call from `pre_exec`. No allocation, no mutex, no non-reentrant C
/// functions. The returned closure captures nothing.
+ Send + Sync + 'static
/// Send `SIGKILL` to every process in the given pid's process group.
///
/// Assumes the target was spawned with [`setsid_pre_exec`], so its
/// `pgid == pid`. Failures are logged at `debug` level and ignored —
/// the common cases are ESRCH (group already dead) and EPERM (we don't
/// own the group), neither of which is actionable.
/// `tokio::process::Child` wrapper that kills the entire process group
/// on drop. Combine with [`setsid_pre_exec`] on the `Command` so the
/// parent is its own session+group leader; every helper it forks
/// inherits the group and dies together on teardown. Without this,
/// SIGKILL to the parent leaves renderer/GPU/zygote subprocesses
/// behind on macOS — visible as "automation Chrome zombies" in
/// `box-dev-gate browser zombies`.
///
/// The inner `Child` still has `kill_on_drop(true)` set, so the parent
/// PID is also killed directly (belt + suspenders). The group kill
/// runs first because fields drop in declaration order.