5656use std:: process:: { Child , Command } ;
5757use std:: sync:: OnceLock ;
5858
59- #[ cfg( unix) ]
60- use running_process_core:: ContainedProcessGroup ;
61- #[ cfg( windows) ]
62- use running_process_core:: { ContainedChild , ContainedProcessGroup , Containment } ;
59+ use running_process_core:: { ContainedProcessGroup , ORIGINATOR_ENV_VAR } ;
6360
6461/// Global process-wide containment group. Initialised once by the
6562/// daemon; remains `None` in non-daemon contexts (CLI binary, tests).
@@ -101,35 +98,48 @@ pub fn is_initialised() -> bool {
10198/// Falls back to uncontained `Command::spawn` when no global group has
10299/// been initialised (non-daemon binaries).
103100///
104- /// **Windows**: delegates to `ContainedProcessGroup::spawn` which
105- /// assigns the child to the Job Object — safe and stateless.
101+ /// **Windows**: spawn directly, then assign the child handle to a Job
102+ /// Object with `JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`
103+ /// ([`windows_job::assign`]) — the same containment mechanism the
104+ /// pre-publication `running-process-core` rev used internally,
105+ /// reimplemented locally since the published 3.4 API no longer exposes
106+ /// `spawn_with_containment(_, Containment::Contained)`.
106107///
107108/// **Unix**: installs a per-child `pre_exec` hook that creates a new
108109/// process group (`setpgid(0, 0)`) and, on Linux, requests
109- /// `PR_SET_PDEATHSIG(SIGKILL)`. This sidesteps
110- /// [ `ContainedProcessGroup::spawn_with_containment`]'s shared- pgid
111- /// behaviour, which fails with EPERM on the second spawn after the
112- /// first child (the pgid leader) has exited — the root cause of
110+ /// `PR_SET_PDEATHSIG(SIGKILL)`. We deliberately do not call
111+ /// `ContainedProcessGroup::spawn` here because the per-child pgid
112+ /// approach sidesteps an EPERM race that hit the second spawn after the
113+ /// first child (the pgid leader) had exited — the root cause of
113114/// FastLED/fbuild#129. The Linux kernel's `PR_SET_PDEATHSIG` still
114115/// enforces the "child dies with daemon" contract; macOS relies on
115116/// process-group-leader death alone.
116117pub fn spawn_contained ( command : & mut Command ) -> std:: io:: Result < Child > {
117118 #[ cfg( windows) ]
118119 {
119- match GLOBAL_GROUP . get ( ) {
120- Some ( group) => {
121- let ContainedChild { child, .. } =
122- group. spawn_with_containment ( command, Containment :: Contained ) ?;
123- Ok ( child)
124- }
125- None => command. spawn ( ) ,
120+ let Some ( group) = GLOBAL_GROUP . get ( ) else {
121+ return command. spawn ( ) ;
122+ } ;
123+ inject_originator_env ( command, group) ;
124+ let mut child = command. spawn ( ) ?;
125+ use std:: os:: windows:: io:: AsRawHandle ;
126+ if let Err ( e) = windows_job:: assign ( child. as_raw_handle ( ) ) {
127+ // The atomic spawn+assign that `ContainedProcessGroup::spawn_with_containment`
128+ // used to provide is gone in 3.4. If assign fails after spawn
129+ // succeeds, kill the orphan so the caller can't leak an
130+ // uncontained child by accident.
131+ let _ = child. kill ( ) ;
132+ let _ = child. wait ( ) ;
133+ return Err ( e) ;
126134 }
135+ Ok ( child)
127136 }
128137 #[ cfg( unix) ]
129138 {
130- if GLOBAL_GROUP . get ( ) . is_none ( ) {
139+ let Some ( group ) = GLOBAL_GROUP . get ( ) else {
131140 return command. spawn ( ) ;
132- }
141+ } ;
142+ inject_originator_env ( command, group) ;
133143 unix_install_pre_exec ( command) ;
134144 command. spawn ( )
135145 }
@@ -141,28 +151,39 @@ pub fn spawn_contained(command: &mut Command) -> std::io::Result<Child> {
141151pub fn spawn_detached ( command : & mut Command ) -> std:: io:: Result < Child > {
142152 #[ cfg( windows) ]
143153 {
144- match GLOBAL_GROUP . get ( ) {
145- Some ( group) => {
146- let ContainedChild { child, .. } =
147- group. spawn_with_containment ( command, Containment :: Detached ) ?;
148- Ok ( child)
149- }
150- None => command. spawn ( ) ,
154+ // Detached: no Job Object assignment so the child survives
155+ // when the daemon's job handle closes. We still inject the
156+ // originator env var for cross-process correlation.
157+ if let Some ( group) = GLOBAL_GROUP . get ( ) {
158+ inject_originator_env ( command, group) ;
151159 }
160+ command. spawn ( )
152161 }
153162 #[ cfg( unix) ]
154163 {
155164 // Detached: create a new session so the child survives the
156165 // daemon thread that spawned it. Matches the upstream behaviour
157166 // but without joining any shared pgid.
158- if GLOBAL_GROUP . get ( ) . is_none ( ) {
167+ let Some ( group ) = GLOBAL_GROUP . get ( ) else {
159168 return command. spawn ( ) ;
160- }
169+ } ;
170+ inject_originator_env ( command, group) ;
161171 unix_install_detached_pre_exec ( command) ;
162172 command. spawn ( )
163173 }
164174}
165175
176+ /// Mirror of `ContainedProcessGroup::inject_originator_env`: stamp
177+ /// `RUNNING_PROCESS_ORIGINATOR=TOOL:PID` onto the command's env. We do
178+ /// this manually because the published 3.4 API only exposes it via
179+ /// `ContainedProcessGroup::spawn` (which returns its own
180+ /// `SpawnedChild`, not a `std::process::Child` — see #32).
181+ fn inject_originator_env ( command : & mut Command , group : & ContainedProcessGroup ) {
182+ if let Some ( value) = group. originator_value ( ) {
183+ command. env ( ORIGINATOR_ENV_VAR , value) ;
184+ }
185+ }
186+
166187/// Install a `pre_exec` hook that puts the child in a fresh process
167188/// group (Unix) and, on Linux, asks the kernel to SIGKILL the child
168189/// when the spawning thread exits.
@@ -240,8 +261,11 @@ pub mod tokio_spawn {
240261 pub fn spawn_contained (
241262 command : & mut tokio:: process:: Command ,
242263 ) -> std:: io:: Result < tokio:: process:: Child > {
243- if ! super :: is_initialised ( ) {
264+ let Some ( group ) = super :: GLOBAL_GROUP . get ( ) else {
244265 return command. spawn ( ) ;
266+ } ;
267+ if let Some ( value) = group. originator_value ( ) {
268+ command. env ( super :: ORIGINATOR_ENV_VAR , value) ;
245269 }
246270 configure ( command) ;
247271 let child = command. spawn ( ) ?;
0 commit comments