Skip to content

Commit 08886a9

Browse files
authored
Add a way to way to wait for process groups. (#846)
When the `Pid` type changed to only accept positive pid values, the `waitpid` function became unable to wait for process groups, because that involves negative pids. Fully fixing this will require a semver bump, but for now, we can add a new `waitpgid` function to add support for waiting for a process group. Also add a `Pgid` arm to `WaitId`, to support waiting for process groups from `waitid` as well.
1 parent fe5e4ea commit 08886a9

6 files changed

Lines changed: 162 additions & 12 deletions

File tree

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ To keep compile times low, most features in rustix's API are behind cargo
99
features. A special feature, `all-apis` enables all APIs, which is useful
1010
for testing.
1111

12-
```
12+
```console
1313
cargo test --features=all-apis
1414
```
1515

1616
And, rustix has two backends, linux_raw and libc, and only one is used in
1717
any given build. To test with the libc backend explicitly, additionally
1818
enable the `use-libc` feature:
1919

20-
```
20+
```console
2121
cargo test --features=all-apis,use-libc
2222
```
2323

src/backend/libc/process/syscalls.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,12 @@ pub(crate) fn waitpid(
386386
_waitpid(Pid::as_raw(pid), waitopts)
387387
}
388388

389+
#[cfg(not(any(target_os = "espidf", target_os = "wasi")))]
390+
#[inline]
391+
pub(crate) fn waitpgid(pgid: Pid, waitopts: WaitOptions) -> io::Result<Option<(Pid, WaitStatus)>> {
392+
_waitpid(-pgid.as_raw_nonzero().get(), waitopts)
393+
}
394+
389395
#[cfg(not(any(target_os = "espidf", target_os = "wasi")))]
390396
#[inline]
391397
pub(crate) fn _waitpid(
@@ -411,6 +417,7 @@ pub(crate) fn waitid(id: WaitId<'_>, options: WaitidOptions) -> io::Result<Optio
411417
match id {
412418
WaitId::All => _waitid_all(options),
413419
WaitId::Pid(pid) => _waitid_pid(pid, options),
420+
WaitId::Pgid(pgid) => _waitid_pgid(pgid, options),
414421
#[cfg(target_os = "linux")]
415422
WaitId::PidFd(fd) => _waitid_pidfd(fd, options),
416423
#[cfg(not(target_os = "linux"))]
@@ -464,6 +471,29 @@ fn _waitid_pid(pid: Pid, options: WaitidOptions) -> io::Result<Option<WaitidStat
464471
Ok(unsafe { cvt_waitid_status(status) })
465472
}
466473

474+
#[cfg(not(any(
475+
target_os = "espidf",
476+
target_os = "redox",
477+
target_os = "openbsd",
478+
target_os = "wasi"
479+
)))]
480+
#[inline]
481+
fn _waitid_pgid(pgid: Option<Pid>, options: WaitidOptions) -> io::Result<Option<WaitidStatus>> {
482+
// `waitid` can return successfully without initializing the struct (no
483+
// children found when using `WNOHANG`)
484+
let mut status = MaybeUninit::<c::siginfo_t>::zeroed();
485+
unsafe {
486+
ret(c::waitid(
487+
c::P_PGID,
488+
Pid::as_raw(pgid) as _,
489+
status.as_mut_ptr(),
490+
options.bits() as _,
491+
))?
492+
};
493+
494+
Ok(unsafe { cvt_waitid_status(status) })
495+
}
496+
467497
#[cfg(target_os = "linux")]
468498
#[inline]
469499
fn _waitid_pidfd(fd: BorrowedFd<'_>, options: WaitidOptions) -> io::Result<Option<WaitidStatus>> {

src/backend/linux_raw/c.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ pub(crate) const EXIT_SIGNALED_SIGABRT: c_int = 128 + linux_raw_sys::general::SI
9090
pub(crate) use linux_raw_sys::{
9191
general::{
9292
CLD_CONTINUED, CLD_DUMPED, CLD_EXITED, CLD_KILLED, CLD_STOPPED, CLD_TRAPPED,
93-
O_NONBLOCK as PIDFD_NONBLOCK, P_ALL, P_PID, P_PIDFD,
93+
O_NONBLOCK as PIDFD_NONBLOCK, P_ALL, P_PGID, P_PID, P_PIDFD,
9494
},
9595
ioctl::TIOCSCTTY,
9696
};

src/backend/linux_raw/process/syscalls.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,11 @@ pub(crate) fn waitpid(
435435
_waitpid(Pid::as_raw(pid), waitopts)
436436
}
437437

438+
#[inline]
439+
pub(crate) fn waitpgid(pgid: Pid, waitopts: WaitOptions) -> io::Result<Option<(Pid, WaitStatus)>> {
440+
_waitpid(-pgid.as_raw_nonzero().get(), waitopts)
441+
}
442+
438443
#[inline]
439444
pub(crate) fn _waitpid(
440445
pid: RawPid,
@@ -459,6 +464,7 @@ pub(crate) fn waitid(id: WaitId<'_>, options: WaitidOptions) -> io::Result<Optio
459464
match id {
460465
WaitId::All => _waitid_all(options),
461466
WaitId::Pid(pid) => _waitid_pid(pid, options),
467+
WaitId::Pgid(pid) => _waitid_pgid(pid, options),
462468
WaitId::PidFd(fd) => _waitid_pidfd(fd, options),
463469
}
464470
}
@@ -501,6 +507,25 @@ fn _waitid_pid(pid: Pid, options: WaitidOptions) -> io::Result<Option<WaitidStat
501507
Ok(unsafe { cvt_waitid_status(status) })
502508
}
503509

510+
#[inline]
511+
fn _waitid_pgid(pgid: Option<Pid>, options: WaitidOptions) -> io::Result<Option<WaitidStatus>> {
512+
// `waitid` can return successfully without initializing the struct (no
513+
// children found when using `WNOHANG`)
514+
let mut status = MaybeUninit::<c::siginfo_t>::zeroed();
515+
unsafe {
516+
ret(syscall!(
517+
__NR_waitid,
518+
c_uint(c::P_PGID),
519+
c_int(Pid::as_raw(pgid)),
520+
by_mut(&mut status),
521+
c_int(options.bits() as _),
522+
zero()
523+
))?
524+
};
525+
526+
Ok(unsafe { cvt_waitid_status(status) })
527+
}
528+
504529
#[inline]
505530
fn _waitid_pidfd(fd: BorrowedFd<'_>, options: WaitidOptions) -> io::Result<Option<WaitidStatus>> {
506531
// `waitid` can return successfully without initializing the struct (no

src/process/wait.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,40 +254,50 @@ impl WaitidStatus {
254254
#[non_exhaustive]
255255
pub enum WaitId<'a> {
256256
/// Wait on all processes.
257+
#[doc(alias = "P_ALL")]
257258
All,
258259

259260
/// Wait for a specific process ID.
261+
#[doc(alias = "P_PID")]
260262
Pid(Pid),
261263

264+
/// Wait for a specific process group ID, or the calling process' group ID.
265+
#[doc(alias = "P_PGID")]
266+
Pgid(Option<Pid>),
267+
262268
/// Wait for a specific process file descriptor.
263269
#[cfg(target_os = "linux")]
270+
#[doc(alias = "P_PIDFD")]
264271
PidFd(BorrowedFd<'a>),
265272

266273
/// Eat the lifetime for non-Linux platforms.
267274
#[doc(hidden)]
268275
#[cfg(not(target_os = "linux"))]
269276
__EatLifetime(core::marker::PhantomData<&'a ()>),
270-
// TODO(notgull): Once this crate has the concept of PGIDs, add a WaitId::Pgid
271277
}
272278

273279
/// `waitpid(pid, waitopts)`—Wait for a specific process to change state.
274280
///
275281
/// If the pid is `None`, the call will wait for any child process whose
276282
/// process group id matches that of the calling process.
277283
///
278-
/// If the pid is equal to `RawPid::MAX`, the call will wait for any child
279-
/// process.
280-
///
281-
/// Otherwise if the `wrapping_neg` of pid is less than pid, the call will wait
282-
/// for any child process with a group ID equal to the `wrapping_neg` of `pid`.
283-
///
284284
/// Otherwise, the call will wait for the child process with the given pid.
285285
///
286286
/// On Success, returns the status of the selected process.
287287
///
288288
/// If `NOHANG` was specified in the options, and the selected child process
289289
/// didn't change state, returns `None`.
290290
///
291+
/// # Bugs
292+
///
293+
/// This function does not currently support waiting for given process group
294+
/// (the < 0 case of `waitpid`); to do that, currently the [`waitpgid`] or
295+
/// [`waitid`] function must be used.
296+
///
297+
/// This function does not currently support waiting for any process (the
298+
/// `-1` case of `waitpid`); to do that, currently the [`wait`] function must
299+
/// be used.
300+
///
291301
/// # References
292302
/// - [POSIX]
293303
/// - [Linux]
@@ -300,6 +310,28 @@ pub fn waitpid(pid: Option<Pid>, waitopts: WaitOptions) -> io::Result<Option<Wai
300310
Ok(backend::process::syscalls::waitpid(pid, waitopts)?.map(|(_, status)| status))
301311
}
302312

313+
/// `waitpid(-pgid, waitopts)`—Wait for a process in a specific process group
314+
/// to change state.
315+
///
316+
/// The call will wait for any child process with the given pgid.
317+
///
318+
/// On Success, returns the status of the selected process.
319+
///
320+
/// If `NOHANG` was specified in the options, and no selected child process
321+
/// changed state, returns `None`.
322+
///
323+
/// # References
324+
/// - [POSIX]
325+
/// - [Linux]
326+
///
327+
/// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/wait.html
328+
/// [Linux]: https://man7.org/linux/man-pages/man2/waitpid.2.html
329+
#[cfg(not(target_os = "wasi"))]
330+
#[inline]
331+
pub fn waitpgid(pgid: Pid, waitopts: WaitOptions) -> io::Result<Option<WaitStatus>> {
332+
Ok(backend::process::syscalls::waitpgid(pgid, waitopts)?.map(|(_, status)| status))
333+
}
334+
303335
/// `wait(waitopts)`—Wait for any of the children of calling process to
304336
/// change state.
305337
///

tests/process/wait.rs

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,23 @@ use std::process::{Command, Stdio};
1010

1111
#[test]
1212
#[serial]
13-
fn test_waitpid() {
13+
fn test_waitpid_none() {
14+
let child = Command::new("yes")
15+
.stdout(Stdio::null())
16+
.stderr(Stdio::null())
17+
.spawn()
18+
.expect("failed to execute child");
19+
unsafe { kill(child.id() as _, SIGSTOP) };
20+
21+
let status = process::waitpid(None, process::WaitOptions::UNTRACED)
22+
.expect("failed to wait")
23+
.unwrap();
24+
assert!(status.stopped());
25+
}
26+
27+
#[test]
28+
#[serial]
29+
fn test_waitpid_some() {
1430
let child = Command::new("yes")
1531
.stdout(Stdio::null())
1632
.stderr(Stdio::null())
@@ -25,6 +41,23 @@ fn test_waitpid() {
2541
assert!(status.stopped());
2642
}
2743

44+
#[test]
45+
#[serial]
46+
fn test_waitpgid() {
47+
let child = Command::new("yes")
48+
.stdout(Stdio::null())
49+
.stderr(Stdio::null())
50+
.spawn()
51+
.expect("failed to execute child");
52+
unsafe { kill(child.id() as _, SIGSTOP) };
53+
54+
let pgid = process::getpgrp();
55+
let status = process::waitpgid(pgid, process::WaitOptions::UNTRACED)
56+
.expect("failed to wait")
57+
.unwrap();
58+
assert!(status.stopped());
59+
}
60+
2861
#[cfg(not(any(
2962
target_os = "wasi",
3063
target_os = "emscripten",
@@ -39,9 +72,13 @@ fn test_waitid() {
3972
.stderr(Stdio::null())
4073
.spawn()
4174
.expect("failed to execute child");
75+
let pid = process::Pid::from_child(&child);
76+
let pgid = process::getpgid(Some(pid)).unwrap();
77+
78+
// Test waiting for the process by pid.
79+
4280
unsafe { kill(child.id() as _, SIGSTOP) };
4381

44-
let pid = process::Pid::from_child(&child);
4582
let status = process::waitid(process::WaitId::Pid(pid), process::WaitidOptions::STOPPED)
4683
.expect("failed to wait")
4784
.unwrap();
@@ -58,6 +95,32 @@ fn test_waitid() {
5895

5996
assert!(status.continued());
6097

98+
// Now do the same thing with the pgid.
99+
100+
unsafe { kill(child.id() as _, SIGSTOP) };
101+
102+
let status = process::waitid(
103+
process::WaitId::Pgid(Some(pgid)),
104+
process::WaitidOptions::STOPPED,
105+
)
106+
.expect("failed to wait")
107+
.unwrap();
108+
109+
assert!(status.stopped());
110+
#[cfg(not(any(target_os = "fuchsia", target_os = "netbsd")))]
111+
assert_eq!(status.stopping_signal(), Some(SIGSTOP as _));
112+
113+
unsafe { kill(child.id() as _, SIGCONT) };
114+
115+
let status = process::waitid(
116+
process::WaitId::Pgid(Some(pgid)),
117+
process::WaitidOptions::CONTINUED,
118+
)
119+
.expect("failed to wait")
120+
.unwrap();
121+
122+
assert!(status.continued());
123+
61124
let status = process::waitid(
62125
process::WaitId::All,
63126
process::WaitidOptions::EXITED | process::WaitidOptions::NOHANG,

0 commit comments

Comments
 (0)