@@ -31,9 +31,9 @@ import Musl
3131public struct TeardownStep : Sendable , Hashable {
3232 internal enum Storage : Sendable , Hashable {
3333 #if !os(Windows)
34- case sendSignal( Signal , allowedDuration: Duration )
34+ case sendSignal( Signal , toProcessGroup : Bool , allowedDuration: Duration )
3535 #endif
36- case gracefulShutDown( allowedDuration: Duration )
36+ case gracefulShutDown( toProcessGroup : Bool , allowedDuration: Duration )
3737 case kill
3838 }
3939 var storage : Storage
@@ -43,13 +43,22 @@ public struct TeardownStep: Sendable, Hashable {
4343 /// before proceeding to the next step.
4444 ///
4545 /// The final step in the sequence always sends a `.kill` signal.
46+ ///
47+ /// - Important: When sending the signal to the process group, unless you
48+ /// also set `createSession` to `true`, or `processGroupID` to a
49+ /// non-inherited value, the targeted process group includes the parent
50+ /// process. This is almost never what you want. Pair `toProcessGroup`
51+ /// with `createSession` to isolate the subprocess and its descendants in
52+ /// their own session.
4653 public static func send(
4754 signal: Signal ,
55+ toProcessGroup: Bool = false ,
4856 allowedDurationToNextStep: Duration
4957 ) -> Self {
5058 return Self (
5159 storage: . sendSignal(
5260 signal,
61+ toProcessGroup: toProcessGroup,
5362 allowedDuration: allowedDurationToNextStep
5463 )
5564 )
@@ -64,11 +73,22 @@ public struct TeardownStep: Sendable, Hashable {
6473 /// 1. Sends `WM_CLOSE` if the child process is a GUI process.
6574 /// 2. Sends `CTRL_C_EVENT` to the console.
6675 /// 3. Sends `CTRL_BREAK_EVENT` to the process group.
76+ ///
77+ /// - Important: On Unix, when sending the signal to the process group,
78+ /// unless you also set `createSession` to `true`, or `processGroupID`
79+ /// to a non-inherited value, the targeted process group includes the parent
80+ /// process. This is almost never what you want. Pair `toProcessGroup`
81+ /// with `createSession` to isolate the subprocess and its descendants in
82+ /// their own session. On Windows, the `toProcessGroup` parameter has no
83+ /// effect; `WM_CLOSE` and `CTRL_C_EVENT` have no process-group equivalent,
84+ /// and `CTRL_BREAK_EVENT` is always sent to the process group.
6785 public static func gracefulShutDown(
86+ toProcessGroup: Bool = false ,
6887 allowedDurationToNextStep: Duration
6988 ) -> Self {
7089 return Self (
7190 storage: . gracefulShutDown(
91+ toProcessGroup: toProcessGroup,
7292 allowedDuration: allowedDurationToNextStep
7393 )
7494 )
@@ -95,6 +115,7 @@ internal enum TeardownStepCompletion {
95115
96116extension Execution {
97117 internal func gracefulShutDown(
118+ toProcessGroup: Bool ,
98119 allowedDurationToNextStep duration: Duration
99120 ) async {
100121 #if os(Windows)
@@ -129,16 +150,30 @@ extension Execution {
129150 // Send SIGTERM
130151 try ? self . send (
131152 signal: . terminate,
132- toProcessGroup: false
153+ toProcessGroup: toProcessGroup
133154 )
134155 #endif
135156 }
136157
137158 internal func runTeardownSequence(
138159 _ sequence: some Sequence < TeardownStep > & Sendable
139160 ) async {
140- // First insert the `.kill` step
141- let finalSequence = sequence + [ TeardownStep ( storage: . kill) ]
161+ // The implicit final `.kill` inherits `toProcessGroup` from the last
162+ // explicit step in the sequence. This matches user intent: A sequence
163+ // configured to target the process group should have its terminal kill
164+ // also target the group, so descendants don't leak after teardown.
165+ let steps = Array ( sequence)
166+ let killProcessGroup : Bool = {
167+ guard let last = steps. last else { return false }
168+ switch last. storage {
169+ #if !os(Windows)
170+ case . sendSignal( _, let toProcessGroup, _) : return toProcessGroup
171+ #endif
172+ case . gracefulShutDown( let toProcessGroup, _) : return toProcessGroup
173+ case . kill: return false
174+ }
175+ } ( )
176+ let finalSequence = steps + [ TeardownStep ( storage: . kill) ]
142177 for step in finalSequence {
143178 let stepCompletion : TeardownStepCompletion
144179 guard self . isPotentiallyStillAlive ( ) else {
@@ -147,7 +182,7 @@ extension Execution {
147182 }
148183
149184 switch step. storage {
150- case . gracefulShutDown( let allowedDuration) :
185+ case . gracefulShutDown( let toProcessGroup , let allowedDuration) :
151186 stepCompletion = await withTaskGroup ( of: TeardownStepCompletion . self) { group in
152187 group. addTask {
153188 do {
@@ -160,12 +195,13 @@ extension Execution {
160195 }
161196 }
162197 await self . gracefulShutDown (
198+ toProcessGroup: toProcessGroup,
163199 allowedDurationToNextStep: allowedDuration
164200 )
165201 return await group. next ( ) !
166202 }
167203 #if !os(Windows)
168- case . sendSignal( let signal, let allowedDuration) :
204+ case . sendSignal( let signal, let toProcessGroup , let allowedDuration) :
169205 stepCompletion = await withTaskGroup ( of: TeardownStepCompletion . self) { group in
170206 group. addTask {
171207 do {
@@ -177,7 +213,7 @@ extension Execution {
177213 return . processHasExited
178214 }
179215 }
180- try ? self . send ( signal: signal, toProcessGroup: false )
216+ try ? self . send ( signal: signal, toProcessGroup: toProcessGroup )
181217 return await group. next ( ) !
182218 }
183219 #endif // !os(Windows)
@@ -187,7 +223,7 @@ extension Execution {
187223 withExitCode: 0
188224 )
189225 #else
190- try ? self . send ( signal: . kill, toProcessGroup: false )
226+ try ? self . send ( signal: . kill, toProcessGroup: killProcessGroup )
191227 #endif
192228 stepCompletion = . killedTheProcess
193229 }
0 commit comments