Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"37c9f8fbca4917e4eb98c892cb7aae13b398953c81226f0ebf1f857ac5746ddf",".github/workflows/ci.yml":"690804f5572a8a2f55a158cb159acdef459ad277eb32f7c267128f6db85a65b6","Cargo.lock":"f1014a71bce925b2dc2578317e8a3aaff95761110aad91906f5fce183b0512eb","Cargo.toml":"6ec0702d10e2161d7028d0d2c0853d954de6778691dbbccea17152f7b69bced8","Cargo.toml.orig":"79bbbf7c7e7c9dfad8d8592223ae4d7fe90f769ab4e65b9d2081b5e5335c8901","README.md":"49edd50da3f5ddd123eb1b75aac4a5877d48632454421d88f18cc4d4e8b3e9ae","README.tpl":"2c85cfb49bf6670ecf60207c1dcbda014c17e9742feda23563629952f7051e34","src/lib.rs":"53ed0a679005bee58c10cacea027c5210850f6969773ea9d5cacc9220979e152"},"package":"47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"}
+6
View File
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "07b95e2fe38b18b376b0f635f2766bf2e641b80b"
},
"path_in_vcs": ""
}
+38
View File
@@ -0,0 +1,38 @@
name: tests
on:
push:
branches:
- "*"
# not on tags
pull_request:
env:
RUSTFLAGS: "-D warnings"
RUST_BACKTRACE: "1"
jobs:
cargo_tests:
name: ${{ matrix.os }} ${{ matrix.rust_channel }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: ["ubuntu-latest", "macOS-latest"]
rust_channel:
- stable
- beta
- nightly
- 1.74 # The real MSRV is 1.63 (inherited from libc) but tests require 1.74.
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.rust_channel }}
# macOS does "timer coalescing" by default, which can add ~100ms to wakeup
# times and cause tests to fail. To be honest, I'm not exactly sure where
# this happens (child wakeup vs SIGCHLD delivery vs poll return).
- name: disable timer coalescing on macOS
run: sudo sysctl -w kern.timer.coalescing_enabled=0
if: matrix.os == 'macOS-latest'
- run: cargo test
+383
View File
@@ -0,0 +1,383 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "anyhow"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "bitflags"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
[[package]]
name = "cfg-if"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
[[package]]
name = "duct"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6ce170a0e8454fa0f9b0e5ca38a6ba17ed76a50916839d217eb5357e05cdfde"
dependencies = [
"libc",
"os_pipe",
"shared_child",
"shared_thread",
]
[[package]]
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "libc"
version = "0.2.174"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
[[package]]
name = "os_pipe"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "shared_child"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
dependencies = [
"libc",
"sigchld 0.2.3",
"windows-sys 0.60.2",
]
[[package]]
name = "shared_thread"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7a6f98357c6bb0ebace19b22220e5543801d9de90ffe77f8abb27c056bac064"
[[package]]
name = "sigchld"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1219ef50fc0fdb04fcc243e6aa27f855553434ffafe4fa26554efb78b5b4bf89"
dependencies = [
"libc",
"os_pipe",
"signal-hook",
]
[[package]]
name = "sigchld"
version = "0.2.4"
dependencies = [
"anyhow",
"duct",
"libc",
"os_pipe",
"rand",
"signal-hook",
]
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
[[package]]
name = "syn"
version = "2.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.2",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+55
View File
@@ -0,0 +1,55 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "sigchld"
version = "0.2.4"
authors = ["Jack O'Connor"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "waiting for SIGCHLD from many threads at once"
documentation = "https://docs.rs/sigchld"
readme = "README.md"
license = "MIT"
repository = "https://github.com/oconnor663/sigchld.rs"
[features]
default = ["os_pipe"]
os_pipe = ["dep:os_pipe"]
[lib]
name = "sigchld"
path = "src/lib.rs"
[dependencies.libc]
version = "0.2.172"
[dependencies.os_pipe]
version = "1.2.2"
optional = true
[dependencies.signal-hook]
version = "0.3.18"
default-features = false
[dev-dependencies.anyhow]
version = "1.0.98"
[dev-dependencies.duct]
version = "1.0.0"
[dev-dependencies.rand]
version = "0.9.1"
+25
View File
@@ -0,0 +1,25 @@
[package]
name = "sigchld"
version = "0.2.4"
authors = ["Jack O'Connor"]
description = "waiting for SIGCHLD from many threads at once"
repository = "https://github.com/oconnor663/sigchld.rs"
readme = "README.md"
documentation = "https://docs.rs/sigchld"
license = "MIT"
edition = "2021"
[dependencies]
libc = "0.2.172"
os_pipe = { version = "1.2.2", optional = true }
signal-hook = { version = "0.3.18", default-features = false }
[dev-dependencies]
anyhow = "1.0.98"
duct = "1.0.0"
rand = "0.9.1"
[features]
default = ["os_pipe"]
# Replacement for std::io::pipe. Without this, the MSRV is 1.63, inherited from libc.
os_pipe = ["dep:os_pipe"]
+37
View File
@@ -0,0 +1,37 @@
## `sigchld` [![Actions Status](https://github.com/oconnor663/sigchld.rs/workflows/tests/badge.svg)](https://github.com/oconnor663/sigchld.rs/actions) [![crates.io](https://img.shields.io/crates/v/sigchld.svg)](https://crates.io/crates/sigchld) [![docs.rs](https://docs.rs/sigchld/badge.svg)](https://docs.rs/sigchld)
This crate is a low-level building block for child process management. Unix doesn't provide a
portable API for waiting for a child process to exit _with a timeout_. (Linux has `pidfd`, but
there's no equivalent on e.g. macOS.) The next best thing is waiting for the `SIGCHLD` signal,
but Unix signal handling is complicated and error-prone. This crate implements `SIGCHLD`
handling using [`signal_hook`] internally, for compatibility with other Rust signal handling
libraries. It allows any number of threads to wait for `SIGCHLD` with an optional timeout.
Note that `SIGCHLD` indicates that _any_ child process has exited, but there's no reliable way
to know _which_ child it was. You generally need to [poll your child process][try_wait] in a
loop, and wait again if it hasn't exited yet. This is still a bit error-prone, and most
applications will prefer a higher-level API that does this loop internally, like
[`shared_child`](https://docs.rs/shared_child) or [`duct`](https://docs.rs/duct).
This crate only supports Unix and doesn't build on Windows. Portable callers need to put this
crate in the `[target.'cfg(unix)'.dependencies]` section of their `Cargo.toml` and only use it
inside of `#[cfg(unix)]` blocks or similar.
## Example
```rust
let mut waiter = sigchld::Waiter::new()?;
// If SIGCHLD arrives after this point, the Waiter will buffer it.
let mut child = std::process::Command::new("sleep").arg("1").spawn()?;
// Block until *any* child exits. See also `wait_timeout` and `wait_deadline`.
waiter.wait()?;
// There's only one child process in this example, so we know that it exited. But in general
// we might not know which child woke us up, and in that case we'd need to wait and check in a
// loop. See the Waiter examples.
assert!(child.try_wait()?.is_some(), "sleep has exited");
```
[`signal_hook`]: https://docs.rs/signal-hook
[try_wait]: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
[`wait_timeout`]: https://docs.rs/shared_child/latest/shared_child/struct.SharedChild.html#method.wait_timeout
[`wait_deadline`]: https://docs.rs/shared_child/latest/shared_child/struct.SharedChild.html#method.wait_deadline
+1
View File
@@ -0,0 +1 @@
{{readme}}
+449
View File
@@ -0,0 +1,449 @@
//! # `sigchld` [![Actions Status](https://github.com/oconnor663/sigchld.rs/workflows/tests/badge.svg)](https://github.com/oconnor663/sigchld.rs/actions) [![crates.io](https://img.shields.io/crates/v/sigchld.svg)](https://crates.io/crates/sigchld) [![docs.rs](https://docs.rs/sigchld/badge.svg)](https://docs.rs/sigchld)
//!
//! This crate is a low-level building block for child process management. Unix doesn't provide a
//! portable API for waiting for a child process to exit _with a timeout_. (Linux has `pidfd`, but
//! there's no equivalent on e.g. macOS.) The next best thing is waiting for the `SIGCHLD` signal,
//! but Unix signal handling is complicated and error-prone. This crate implements `SIGCHLD`
//! handling using [`signal_hook`] internally, for compatibility with other Rust signal handling
//! libraries. It allows any number of threads to wait for `SIGCHLD` with an optional timeout.
//!
//! Note that `SIGCHLD` indicates that _any_ child process has exited, but there's no reliable way
//! to know _which_ child it was. You generally need to [poll your child process][try_wait] in a
//! loop, and wait again if it hasn't exited yet. This is still a bit error-prone, and most
//! applications will prefer a higher-level API that does this loop internally, like
//! [`shared_child`](https://docs.rs/shared_child) or [`duct`](https://docs.rs/duct).
//!
//! This crate only supports Unix and doesn't build on Windows. Portable callers need to put this
//! crate in the `[target.'cfg(unix)'.dependencies]` section of their `Cargo.toml` and only use it
//! inside of `#[cfg(unix)]` blocks or similar.
//!
//! # Example
//!
//! ```rust
//! # fn main() -> std::io::Result<()> {
//! # use std::time::Duration;
//! let mut waiter = sigchld::Waiter::new()?;
//! // If SIGCHLD arrives after this point, the Waiter will buffer it.
//! let mut child = std::process::Command::new("sleep").arg("1").spawn()?;
//! // Block until *any* child exits. See also `wait_timeout` and `wait_deadline`.
//! waiter.wait()?;
//! // There's only one child process in this example, so we know that it exited. But in general
//! // we might not know which child woke us up, and in that case we'd need to wait and check in a
//! // loop. See the Waiter examples.
//! assert!(child.try_wait()?.is_some(), "sleep has exited");
//! # Ok(())
//! # }
//! ```
//!
//! [`signal_hook`]: https://docs.rs/signal-hook
//! [try_wait]: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
//! [`wait_timeout`]: https://docs.rs/shared_child/latest/shared_child/struct.SharedChild.html#method.wait_timeout
//! [`wait_deadline`]: https://docs.rs/shared_child/latest/shared_child/struct.SharedChild.html#method.wait_deadline
#[cfg(not(unix))]
compile_error!(
"This crate is Unix-only. Put it in [target.'cfg(unix)'.dependencies] in Cargo.toml."
);
use std::io::{self, ErrorKind, Read};
use std::os::raw::c_int;
use std::os::unix::io::AsRawFd;
use std::time::{Duration, Instant};
// `os_pipe` predates `std::io::pipe`, and they have almost the exact same API. Taking the
// dependency reduces the MSRV from 1.87 to 1.63 (inherited from `libc`).
#[cfg(feature = "os_pipe")]
use os_pipe::{pipe, PipeReader};
#[cfg(not(feature = "os_pipe"))]
use std::io::{pipe, PipeReader};
// Use anyhow errors in testing, for backtraces.
#[cfg(test)]
type Result<T> = anyhow::Result<T>;
#[cfg(not(test))]
type Result<T> = io::Result<T>;
/// An object that buffers `SIGCHLD` signals so that you can wait on them reliably.
///
/// `Waiter` can't tell you _which_ process woke you up, so you usually need to wait in a loop and
/// [poll your `Child`][try_wait] each time through. One way to make sure you don't miss a signal
/// (and potentially wait forever) is to create a `Waiter` before you spawn your child process,
/// like this:
///
/// ```
/// # use std::io;
/// # fn main() -> io::Result<()> {
/// let mut waiter = sigchld::Waiter::new()?;
/// // Any SIGCHLD after this point will be buffered by the Waiter.
/// let mut child = std::process::Command::new("sleep").arg("1").spawn()?;
/// loop {
/// waiter.wait()?;
/// // *Some* child has exited. Check whether it was our child.
/// if child.try_wait()?.is_some() {
/// break;
/// }
/// }
/// // Our child has exited.
/// # Ok(())
/// # }
/// ```
///
/// If you create a `Waiter` after your child is already running, then you need to poll the child
/// at least once before you wait:
///
/// ```
/// # use std::io;
/// # fn main() -> io::Result<()> {
/// let mut child = std::process::Command::new("sleep").arg("1").spawn()?;
/// // If SIGCHLD arrives here, before the Waiter is created, we could miss it.
/// let mut waiter = sigchld::Waiter::new()?;
/// while child.try_wait()?.is_none() {
/// // Now we know the child didn't exit before we created the Waiter.
/// waiter.wait()?;
/// }
/// // Our child has exited.
/// # Ok(())
/// # }
/// ```
///
/// The following order of operations is broken. You could miss `SIGCHLD` and wait forever:
///
/// <div class="warning">
///
/// ```no_run
/// # use std::io;
/// # fn main() -> io::Result<()> {
/// let mut child = std::process::Command::new("sleep").arg("1").spawn()?;
/// // If SIGCHLD arrives now, before the Waiter is created, we could miss it.
/// let mut waiter = sigchld::Waiter::new()?;
/// // OOPS: If we missed SIGCHLD, we'll wait forever.
/// waiter.wait()?;
/// # Ok(())
/// # }
/// ```
///
/// </div>
///
/// Most applications will prefer higher-level APIs like
/// [`shared_child`](https://docs.rs/shared_child) or [`duct`](https://docs.rs/duct), where you
/// don't have to worry about this sort of mistake. This crate is intended more as a building block
/// for those APIs.
///
/// [try_wait]: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
#[derive(Debug)]
pub struct Waiter {
reader: PipeReader,
sig_id: signal_hook::SigId,
}
impl Waiter {
/// Create a `Waiter`.
///
/// Any `SIGCHLD` signals that arrive after a `Waiter` is created, but before a call to
/// [`wait`](Self::wait), [`wait_timeout`](Self::wait_timeout), or
/// [`wait_deadline`](Self::wait_deadline), will be buffered. In that case the next call to one
/// of those methods will return immediately. Note that each wait clears the entire buffer, so
/// a single wakeup could indicate that multiple signals arrived. In other words, signals can
/// be "coalesced".
pub fn new() -> Result<Self> {
let (reader, writer) = pipe()?;
set_nonblocking(&reader)?;
set_nonblocking(&writer)?;
let sig_id = signal_hook::low_level::pipe::register(libc::SIGCHLD, writer)?;
Ok(Self { reader, sig_id })
}
/// Block the current thread until any `SIGCHLD` signal arrives.
///
/// If any `SIGCHLD` signals have arrived since the `Waiter` was created, this function will
/// return immediately. This avoids a race condition where the child exits right after you call
/// [`Child::try_wait`] but right before you call this function.
///
/// This function does not reap any exited children. Child process cleanup is only done by
/// [`Child::wait`] or [`Child::try_wait`].
///
/// This function is not currently susceptible to "spurious wakeups" (i.e. returning early for
/// no reason), but this property isn't guaranteed, and future versions might be. Getting woken
/// up early by an unrelated child process exiting (e.g. one spawned by some unknown library
/// code running on another thread) is similar to a spurious wakeup, and you might need to be
/// defensive and wait in a loop either way.
///
/// [`Child::wait`]: https://doc.rust-lang.org/std/process/struct.Child.html#method.wait
/// [`Child::try_wait`]: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
pub fn wait(&mut self) -> Result<()> {
let signaled = self.wait_inner(None)?;
debug_assert!(signaled, "timeout shouldn't be possible");
Ok(())
}
/// Block the current thread until either any `SIGCHLD` signal arrives or a timeout passes.
/// Return `true` if a signal arrived before the timeout.
///
/// If any `SIGCHLD` signals have arrived since the `Waiter` was created, this function will
/// return immediately. This avoids a race condition where the child exits right after you call
/// [`Child::try_wait`] but right before you call this function.
///
/// This function does not reap any exited children. Child process cleanup is only done by
/// [`Child::wait`] or [`Child::try_wait`].
///
/// This function is not currently susceptible to "spurious wakeups" (i.e. returning early for
/// no reason), but this property isn't guaranteed, and future versions might be. Getting woken
/// up early by an unrelated child process exiting (e.g. one spawned by some unknown library
/// code running on another thread) is similar to a spurious wakeup, and you might need to be
/// defensive and wait in a loop either way.
///
/// [`Child::wait`]: https://doc.rust-lang.org/std/process/struct.Child.html#method.wait
/// [`Child::try_wait`]: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
pub fn wait_timeout(&mut self, timeout: Duration) -> Result<bool> {
let deadline = Instant::now() + timeout;
self.wait_inner(Some(deadline))
}
/// Block the current thread until either any `SIGCHLD` signal arrives or a deadline passes.
/// Return `true` if a signal arrived before the deadline.
///
/// If any `SIGCHLD` signals have arrived since the `Waiter` was created, this function will
/// return immediately. This avoids a race condition where the child exits right after you call
/// [`Child::try_wait`] but right before you call this function.
///
/// This function does not reap any exited children. Child process cleanup is only done by
/// [`Child::wait`] or [`Child::try_wait`].
///
/// This function is not currently susceptible to "spurious wakeups" (i.e. returning early for
/// no reason), but this property isn't guaranteed, and future versions might be. Getting woken
/// up early by an unrelated child process exiting (e.g. one spawned by some unknown library
/// code running on another thread) is similar to a spurious wakeup, and you might need to be
/// defensive and wait in a loop either way.
///
/// [`Child::wait`]: https://doc.rust-lang.org/std/process/struct.Child.html#method.wait
/// [`Child::try_wait`]: https://doc.rust-lang.org/std/process/struct.Child.html#method.try_wait
pub fn wait_deadline(&mut self, deadline: Instant) -> Result<bool> {
self.wait_inner(Some(deadline))
}
fn wait_inner(&mut self, maybe_deadline: Option<Instant>) -> Result<bool> {
// Loop to handle spurious wakeups from poll().
loop {
// Read the pipe until EWOULDBLOCK. This could take more than one read.
let mut buf = [0u8; 1024];
let mut signaled = false;
loop {
match self.reader.read(&mut buf) {
Ok(0) => unreachable!("this pipe should never close"),
Ok(_) => signaled = true,
Err(e) if e.kind() == ErrorKind::WouldBlock => break,
// EINTR shouldn't be possible for a nonblocking read.
#[allow(clippy::useless_conversion)]
Err(e) => return Err(e.into()),
}
}
// If we were signaled, return true.
if signaled {
return Ok(true);
}
// If the deadline has passed, return false.
if let Some(deadline) = maybe_deadline {
if Instant::now() > deadline {
return Ok(false);
}
}
// Use poll() to wait until either the deadline passes or the pipe is readable.
let mut poll_fd = libc::pollfd {
fd: self.reader.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
};
let timeout_ms: c_int = if let Some(deadline) = maybe_deadline {
let timeout = deadline.saturating_duration_since(Instant::now());
// Convert to milliseconds, rounding *up*. (That way we don't repeatedly sleep for
// 0ms when we're close to the timeout.)
(timeout.as_nanos().saturating_add(999_999) / 1_000_000)
.try_into()
.unwrap_or(c_int::MAX)
} else {
-1 // infinite timeout
};
let poll_return_code = unsafe {
libc::poll(
&mut poll_fd, // an "array" of one
1, // the "array" length
timeout_ms,
)
};
if poll_return_code < 0 {
// EINTR is expected here, and the delivery of SIGCHLD usually causes it.
let last_error = io::Error::last_os_error();
if last_error.kind() != ErrorKind::Interrupted {
#[allow(clippy::useless_conversion)]
return Err(last_error.into());
}
}
// Go back to the top of the loop and try to read again.
}
}
}
impl Drop for Waiter {
fn drop(&mut self) {
let existed = signal_hook::low_level::unregister(self.sig_id);
debug_assert!(existed, "should've existed");
}
}
// The standard library doesn't expose set_nonblocking for pipes. Do it the old-fashioned way.
fn set_nonblocking(fd: &impl AsRawFd) -> Result<()> {
unsafe {
let return_code = libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, libc::O_NONBLOCK);
if return_code == -1 {
#[allow(clippy::useless_conversion)]
Err(io::Error::last_os_error().into())
} else {
Ok(())
}
}
}
#[cfg(test)]
mod test {
use super::*;
use duct::cmd;
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, Instant};
// We need to make sure only one test runs at a time, because these waits are global, and
// they'll confuse each other. Use a parking_lot mutex so that it doesn't get poisoned.
//
// XXX: These tests don't wait in a loop, because if there are bugs here that cause early
// wakeups, I'd rather the tests fail than hide the bug. I expect these tests will "randomly"
// fail under certain circumstances, and that's worth it to me to catch more bugs. But real
// callers should wait in a loop so that they don't randomly fail.
static ONE_TEST_AT_A_TIME: Mutex<()> = Mutex::new(());
fn lock_no_poison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
match mutex.lock() {
Ok(guard) => guard,
Err(e) => e.into_inner(),
}
}
#[track_caller]
fn assert_approx_eq(dur1: Duration, dur2: Duration) {
const CLOSE_ENOUGH: f64 = 0.1; // 10%
let lower_bound = 1.0 - CLOSE_ENOUGH;
let upper_bound = 1.0 + CLOSE_ENOUGH;
let ratio = dur1.as_secs_f64() / dur2.as_secs_f64();
assert!(
lower_bound < ratio && ratio < upper_bound,
"{dur1:?} and {dur2:?} are not close enough",
);
}
#[test]
fn test_wait() -> Result<()> {
let _test_guard = lock_no_poison(&ONE_TEST_AT_A_TIME); // see comment on the lock
let start = Instant::now();
let mut waiter = Waiter::new()?;
cmd!("sleep", "0.25").start()?;
waiter.wait()?;
let dur = Instant::now() - start;
assert_approx_eq(Duration::from_millis(250), dur);
Ok(())
}
#[test]
fn test_wait_deadline() -> Result<()> {
let _test_guard = lock_no_poison(&ONE_TEST_AT_A_TIME); // see comment on the lock
let start = Instant::now();
let timeout = Duration::from_millis(500);
let mut waiter = Waiter::new()?;
cmd!("sleep", "0.25").start()?;
// This first wait should return true.
let signaled = waiter.wait_deadline(Instant::now() + timeout)?;
let dur = Instant::now() - start;
assert_approx_eq(Duration::from_millis(250), dur);
assert!(signaled);
// This second wait should time out and return false.
let mut waiter2 = Waiter::new()?;
let signaled2 = waiter2.wait_deadline(Instant::now() + timeout)?;
let dur2 = Instant::now() - start;
assert_approx_eq(Duration::from_millis(750), dur2);
assert!(!signaled2);
Ok(())
}
#[test]
fn test_wait_timeout() -> Result<()> {
let _test_guard = lock_no_poison(&ONE_TEST_AT_A_TIME); // see comment on the lock
let start = Instant::now();
let timeout = Duration::from_millis(500);
let mut waiter = Waiter::new()?;
cmd!("sleep", "0.25").start()?;
// This first wait should return true.
let signaled = waiter.wait_timeout(timeout)?;
let dur = Instant::now() - start;
assert_approx_eq(Duration::from_millis(250), dur);
assert!(signaled);
// This second wait should time out and return false.
let mut waiter2 = Waiter::new()?;
let signaled2 = waiter2.wait_timeout(timeout)?;
let dur2 = Instant::now() - start;
assert_approx_eq(Duration::from_millis(750), dur2);
assert!(!signaled2);
Ok(())
}
#[test]
fn test_wait_many_threads() -> Result<()> {
let _test_guard = lock_no_poison(&ONE_TEST_AT_A_TIME); // see comment on the lock
let start = Instant::now();
let handle = Arc::new(cmd!("sleep", "1").start()?);
let mut wait_threads = Vec::new();
let mut short_timeout_threads = Vec::new();
let mut long_timeout_threads = Vec::new();
for _ in 0..3 {
let handle_clone = handle.clone();
let mut waiter = Waiter::new()?;
wait_threads.push(std::thread::spawn(move || -> Result<Duration> {
waiter.wait()?;
let dur = Instant::now() - start;
assert!(handle_clone.try_wait()?.is_some(), "should've exited");
Ok(dur)
}));
let handle_clone = handle.clone();
let mut waiter = Waiter::new()?;
short_timeout_threads.push(std::thread::spawn(move || -> Result<bool> {
let signaled = waiter.wait_timeout(Duration::from_millis(500))?;
assert!(handle_clone.try_wait()?.is_none(), "shouldn't have exited");
Ok(signaled)
}));
let handle_clone = handle.clone();
let mut waiter = Waiter::new()?;
long_timeout_threads.push(std::thread::spawn(move || -> Result<bool> {
let signaled = waiter.wait_timeout(Duration::from_millis(1500))?;
assert!(handle_clone.try_wait()?.is_some(), "should've exited");
Ok(signaled)
}));
}
for thread in wait_threads {
let dur = thread.join().unwrap()?;
assert_approx_eq(Duration::from_millis(1000), dur);
}
for thread in short_timeout_threads {
assert!(!thread.join().unwrap()?, "should not be signaled");
}
for thread in long_timeout_threads {
assert!(thread.join().unwrap()?, "should be signaled");
}
Ok(())
}
}