Skip to content

Commit cd87367

Browse files
committed
Experiment with fixing wait_with_pipe()
1 parent f0c102c commit cd87367

File tree

2 files changed

+86
-3
lines changed

2 files changed

+86
-3
lines changed

src/child.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,17 @@ impl FunChildren {
115115
}
116116
}
117117

118-
/// Waits for the children processes to exit completely, pipe content will be processed by
119-
/// provided function.
118+
/// Pipes stdout from the last child in the pipeline to the given function, which runs in
119+
/// **the current thread**, then waits for all of the children to exit.
120+
///
121+
/// <div class=warning>
122+
///
123+
/// # Bugs
124+
///
125+
/// The exit status of the last child is **ignored**. If the function returns early, without
126+
/// reading from stdout until the last child exits, then the last child may be killed instead
127+
/// of being waited for. To avoid these limitations, use [`Self::wait_with_stdout_thread`].
128+
/// </div>
120129
pub fn wait_with_pipe(&mut self, f: &mut dyn FnMut(Box<dyn Read>)) -> CmdResult {
121130
let child = self.children.pop().unwrap();
122131
let stderr_thread =
@@ -143,6 +152,22 @@ impl FunChildren {
143152
CmdChildren::wait_children(&mut self.children)
144153
}
145154

155+
/// Pipes stdout from the last child in the pipeline to the given function, which runs in
156+
/// **a new thread**, then waits for all of the children to exit.
157+
pub fn wait_with_pipe_thread(
158+
&mut self,
159+
f: impl FnOnce(Box<dyn Read + Send>) + Send + 'static,
160+
) -> CmdResult {
161+
if let Some(stdout) = self.children.last_mut().unwrap().stdout.take() {
162+
let thread = std::thread::spawn(|| f(Box::new(stdout)));
163+
let wait_res = self.wait_with_output().map(|_| ());
164+
thread.join().expect("stdout thread panicked");
165+
return wait_res;
166+
}
167+
168+
Ok(())
169+
}
170+
146171
/// Returns the OS-assigned process identifiers associated with these children processes.
147172
pub fn pids(&self) -> Vec<u32> {
148173
self.children.iter().filter_map(|x| x.pid()).collect()

tests/test_macros.rs

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ fn test_tls_set() {
134134
}
135135

136136
#[test]
137-
fn test_pipe() {
137+
fn test_pipe() -> CmdResult {
138138
assert!(run_cmd!(echo "xx").is_ok());
139139
assert_eq!(run_fun!(echo "xx").unwrap(), "xx");
140140
assert!(run_cmd!(echo xx | wc).is_ok());
@@ -158,6 +158,64 @@ fn test_pipe() {
158158
set_pipefail(false);
159159
assert!(run_fun!(false | true).is_ok());
160160
set_pipefail(true);
161+
162+
// test that illustrates the bugs in wait_with_pipe()
163+
// FIXME: make set_pipefail() thread safe, then move this to a separate test function
164+
assert!(spawn_with_output!(false)?.wait_with_all().0.is_err());
165+
assert!(spawn_with_output!(false)?.wait_with_output().is_err());
166+
assert!(spawn_with_output!(false)?
167+
.wait_with_raw_output(&mut vec![])
168+
.is_err());
169+
170+
// wait_with_pipe() can’t check the exit status of the last child
171+
assert!(spawn_with_output!(false)?
172+
.wait_with_pipe(&mut |_stdout| {})
173+
.is_ok());
174+
175+
// wait_with_pipe() kills the last child when the provided function returns
176+
assert!(spawn_with_output!(sh -c "while :; do :; done")?
177+
.wait_with_pipe(&mut |_stdout| {})
178+
.is_ok());
179+
180+
// wait_with_pipe_thread() checks the exit status of the last child, even if pipefail is disabled
181+
set_pipefail(false);
182+
assert!(spawn_with_output!(true | false)?
183+
.wait_with_pipe_thread(|_stdout| {})
184+
.is_err());
185+
assert!(spawn_with_output!(true | true)?
186+
.wait_with_pipe_thread(|_stdout| {})
187+
.is_ok());
188+
assert!(spawn_with_output!(false)?
189+
.wait_with_pipe_thread(|_stdout| {})
190+
.is_err());
191+
assert!(spawn_with_output!(true)?
192+
.wait_with_pipe_thread(|_stdout| {})
193+
.is_ok());
194+
set_pipefail(true);
195+
// wait_with_pipe_thread() checks the exit status of the other children, unless pipefail is disabled
196+
set_pipefail(false);
197+
assert!(spawn_with_output!(false | true)?
198+
.wait_with_pipe_thread(|_stdout| {})
199+
.is_ok());
200+
set_pipefail(true);
201+
assert!(spawn_with_output!(false | true)?
202+
.wait_with_pipe_thread(|_stdout| {})
203+
.is_err());
204+
assert!(spawn_with_output!(true | true)?
205+
.wait_with_pipe_thread(|_stdout| {})
206+
.is_ok());
207+
// wait_with_pipe_thread() handles `ignore`
208+
assert!(spawn_with_output!(ignore false | true)?
209+
.wait_with_pipe_thread(|_stdout| {})
210+
.is_ok());
211+
assert!(spawn_with_output!(ignore true | false)?
212+
.wait_with_pipe_thread(|_stdout| {})
213+
.is_ok());
214+
assert!(spawn_with_output!(ignore false)?
215+
.wait_with_pipe_thread(|_stdout| {})
216+
.is_ok());
217+
218+
Ok(())
161219
}
162220

163221
#[test]

0 commit comments

Comments
 (0)