Skip to content

Commit ff05adb

Browse files
committed
Fix hang when wait_with_borrowed_pipe() called with built-ins
1 parent 629c9ee commit ff05adb

File tree

3 files changed

+113
-24
lines changed

3 files changed

+113
-24
lines changed

src/child.rs

Lines changed: 101 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::{info, warn};
22
use crate::{process, CmdResult, FunResult};
33
use os_pipe::PipeReader;
4+
use std::any::Any;
5+
use std::fmt::Display;
46
use std::io::{BufRead, BufReader, Error, ErrorKind, Read, Result};
57
use std::process::{Child, ExitStatus};
68
use std::thread::JoinHandle;
@@ -170,19 +172,33 @@ impl FunChildren {
170172
let mut stdout: Box<dyn Read> = Box::new(stdout);
171173
f(&mut stdout);
172174
let mut buf = vec![0; 65536];
173-
let status = loop {
174-
if let CmdChildHandle::Proc(child) = &mut last_child.handle {
175-
if let Ok(Some(status)) = child.try_wait() {
176-
break status;
175+
let status: Box<dyn ChildOutcome> = loop {
176+
match last_child.handle {
177+
CmdChildHandle::Proc(ref mut child) => {
178+
if let Ok(Some(status)) = child.try_wait() {
179+
break Box::new(status);
180+
}
181+
}
182+
CmdChildHandle::Thread(ref mut join_handle) => {
183+
if let Some(handle) = join_handle.take() {
184+
if handle.is_finished() {
185+
break Box::new(ThreadJoinOutcome::from(handle.join()));
186+
} else {
187+
join_handle.replace(handle);
188+
}
189+
}
190+
}
191+
CmdChildHandle::SyncFn => {
192+
break Box::new(SyncFnOutcome);
177193
}
178194
}
179195
let _ = stdout.read(&mut buf);
180196
};
181197
if status.success() {
182198
Ok(())
183199
} else {
184-
Err(CmdChildHandle::status_to_io_error(
185-
status,
200+
Err(CmdChildHandle::outcome_to_io_error(
201+
&*status,
186202
&last_child.cmd,
187203
&last_child.file,
188204
last_child.line,
@@ -309,10 +325,70 @@ impl CmdChild {
309325

310326
pub(crate) enum CmdChildHandle {
311327
Proc(Child),
312-
Thread(JoinHandle<CmdResult>),
328+
Thread(Option<JoinHandle<CmdResult>>),
313329
SyncFn,
314330
}
315331

332+
#[derive(Debug)]
333+
enum ThreadJoinOutcome {
334+
Ok,
335+
Err(std::io::Error),
336+
Panic(Box<dyn Any + Send + 'static>),
337+
}
338+
impl From<std::thread::Result<CmdResult>> for ThreadJoinOutcome {
339+
fn from(result: std::thread::Result<CmdResult>) -> Self {
340+
match result {
341+
Ok(Ok(())) => Self::Ok,
342+
Ok(Err(error)) => Self::Err(error),
343+
Err(panic) => Self::Panic(panic),
344+
}
345+
}
346+
}
347+
impl Display for ThreadJoinOutcome {
348+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349+
match self {
350+
Self::Ok => write!(f, "Command thread succeeded"),
351+
Self::Err(error) => write!(f, "Command thread returned error: {error:?}"),
352+
Self::Panic(panic) => write!(f, "Command thread panicked: {panic:?}"),
353+
}
354+
}
355+
}
356+
#[derive(Debug)]
357+
struct SyncFnOutcome;
358+
impl Display for SyncFnOutcome {
359+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360+
write!(f, "Command finished")
361+
}
362+
}
363+
trait ChildOutcome: Display {
364+
fn success(&self) -> bool;
365+
fn code(&self) -> Option<i32>;
366+
}
367+
impl ChildOutcome for ExitStatus {
368+
fn success(&self) -> bool {
369+
self.success()
370+
}
371+
fn code(&self) -> Option<i32> {
372+
self.code()
373+
}
374+
}
375+
impl ChildOutcome for ThreadJoinOutcome {
376+
fn success(&self) -> bool {
377+
matches!(self, Self::Ok)
378+
}
379+
fn code(&self) -> Option<i32> {
380+
None
381+
}
382+
}
383+
impl ChildOutcome for SyncFnOutcome {
384+
fn success(&self) -> bool {
385+
true
386+
}
387+
fn code(&self) -> Option<i32> {
388+
None
389+
}
390+
}
391+
316392
impl CmdChildHandle {
317393
fn wait(self, cmd: &str, file: &str, line: u32) -> CmdResult {
318394
match self {
@@ -322,26 +398,28 @@ impl CmdChildHandle {
322398
Err(e) => return Err(process::new_cmd_io_error(&e, cmd, file, line)),
323399
Ok(status) => {
324400
if !status.success() {
325-
return Err(Self::status_to_io_error(status, cmd, file, line));
401+
return Err(Self::outcome_to_io_error(&status, cmd, file, line));
326402
}
327403
}
328404
}
329405
}
330-
CmdChildHandle::Thread(thread) => {
331-
let status = thread.join();
332-
match status {
333-
Ok(result) => {
334-
if let Err(e) = result {
335-
return Err(process::new_cmd_io_error(&e, cmd, file, line));
406+
CmdChildHandle::Thread(mut thread) => {
407+
if let Some(thread) = thread.take() {
408+
let status = thread.join();
409+
match status {
410+
Ok(result) => {
411+
if let Err(e) = result {
412+
return Err(process::new_cmd_io_error(&e, cmd, file, line));
413+
}
336414
}
337-
}
338-
Err(e) => {
339-
return Err(Error::new(
340-
ErrorKind::Other,
341-
format!(
415+
Err(e) => {
416+
return Err(Error::new(
417+
ErrorKind::Other,
418+
format!(
342419
"Running [{cmd}] thread joined with error: {e:?} at {file}:{line}"
343420
),
344-
))
421+
))
422+
}
345423
}
346424
}
347425
}
@@ -350,8 +428,8 @@ impl CmdChildHandle {
350428
Ok(())
351429
}
352430

353-
fn status_to_io_error(status: ExitStatus, cmd: &str, file: &str, line: u32) -> Error {
354-
if let Some(code) = status.code() {
431+
fn outcome_to_io_error(outcome: &dyn ChildOutcome, cmd: &str, file: &str, line: u32) -> Error {
432+
if let Some(code) = outcome.code() {
355433
Error::new(
356434
ErrorKind::Other,
357435
format!("Running [{cmd}] exited with error; status code: {code} at {file}:{line}"),
@@ -360,7 +438,7 @@ impl CmdChildHandle {
360438
Error::new(
361439
ErrorKind::Other,
362440
format!(
363-
"Running [{cmd}] exited with error; terminated by {status} at {file}:{line}"
441+
"Running [{cmd}] exited with error; terminated by {outcome} at {file}:{line}"
364442
),
365443
)
366444
}

src/process.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ impl Cmd {
460460
if pipe_out || with_output {
461461
let handle = thread::Builder::new().spawn(move || internal_cmd(&mut env))?;
462462
Ok(CmdChild::new(
463-
CmdChildHandle::Thread(handle),
463+
CmdChildHandle::Thread(Some(handle)),
464464
full_cmds,
465465
self.file,
466466
self.line,

tests/test_macros.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,17 @@ fn test_pipe() -> CmdResult {
217217
test_case!(true, true, ($macro $bang (ignore true | false)) $($after)*),
218218
test_case!(true, true, ($macro $bang (ignore false | true)) $($after)*),
219219
test_case!(true, true, ($macro $bang (ignore false | false)) $($after)*),
220+
// Built-ins should work too, without locking up.
221+
test_case!(true, true, ($macro $bang (echo)) $($after)*),
222+
test_case!(true, true, ($macro $bang (echo | true)) $($after)*),
223+
test_case!(false, false, ($macro $bang (echo | false)) $($after)*),
224+
test_case!(true, true, ($macro $bang (true | echo)) $($after)*),
225+
test_case!(false, true, ($macro $bang (false | echo)) $($after)*),
226+
test_case!(true, true, ($macro $bang (cd /)) $($after)*),
227+
test_case!(true, true, ($macro $bang (cd / | true)) $($after)*),
228+
test_case!(false, false, ($macro $bang (cd / | false)) $($after)*),
229+
test_case!(true, true, ($macro $bang (true | cd /)) $($after)*),
230+
test_case!(false, true, ($macro $bang (false | cd /)) $($after)*),
220231
]
221232
};
222233
}

0 commit comments

Comments
 (0)