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 ;
1030use 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 ) ]
1737pub struct DoubleSpawnInfo {
18- inner : DoubleSpawnInfoImp ,
38+ inner : imp :: DoubleSpawnInfo ,
1939}
2040
2141impl 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) ]
49102mod imp {
103+ use nix:: sys:: signal:: { SigSet , Signal } ;
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) ) ]
88178mod 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}
0 commit comments