Skip to content

Commit 5431e94

Browse files
authored
uucore: process: fix exit status processing (#1743)
* uucore: process: fix exit status processing * tests: timeout: check whether subcommand exit codes are returned
1 parent 7e5d9ee commit 5431e94

File tree

3 files changed

+53
-44
lines changed

3 files changed

+53
-44
lines changed

src/uucore/src/lib/features/process.rs

Lines changed: 26 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
// spell-checker:ignore (vars) cvar exitstatus
1010
// spell-checker:ignore (sys/unix) WIFSIGNALED
1111

12-
use libc::{c_int, gid_t, pid_t, uid_t};
12+
use libc::{gid_t, pid_t, uid_t};
1313
use std::fmt;
1414
use std::io;
1515
use std::process::Child;
16-
use std::sync::{Arc, Condvar, Mutex};
16+
use std::process::ExitStatus as StdExitStatus;
1717
use std::thread;
1818
use std::time::{Duration, Instant};
1919

@@ -41,13 +41,18 @@ pub enum ExitStatus {
4141
}
4242

4343
impl ExitStatus {
44-
fn from_status(status: c_int) -> ExitStatus {
45-
if status & 0x7F != 0 {
46-
// WIFSIGNALED(status) == terminating by/with unhandled signal
47-
ExitStatus::Signal(status & 0x7F)
48-
} else {
49-
ExitStatus::Code(status & 0xFF00 >> 8)
44+
fn from_std_status(status: StdExitStatus) -> Self {
45+
#[cfg(unix)]
46+
{
47+
use std::os::unix::process::ExitStatusExt;
48+
49+
if let Some(signal) = status.signal() {
50+
return ExitStatus::Signal(signal);
51+
}
5052
}
53+
54+
// NOTE: this should never fail as we check if the program exited through a signal above
55+
ExitStatus::Code(status.code().unwrap())
5156
}
5257

5358
pub fn success(&self) -> bool {
@@ -100,47 +105,25 @@ impl ChildExt for Child {
100105
}
101106

102107
fn wait_or_timeout(&mut self, timeout: Duration) -> io::Result<Option<ExitStatus>> {
103-
// The result will be written to that Option, protected by a Mutex
104-
// Then the Condvar will be signaled
105-
let state = Arc::new((
106-
Mutex::new(Option::None::<io::Result<ExitStatus>>),
107-
Condvar::new(),
108-
));
109-
110-
// Start the waiting thread
111-
let state_th = state.clone();
112-
let pid_th = self.id();
113-
thread::spawn(move || {
114-
let &(ref lock_th, ref cvar_th) = &*state_th;
115-
// Child::wait() would need a &mut to self, can't use that...
116-
// use waitpid() directly, with our own ExitStatus
117-
let mut status: c_int = 0;
118-
let r = unsafe { libc::waitpid(pid_th as i32, &mut status, 0) };
119-
// Fill the Option and notify on the Condvar
120-
let mut exitstatus_th = lock_th.lock().unwrap();
121-
if r != pid_th as c_int {
122-
*exitstatus_th = Some(Err(io::Error::last_os_error()));
123-
} else {
124-
let s = ExitStatus::from_status(status);
125-
*exitstatus_th = Some(Ok(s));
126-
}
127-
cvar_th.notify_one();
128-
});
108+
// .try_wait() doesn't drop stdin, so we do it manually
109+
drop(self.stdin.take());
129110

130-
// Main thread waits
131-
let &(ref lock, ref cvar) = &*state;
132-
let mut exitstatus = lock.lock().unwrap();
133-
// Condvar::wait_timeout_ms() can wake too soon, in this case wait again
134111
let start = Instant::now();
135112
loop {
136-
if let Some(exitstatus) = exitstatus.take() {
137-
return exitstatus.map(Some);
113+
if let Some(status) = self.try_wait()? {
114+
return Ok(Some(ExitStatus::from_std_status(status)));
138115
}
116+
139117
if start.elapsed() >= timeout {
140-
return Ok(None);
118+
break;
141119
}
142-
let cvar_timeout = timeout - start.elapsed();
143-
exitstatus = cvar.wait_timeout(exitstatus, cvar_timeout).unwrap().0;
120+
121+
// XXX: this is kinda gross, but it's cleaner than starting a thread just to wait
122+
// (which was the previous solution). We might want to use a different duration
123+
// here as well
124+
thread::sleep(Duration::from_millis(100));
144125
}
126+
127+
Ok(None)
145128
}
146129
}

tests/by-util/test_timeout.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,18 @@
1-
// ToDO: add tests
1+
use crate::common::util::*;
2+
3+
// FIXME: this depends on the system having true and false in PATH
4+
// the best solution is probably to generate some test binaries that we can call for any
5+
// utility that requires executing another program (kill, for instance)
6+
#[test]
7+
fn test_subcommand_retcode() {
8+
new_ucmd!()
9+
.arg("1")
10+
.arg("true")
11+
.succeeds();
12+
13+
new_ucmd!()
14+
.arg("1")
15+
.arg("false")
16+
.run()
17+
.status_code(1);
18+
}

tests/common/util.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ pub fn repeat_str(s: &str, n: u32) -> String {
6969
pub struct CmdResult {
7070
//tmpd is used for convenience functions for asserts against fixtures
7171
tmpd: Option<Rc<TempDir>>,
72+
/// exit status for command (if there is one)
73+
pub code: Option<i32>,
7274
/// zero-exit from running the Command?
7375
/// see [`success`]
7476
pub success: bool,
@@ -91,6 +93,12 @@ impl CmdResult {
9193
Box::new(self)
9294
}
9395

96+
/// asserts that the command's exit code is the same as the given one
97+
pub fn status_code(&self, code: i32) -> Box<&CmdResult> {
98+
assert!(self.code == Some(code));
99+
Box::new(self)
100+
}
101+
94102
/// asserts that the command resulted in empty (zero-length) stderr stream output
95103
/// generally, it's better to use stdout_only() instead,
96104
/// but you might find yourself using this function if
@@ -573,6 +581,7 @@ impl UCommand {
573581

574582
CmdResult {
575583
tmpd: self.tmpd.clone(),
584+
code: prog.status.code(),
576585
success: prog.status.success(),
577586
stdout: from_utf8(&prog.stdout).unwrap().to_string(),
578587
stderr: from_utf8(&prog.stderr).unwrap().to_string(),

0 commit comments

Comments
 (0)