5
5
//!
6
6
//! Nextest has experimental support on Unix for spawning test processes twice, to enable better
7
7
//! 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.
8
29
9
- use self :: imp:: DoubleSpawnInfoImp ;
10
30
use std:: path:: Path ;
11
31
12
32
/// Information about double-spawning processes. This determines whether a process will be
@@ -15,7 +35,7 @@ use std::path::Path;
15
35
/// This is used by the main nextest process.
16
36
#[ derive( Clone , Debug ) ]
17
37
pub struct DoubleSpawnInfo {
18
- inner : DoubleSpawnInfoImp ,
38
+ inner : imp :: DoubleSpawnInfo ,
19
39
}
20
40
21
41
impl DoubleSpawnInfo {
@@ -27,39 +47,75 @@ impl DoubleSpawnInfo {
27
47
/// This is super experimental, and should be used with caution.
28
48
pub fn enabled ( ) -> Self {
29
49
Self {
30
- inner : DoubleSpawnInfoImp :: enabled ( ) ,
50
+ inner : imp :: DoubleSpawnInfo :: enabled ( ) ,
31
51
}
32
52
}
33
53
34
54
/// This returns a `DoubleSpawnInfo` which disables double-spawning.
35
55
pub fn disabled ( ) -> Self {
36
56
Self {
37
- inner : DoubleSpawnInfoImp :: disabled ( ) ,
57
+ inner : imp :: DoubleSpawnInfo :: disabled ( ) ,
38
58
}
39
59
}
60
+
40
61
/// Returns the current executable, if one is available.
41
62
///
42
63
/// If `None`, double-spawning is not used.
43
64
pub fn current_exe ( & self ) -> Option < & Path > {
44
65
self . inner . current_exe ( )
45
66
}
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 ( )
46
99
}
47
100
48
101
#[ cfg( unix) ]
49
102
mod imp {
103
+ use nix:: sys:: signal:: { SigSet , Signal } ;
104
+
50
105
use super :: * ;
51
106
use std:: path:: PathBuf ;
52
107
53
108
#[ derive( Clone , Debug ) ]
54
- pub ( super ) struct DoubleSpawnInfoImp {
109
+ pub ( super ) struct DoubleSpawnInfo {
55
110
current_exe : Option < PathBuf > ,
56
111
}
57
112
58
- impl DoubleSpawnInfoImp {
113
+ impl DoubleSpawnInfo {
59
114
#[ inline]
60
115
pub ( super ) fn enabled ( ) -> Self {
61
116
// Attempt to obtain the current exe, and warn if it couldn't be found.
62
117
// TODO: maybe add an option to fail?
118
+ // TODO: Always use /proc/self/exe directly on Linux, just make sure it's always accessible
63
119
let current_exe = std:: env:: current_exe ( ) . map_or_else (
64
120
|error| {
65
121
log:: warn!(
@@ -82,16 +138,50 @@ mod imp {
82
138
self . current_exe . as_deref ( )
83
139
}
84
140
}
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
+ }
85
175
}
86
176
87
177
#[ cfg( not( unix) ) ]
88
178
mod imp {
89
179
use super :: * ;
90
180
91
181
#[ derive( Clone , Debug ) ]
92
- pub ( super ) struct DoubleSpawnInfoImp { }
182
+ pub ( super ) struct DoubleSpawnInfo { }
93
183
94
- impl DoubleSpawnInfoImp {
184
+ impl DoubleSpawnInfo {
95
185
#[ inline]
96
186
pub ( super ) fn enabled ( ) -> Self {
97
187
Self { }
@@ -107,4 +197,17 @@ mod imp {
107
197
None
108
198
}
109
199
}
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 ( ) { }
110
213
}
0 commit comments