Skip to content

Commit ab59482

Browse files
committed
[cargo-nextest] block/unblock SIGTSTP while double-spawning
This is only really useful with Rust 1.66+ (currently in beta), which has rust-lang/rust#101077. But let's get this in so we can experiment with it.
1 parent e5caa33 commit ab59482

File tree

6 files changed

+147
-15
lines changed

6 files changed

+147
-15
lines changed

Cargo.lock

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cargo-nextest/src/double_spawn.rs

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
use crate::{output::Color, ExpectedError, Result};
55
use camino::Utf8PathBuf;
66
use clap::Args;
7+
use nextest_runner::double_spawn::double_spawn_child_init;
78
use std::os::unix::process::CommandExt;
89

910
#[derive(Debug, Args)]
@@ -19,6 +20,7 @@ impl DoubleSpawnOpts {
1920
pub(crate) fn exec(self) -> Result<i32> {
2021
// This double-spawned process should never use coloring.
2122
Color::Never.init();
23+
double_spawn_child_init();
2224
let args = shell_words::split(&self.args).map_err(|err| {
2325
ExpectedError::DoubleSpawnParseArgsError {
2426
args: self.args,

nextest-runner/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ nextest-workspace-hack = { version = "0.1", path = "../workspace-hack" }
8686

8787
[target.'cfg(unix)'.dependencies]
8888
libc = "0.2.137"
89+
nix = { version = "0.25.0", default-features = false, features = ["signal"] }
8990

9091
[target.'cfg(windows)'.dependencies]
9192
windows = { version = "0.42.0", features = [

nextest-runner/src/double_spawn.rs

+111-8
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,28 @@
55
//!
66
//! Nextest has experimental support on Unix for spawning test processes twice, to enable better
77
//! isolation and solve some thorny issues.
8+
//!
9+
//! ## Issues this currently solves
10+
//!
11+
//! ### `posix_spawn` SIGTSTP race
12+
//!
13+
//! It's been empirically observed that if nextest receives a `SIGTSTP` (Ctrl-Z) while it's running,
14+
//! it can get completely stuck sometimes. This is due to a race between the child being spawned and it
15+
//! receiving a `SIGTSTP` signal.
16+
//!
17+
//! For more details, see [this
18+
//! message](https://sourceware.org/pipermail/libc-help/2022-August/006263.html) on the glibc-help
19+
//! mailing list.
20+
//!
21+
//! To solve this issue, we do the following:
22+
//!
23+
//! 1. In the main nextest runner process, using `DoubleSpawnContext`, block `SIGTSTP` in the
24+
//! current thread (using `pthread_sigmask`) before spawning the stub child cargo-nextest
25+
//! process.
26+
//! 2. In the stub child process, unblock `SIGTSTP`.
27+
//!
28+
//! With this approach, the race condition between posix_spawn and `SIGTSTP` no longer exists.
829
9-
use self::imp::DoubleSpawnInfoImp;
1030
use std::path::Path;
1131

1232
/// Information about double-spawning processes. This determines whether a process will be
@@ -15,7 +35,7 @@ use std::path::Path;
1535
/// This is used by the main nextest process.
1636
#[derive(Clone, Debug)]
1737
pub struct DoubleSpawnInfo {
18-
inner: DoubleSpawnInfoImp,
38+
inner: imp::DoubleSpawnInfo,
1939
}
2040

2141
impl DoubleSpawnInfo {
@@ -27,39 +47,75 @@ impl DoubleSpawnInfo {
2747
/// This is super experimental, and should be used with caution.
2848
pub fn enabled() -> Self {
2949
Self {
30-
inner: DoubleSpawnInfoImp::enabled(),
50+
inner: imp::DoubleSpawnInfo::enabled(),
3151
}
3252
}
3353

3454
/// This returns a `DoubleSpawnInfo` which disables double-spawning.
3555
pub fn disabled() -> Self {
3656
Self {
37-
inner: DoubleSpawnInfoImp::disabled(),
57+
inner: imp::DoubleSpawnInfo::disabled(),
3858
}
3959
}
60+
4061
/// Returns the current executable, if one is available.
4162
///
4263
/// If `None`, double-spawning is not used.
4364
pub fn current_exe(&self) -> Option<&Path> {
4465
self.inner.current_exe()
4566
}
67+
68+
/// Returns a context that is meant to be obtained before spawning processes and dropped afterwards.
69+
pub fn spawn_context(&self) -> Option<DoubleSpawnContext> {
70+
self.current_exe().map(|_| DoubleSpawnContext::new())
71+
}
72+
}
73+
74+
/// Context to be used before spawning processes and dropped afterwards.
75+
///
76+
/// Returned by [`DoubleSpawnInfo::spawn_context`].
77+
#[derive(Debug)]
78+
pub struct DoubleSpawnContext {
79+
// Only used for the Drop impl.
80+
#[allow(dead_code)]
81+
inner: imp::DoubleSpawnContext,
82+
}
83+
84+
impl DoubleSpawnContext {
85+
#[inline]
86+
fn new() -> Self {
87+
Self {
88+
inner: imp::DoubleSpawnContext::new(),
89+
}
90+
}
91+
92+
/// Close the double-spawn context, dropping any changes that needed to be done to it.
93+
pub fn finish(self) {}
94+
}
95+
96+
/// Initialization for the double-spawn child.
97+
pub fn double_spawn_child_init() {
98+
imp::double_spawn_child_init()
4699
}
47100

48101
#[cfg(unix)]
49102
mod imp {
103+
use nix::sys::{signal::Signal, signalfd::SigSet};
104+
50105
use super::*;
51106
use std::path::PathBuf;
52107

53108
#[derive(Clone, Debug)]
54-
pub(super) struct DoubleSpawnInfoImp {
109+
pub(super) struct DoubleSpawnInfo {
55110
current_exe: Option<PathBuf>,
56111
}
57112

58-
impl DoubleSpawnInfoImp {
113+
impl DoubleSpawnInfo {
59114
#[inline]
60115
pub(super) fn enabled() -> Self {
61116
// Attempt to obtain the current exe, and warn if it couldn't be found.
62117
// TODO: maybe add an option to fail?
118+
// TODO: Always use /proc/self/exe directly on Linux, just make sure it's always accessible
63119
let current_exe = std::env::current_exe().map_or_else(
64120
|error| {
65121
log::warn!(
@@ -82,16 +138,50 @@ mod imp {
82138
self.current_exe.as_deref()
83139
}
84140
}
141+
142+
#[derive(Debug)]
143+
pub(super) struct DoubleSpawnContext {
144+
to_unblock: Option<SigSet>,
145+
}
146+
147+
impl DoubleSpawnContext {
148+
#[inline]
149+
pub(super) fn new() -> Self {
150+
// Block SIGTSTP, unblocking it in the child process. This avoids a complex race
151+
// condition.
152+
let mut sigset = SigSet::empty();
153+
sigset.add(Signal::SIGTSTP);
154+
let to_unblock = sigset.thread_block().ok().map(|()| sigset);
155+
Self { to_unblock }
156+
}
157+
}
158+
159+
impl Drop for DoubleSpawnContext {
160+
fn drop(&mut self) {
161+
if let Some(sigset) = &self.to_unblock {
162+
_ = sigset.thread_unblock();
163+
}
164+
}
165+
}
166+
167+
#[inline]
168+
pub(super) fn double_spawn_child_init() {
169+
let mut sigset = SigSet::empty();
170+
sigset.add(Signal::SIGTSTP);
171+
if sigset.thread_unblock().is_err() {
172+
log::warn!("[double-spawn] unable to unblock SIGTSTP in child");
173+
}
174+
}
85175
}
86176

87177
#[cfg(not(unix))]
88178
mod imp {
89179
use super::*;
90180

91181
#[derive(Clone, Debug)]
92-
pub(super) struct DoubleSpawnInfoImp {}
182+
pub(super) struct DoubleSpawnInfo {}
93183

94-
impl DoubleSpawnInfoImp {
184+
impl DoubleSpawnInfo {
95185
#[inline]
96186
pub(super) fn enabled() -> Self {
97187
Self {}
@@ -107,4 +197,17 @@ mod imp {
107197
None
108198
}
109199
}
200+
201+
#[derive(Debug)]
202+
pub(super) struct DoubleSpawnContext {}
203+
204+
impl DoubleSpawnContext {
205+
#[inline]
206+
pub(super) fn new() -> Self {
207+
Self {}
208+
}
209+
}
210+
211+
#[inline]
212+
pub(super) fn double_spawn_child_init() {}
110213
}

nextest-runner/src/test_command.rs

+16-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// SPDX-License-Identifier: MIT OR Apache-2.0
33

44
use crate::{
5-
cargo_config::EnvironmentMap, double_spawn::DoubleSpawnInfo, helpers::dylib_path_envvar,
5+
cargo_config::EnvironmentMap,
6+
double_spawn::{DoubleSpawnContext, DoubleSpawnInfo},
7+
helpers::dylib_path_envvar,
68
target_runner::TargetRunner,
79
};
810
use camino::Utf8PathBuf;
@@ -25,6 +27,8 @@ pub(crate) struct LocalExecuteContext<'a> {
2527
pub(crate) struct TestCommand {
2628
/// The command to be run.
2729
command: std::process::Command,
30+
/// Double-spawn context.
31+
double_spawn: Option<DoubleSpawnContext>,
2832
}
2933

3034
impl TestCommand {
@@ -164,7 +168,12 @@ impl TestCommand {
164168
cmd.env(format!("NEXTEST_BIN_EXE_{}", name), path);
165169
}
166170

167-
Self { command: cmd }
171+
let double_spawn = ctx.double_spawn.spawn_context();
172+
173+
Self {
174+
command: cmd,
175+
double_spawn,
176+
}
168177
}
169178

170179
#[inline]
@@ -174,6 +183,10 @@ impl TestCommand {
174183

175184
pub(crate) fn spawn(self) -> std::io::Result<tokio::process::Child> {
176185
let mut command = tokio::process::Command::from(self.command);
177-
command.spawn()
186+
let res = command.spawn();
187+
if let Some(ctx) = self.double_spawn {
188+
ctx.finish();
189+
}
190+
res
178191
}
179192
}

workspace-hack/Cargo.toml

+4-4
Original file line numberDiff line numberDiff line change
@@ -45,26 +45,26 @@ syn = { version = "1.0.103", features = ["clone-impls", "derive", "extra-traits"
4545
futures-core = { version = "0.3.25", features = ["alloc", "std"] }
4646
futures-sink = { version = "0.3.25" }
4747
indexmap = { version = "1.9.1", default-features = false, features = ["std"] }
48-
libc = { version = "0.2.137", features = ["std"] }
48+
libc = { version = "0.2.137", features = ["extra_traits", "std"] }
4949
once_cell = { version = "1.16.0", features = ["alloc", "race", "std"] }
5050
tokio = { version = "1.21.2", features = ["bytes", "io-util", "libc", "macros", "memchr", "mio", "net", "num_cpus", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "time", "tokio-macros"] }
5151

5252
[target.x86_64-unknown-linux-gnu.build-dependencies]
5353
getrandom = { version = "0.2.8", default-features = false, features = ["std"] }
54-
libc = { version = "0.2.137", features = ["std"] }
54+
libc = { version = "0.2.137", features = ["extra_traits", "std"] }
5555
once_cell = { version = "1.16.0", features = ["alloc", "race", "std"] }
5656

5757
[target.x86_64-apple-darwin.dependencies]
5858
futures-core = { version = "0.3.25", features = ["alloc", "std"] }
5959
futures-sink = { version = "0.3.25" }
6060
indexmap = { version = "1.9.1", default-features = false, features = ["std"] }
61-
libc = { version = "0.2.137", features = ["std"] }
61+
libc = { version = "0.2.137", features = ["extra_traits", "std"] }
6262
once_cell = { version = "1.16.0", features = ["alloc", "race", "std"] }
6363
tokio = { version = "1.21.2", features = ["bytes", "io-util", "libc", "macros", "memchr", "mio", "net", "num_cpus", "process", "rt", "rt-multi-thread", "signal", "signal-hook-registry", "socket2", "sync", "time", "tokio-macros"] }
6464

6565
[target.x86_64-apple-darwin.build-dependencies]
6666
getrandom = { version = "0.2.8", default-features = false, features = ["std"] }
67-
libc = { version = "0.2.137", features = ["std"] }
67+
libc = { version = "0.2.137", features = ["extra_traits", "std"] }
6868
once_cell = { version = "1.16.0", features = ["alloc", "race", "std"] }
6969

7070
[target.x86_64-pc-windows-msvc.dependencies]

0 commit comments

Comments
 (0)