Skip to content

Commit 951e4e6

Browse files
authored
refactor(fbuild-core): migrate to running-process-core 3.4 (crates.io) (#254)
1 parent 60a4ae3 commit 951e4e6

4 files changed

Lines changed: 65 additions & 46 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,12 +71,8 @@ tracing-test = "0.2"
7171

7272
# Process containment: all subprocess spawns the daemon performs (compilers,
7373
# esptool, qemu, simavr, node, npm, …) and any grandchildren they fork must
74-
# die with the daemon. Pinned to a pre-release rev that exposes the
75-
# `Containment` enum / `spawn_with_containment` API. The published
76-
# `running-process-core` 3.4.x dropped those in favor of
77-
# `ContainedProcessGroup` — fbuild will need a containment-layer
78-
# refactor before it can move to crates.io. See FastLED/fbuild#32.
79-
running-process-core = { git = "https://github.com/zackees/running-process", rev = "ff9d7972504f7a0dcee0f410274daf3b02e4fcc2", package = "running-process-core" }
74+
# die with the daemon. See FastLED/fbuild#32.
75+
running-process-core = "3.4"
8076

8177
[profile.dev]
8278
debug = 0

crates/fbuild-core/src/containment.rs

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,7 @@
5656
use std::process::{Child, Command};
5757
use 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.
116117
pub 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> {
141151
pub 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()?;

crates/fbuild-core/src/subprocess.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@
1212
//! * strip MSYS/MSYS2 env vars that would otherwise poison native
1313
//! Windows toolchain binaries.
1414
//!
15-
//! Containment is honoured via `ProcessConfig::containment = Some(...)`
16-
//! when the daemon has installed the global containment group. CLI
17-
//! binaries and unit tests run uncontained just as before.
15+
//! On Windows, containment is applied post-spawn by
16+
//! [`crate::containment::windows_job::assign`] when the daemon has
17+
//! installed the global containment group; CLI binaries and unit tests
18+
//! run uncontained just as before. (Earlier code used a `containment:`
19+
//! field on `ProcessConfig` from a pre-release `running-process-core`;
20+
//! the published 3.4 API does not expose that field — see #32.)
1821
1922
use std::path::Path;
2023
use std::time::Duration;
2124

2225
use running_process_core::{
23-
CommandSpec, Containment, NativeProcess, ProcessConfig, ProcessError, StderrMode, StdinMode,
26+
CommandSpec, NativeProcess, ProcessConfig, ProcessError, StderrMode, StdinMode,
2427
};
2528

2629
use crate::{FbuildError, Result};
@@ -153,11 +156,6 @@ fn build_config(
153156
create_process_group: false,
154157
stdin_mode,
155158
nice: None,
156-
containment: if crate::containment::is_initialised() {
157-
Some(Containment::Contained)
158-
} else {
159-
None
160-
},
161159
})
162160
}
163161

0 commit comments

Comments
 (0)