Skip to content

Commit 0e190b9

Browse files
committed
native: Use WNOHANG before signaling
It turns out that on linux, and possibly other platforms, child processes will continue to accept signals until they have been *reaped*. This means that once the child has exited, it will succeed to receive signals until waitpid() has been invoked on it. This is unfortunate behavior, and differs from what is seen on OSX and windows. This commit changes the behavior of Process::signal() to be the same across platforms, and updates the documentation of Process::kill() to note that when signaling a foreign process it may accept signals until reaped. Implementation-wise, this invokes waitpid() with WNOHANG before each signal to the child to ensure that if the child has exited that we will reap it. Other possibilities include installing a SIGCHLD signal handler, but at this time I believe that that's too complicated. Closes #13124
1 parent b8601a3 commit 0e190b9

File tree

4 files changed

+116
-45
lines changed

4 files changed

+116
-45
lines changed

src/libnative/io/process.rs

+58-23
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ impl rtio::RtioProcess for Process {
134134
}
135135

136136
fn kill(&mut self, signum: int) -> Result<(), io::IoError> {
137+
// On linux (and possibly other unices), a process that has exited will
138+
// continue to accept signals because it is "defunct". The delivery of
139+
// signals will only fail once the child has been reaped. For this
140+
// reason, if the process hasn't exited yet, then we attempt to collect
141+
// their status with WNOHANG.
142+
if self.exit_code.is_none() {
143+
match waitpid_nowait(self.pid) {
144+
Some(code) => { self.exit_code = Some(code); }
145+
None => {}
146+
}
147+
}
148+
137149
// if the process has finished, and therefore had waitpid called,
138150
// and we kill it, then on unix we might ending up killing a
139151
// newer process that happens to have the same (re-used) id
@@ -662,6 +674,31 @@ fn free_handle(_handle: *()) {
662674
// unix has no process handle object, just a pid
663675
}
664676

677+
#[cfg(unix)]
678+
fn translate_status(status: c_int) -> p::ProcessExit {
679+
#[cfg(target_os = "linux")]
680+
#[cfg(target_os = "android")]
681+
mod imp {
682+
pub fn WIFEXITED(status: i32) -> bool { (status & 0xff) == 0 }
683+
pub fn WEXITSTATUS(status: i32) -> i32 { (status >> 8) & 0xff }
684+
pub fn WTERMSIG(status: i32) -> i32 { status & 0x7f }
685+
}
686+
687+
#[cfg(target_os = "macos")]
688+
#[cfg(target_os = "freebsd")]
689+
mod imp {
690+
pub fn WIFEXITED(status: i32) -> bool { (status & 0x7f) == 0 }
691+
pub fn WEXITSTATUS(status: i32) -> i32 { status >> 8 }
692+
pub fn WTERMSIG(status: i32) -> i32 { status & 0o177 }
693+
}
694+
695+
if imp::WIFEXITED(status) {
696+
p::ExitStatus(imp::WEXITSTATUS(status) as int)
697+
} else {
698+
p::ExitSignal(imp::WTERMSIG(status) as int)
699+
}
700+
}
701+
665702
/**
666703
* Waits for a process to exit and returns the exit code, failing
667704
* if there is no process with the specified id.
@@ -723,33 +760,31 @@ fn waitpid(pid: pid_t) -> p::ProcessExit {
723760
#[cfg(unix)]
724761
fn waitpid_os(pid: pid_t) -> p::ProcessExit {
725762
use std::libc::funcs::posix01::wait;
726-
727-
#[cfg(target_os = "linux")]
728-
#[cfg(target_os = "android")]
729-
mod imp {
730-
pub fn WIFEXITED(status: i32) -> bool { (status & 0xff) == 0 }
731-
pub fn WEXITSTATUS(status: i32) -> i32 { (status >> 8) & 0xff }
732-
pub fn WTERMSIG(status: i32) -> i32 { status & 0x7f }
763+
let mut status = 0 as c_int;
764+
match retry(|| unsafe { wait::waitpid(pid, &mut status, 0) }) {
765+
-1 => fail!("unknown waitpid error: {}", super::last_error()),
766+
_ => translate_status(status),
733767
}
768+
}
769+
}
734770

735-
#[cfg(target_os = "macos")]
736-
#[cfg(target_os = "freebsd")]
737-
mod imp {
738-
pub fn WIFEXITED(status: i32) -> bool { (status & 0x7f) == 0 }
739-
pub fn WEXITSTATUS(status: i32) -> i32 { status >> 8 }
740-
pub fn WTERMSIG(status: i32) -> i32 { status & 0o177 }
741-
}
771+
fn waitpid_nowait(pid: pid_t) -> Option<p::ProcessExit> {
772+
return waitpid_os(pid);
742773

774+
// This code path isn't necessary on windows
775+
#[cfg(windows)]
776+
fn waitpid_os(_pid: pid_t) -> Option<p::ProcessExit> { None }
777+
778+
#[cfg(unix)]
779+
fn waitpid_os(pid: pid_t) -> Option<p::ProcessExit> {
780+
use std::libc::funcs::posix01::wait;
743781
let mut status = 0 as c_int;
744-
match retry(|| unsafe { wait::waitpid(pid, &mut status, 0) }) {
745-
-1 => fail!("unknown waitpid error: {}", super::last_error()),
746-
_ => {
747-
if imp::WIFEXITED(status) {
748-
p::ExitStatus(imp::WEXITSTATUS(status) as int)
749-
} else {
750-
p::ExitSignal(imp::WTERMSIG(status) as int)
751-
}
752-
}
782+
match retry(|| unsafe {
783+
wait::waitpid(pid, &mut status, libc::WNOHANG)
784+
}) {
785+
n if n == pid => Some(translate_status(status)),
786+
0 => None,
787+
n => fail!("unknown waitpid error `{}`: {}", n, super::last_error()),
753788
}
754789
}
755790
}

src/libstd/io/process.rs

+26-3
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,9 @@ impl Process {
331331
/// signals (SIGTERM/SIGKILL/SIGINT) are translated to `TerminateProcess`.
332332
///
333333
/// Additionally, a signal number of 0 can check for existence of the target
334-
/// process.
334+
/// process. Note, though, that on some platforms signals will continue to
335+
/// be successfully delivered if the child has exited, but not yet been
336+
/// reaped.
335337
pub fn kill(id: libc::pid_t, signal: int) -> IoResult<()> {
336338
LocalIo::maybe_raise(|io| io.kill(id, signal))
337339
}
@@ -342,8 +344,16 @@ impl Process {
342344
/// Sends the specified signal to the child process, returning whether the
343345
/// signal could be delivered or not.
344346
///
345-
/// Note that this is purely a wrapper around libuv's `uv_process_kill`
346-
/// function.
347+
/// Note that signal 0 is interpreted as a poll to check whether the child
348+
/// process is still alive or not. If an error is returned, then the child
349+
/// process has exited.
350+
///
351+
/// On some unix platforms signals will continue to be received after a
352+
/// child has exited but not yet been reaped. In order to report the status
353+
/// of signal delivery correctly, unix implementations may invoke
354+
/// `waitpid()` with `WNOHANG` in order to reap the child as necessary.
355+
///
356+
/// # Errors
347357
///
348358
/// If the signal delivery fails, the corresponding error is returned.
349359
pub fn signal(&mut self, signal: int) -> IoResult<()> {
@@ -833,4 +843,17 @@ mod tests {
833843
p.signal_kill().unwrap();
834844
assert!(!p.wait().success());
835845
})
846+
847+
iotest!(fn test_zero() {
848+
let mut p = sleeper();
849+
p.signal_kill().unwrap();
850+
for _ in range(0, 20) {
851+
if p.signal(0).is_err() {
852+
assert!(!p.wait().success());
853+
return
854+
}
855+
timer::sleep(100);
856+
}
857+
fail!("never saw the child go away");
858+
})
836859
}

src/libstd/libc.rs

+6
Original file line numberDiff line numberDiff line change
@@ -2356,6 +2356,8 @@ pub mod consts {
23562356

23572357
pub static CLOCK_REALTIME: c_int = 0;
23582358
pub static CLOCK_MONOTONIC: c_int = 1;
2359+
2360+
pub static WNOHANG: c_int = 1;
23592361
}
23602362
pub mod posix08 {
23612363
}
@@ -2802,6 +2804,8 @@ pub mod consts {
28022804

28032805
pub static CLOCK_REALTIME: c_int = 0;
28042806
pub static CLOCK_MONOTONIC: c_int = 4;
2807+
2808+
pub static WNOHANG: c_int = 1;
28052809
}
28062810
pub mod posix08 {
28072811
}
@@ -3187,6 +3191,8 @@ pub mod consts {
31873191
pub static PTHREAD_CREATE_JOINABLE: c_int = 1;
31883192
pub static PTHREAD_CREATE_DETACHED: c_int = 2;
31893193
pub static PTHREAD_STACK_MIN: size_t = 8192;
3194+
3195+
pub static WNOHANG: c_int = 1;
31903196
}
31913197
pub mod posix08 {
31923198
}

src/test/run-pass/core-run-destroy.rs

+26-19
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ extern crate native;
2222
extern crate green;
2323
extern crate rustuv;
2424

25+
use std::io::Process;
26+
27+
macro_rules! succeed( ($e:expr) => (
28+
match $e { Ok(..) => {}, Err(e) => fail!("failure: {}", e) }
29+
) )
30+
2531
macro_rules! iotest (
2632
{ fn $name:ident() $b:block $($a:attr)* } => (
2733
mod $name {
@@ -53,28 +59,29 @@ fn start(argc: int, argv: **u8) -> int {
5359
}
5460

5561
iotest!(fn test_destroy_once() {
56-
#[cfg(not(target_os="android"))]
57-
static mut PROG: &'static str = "echo";
58-
59-
#[cfg(target_os="android")]
60-
static mut PROG: &'static str = "ls"; // android don't have echo binary
61-
62-
let mut p = unsafe {Process::new(PROG, []).unwrap()};
63-
p.signal_exit().unwrap(); // this shouldn't crash (and nor should the destructor)
62+
let mut p = sleeper();
63+
match p.signal_exit() {
64+
Ok(()) => {}
65+
Err(e) => fail!("error: {}", e),
66+
}
6467
})
6568

69+
#[cfg(unix)]
70+
pub fn sleeper() -> Process {
71+
Process::new("sleep", [~"1000"]).unwrap()
72+
}
73+
#[cfg(windows)]
74+
pub fn sleeper() -> Process {
75+
// There's a `timeout` command on windows, but it doesn't like having
76+
// its output piped, so instead just ping ourselves a few times with
77+
// gaps inbetweeen so we're sure this process is alive for awhile
78+
Process::new("ping", [~"127.0.0.1", ~"-n", ~"1000"]).unwrap()
79+
}
80+
6681
iotest!(fn test_destroy_twice() {
67-
#[cfg(not(target_os="android"))]
68-
static mut PROG: &'static str = "echo";
69-
#[cfg(target_os="android")]
70-
static mut PROG: &'static str = "ls"; // android don't have echo binary
71-
72-
let mut p = match unsafe{Process::new(PROG, [])} {
73-
Ok(p) => p,
74-
Err(e) => fail!("wut: {}", e),
75-
};
76-
p.signal_exit().unwrap(); // this shouldnt crash...
77-
p.signal_exit().unwrap(); // ...and nor should this (and nor should the destructor)
82+
let mut p = sleeper();
83+
succeed!(p.signal_exit()); // this shouldnt crash...
84+
let _ = p.signal_exit(); // ...and nor should this (and nor should the destructor)
7885
})
7986

8087
pub fn test_destroy_actually_kills(force: bool) {

0 commit comments

Comments
 (0)