From bb1cb041d38431596b5b7c8b6c55ded7b3a03a66 Mon Sep 17 00:00:00 2001 From: Jiahao XU Date: Tue, 26 Mar 2024 23:57:35 +1100 Subject: [PATCH 1/3] Refactor CI: Extract `.github/actions/compile-make` Signed-off-by: Jiahao XU --- .github/actions/compile-make/action.yml | 37 ++++++++++++ .github/workflows/main.yml | 75 ++++++++++--------------- 2 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 .github/actions/compile-make/action.yml diff --git a/.github/actions/compile-make/action.yml b/.github/actions/compile-make/action.yml new file mode 100644 index 0000000..e4beff5 --- /dev/null +++ b/.github/actions/compile-make/action.yml @@ -0,0 +1,37 @@ +name: Compile make +description: compile-make +inputs: + version: + description: make version + required: true + workaround: + description: enable workaround for _alloc bug + required: false + default: "false" + +runs: + using: composite + steps: + - name: Cache make compiled + if: ${{ !startsWith(runner.os, 'windows') }} + id: cache-maka + uses: actions/cache@v4 + with: + path: /usr/local/bin/make-${{ inputs.version }} + key: v1-${{ runner.os }}-make-${{ inputs.version }} + + # Compile it from source (temporarily) + - name: Make GNU Make from source + if: ${{ !startsWith(runner.os, 'windows') && steps.cache-make.outputs.cache-hit != 'true' }} + env: + VERSION: ${{ inputs.version }} + WORKAROUND: ${{ inputs.workaround }} + shell: bash + run: | + curl "https://ftp.gnu.org/gnu/make/make-${VERSION}.tar.gz" | tar xz + pushd "make-${VERSION}" + ./configure + [[ "$WORKAROUND" = "true" ]] && sed -i 's/#if !defined __alloca \&\& !defined __GNU_LIBRARY__/#if !defined __alloca \&\& defined __GNU_LIBRARY__/g; s/#ifndef __GNU_LIBRARY__/#ifdef __GNU_LIBRARY__/g' "./glob/glob.c" + make -j 4 + popd + cp -p "make-${VERSION}/make" "/usr/local/bin/make-${VERSION}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 919c97b..b9bf58d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,54 +13,37 @@ jobs: rust: [stable, beta, nightly] os: [ubuntu-latest, macos-14, windows-latest] steps: - - uses: actions/checkout@master - - name: Install Rust (rustup) - run: | - rustup toolchain install ${{ matrix.rust }} --no-self-update --profile minimal - rustup default ${{ matrix.rust }} - shell: bash - - - uses: Swatinem/rust-cache@v2 + - uses: actions/checkout@master + - name: Install Rust (rustup) + run: | + rustup toolchain install ${{ matrix.rust }} --no-self-update --profile minimal + rustup default ${{ matrix.rust }} + shell: bash - - run: cargo test --locked + - uses: Swatinem/rust-cache@v2 - - name: Cache make compiled - if: ${{ !startsWith(matrix.os, 'windows') }} - id: cache-make - uses: actions/cache@v4 - with: - path: /usr/local/bin/make - key: ${{ runner.os }}-make-4.4.1 + - run: cargo test --locked - # Compile it from source (temporarily) - - name: Make GNU Make from source - if: ${{ !startsWith(matrix.os, 'windows') && steps.cache-make.outputs.cache-hit != 'true' }} - env: - VERSION: "4.4.1" - shell: bash - run: | - curl "https://ftp.gnu.org/gnu/make/make-${VERSION}.tar.gz" | tar xz - pushd "make-${VERSION}" - ./configure - make -j 4 - popd - cp -p "make-${VERSION}/make" /usr/local/bin + - name: Compile make 4.4.1 + uses: ./.github/actions/compile-make + with: + version: 4.4.1 - - name: Test against GNU Make from source - if: ${{ !startsWith(matrix.os, 'windows') }} - shell: bash - run: cargo test --locked - env: - MAKE: /usr/local/bin/make + - name: Test against GNU Make 4.4.1 + if: ${{ !startsWith(matrix.os, 'windows') }} + shell: bash + run: cargo test --locked + env: + MAKE: /usr/local/bin/make-4.4.1 rustfmt: name: Rustfmt runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update stable && rustup default stable && rustup component add rustfmt - - run: cargo fmt -- --check + - uses: actions/checkout@master + - name: Install Rust + run: rustup update stable && rustup default stable && rustup component add rustfmt + - run: cargo fmt -- --check publish_docs: name: Publish Documentation @@ -86,12 +69,12 @@ jobs: matrix: os: [ubuntu-latest, macos-14, windows-latest] steps: - - uses: actions/checkout@master - - name: Install Rust (rustup) - run: rustup toolchain install nightly --no-self-update --profile minimal - shell: bash + - uses: actions/checkout@master + - name: Install Rust (rustup) + run: rustup toolchain install nightly --no-self-update --profile minimal + shell: bash - - uses: taiki-e/install-action@cargo-hack - - uses: Swatinem/rust-cache@v2 + - uses: taiki-e/install-action@cargo-hack + - uses: Swatinem/rust-cache@v2 - - run: cargo hack check --lib --rust-version --ignore-private --locked + - run: cargo hack check --lib --rust-version --ignore-private --locked From 89432f4e8dc785def2bf95c80e46b3bdc95dddd1 Mon Sep 17 00:00:00 2001 From: Jiahao XU Date: Sat, 2 Mar 2024 18:19:29 +1100 Subject: [PATCH 2/3] Add optimization for linux: Reopen pipe as fifo so that we could set `O_NONBLOCK` on it. Signed-off-by: Jiahao XU --- src/unix.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/unix.rs b/src/unix.rs index 25efef1..6dd9355 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -155,6 +155,14 @@ impl Client { (read_err, write_err) => { read_err?; write_err?; + + #[cfg(target_os = "linux")] + // Optimization: Try converting it to a fifo by using /dev/fd + if let Ok(Some(jobserver)) = + Self::from_fifo(&format!("/dev/fd/{}", read.as_raw_fd())) + { + return Ok(Some(jobserver)); + } } } From 81195a4f6c445f39a3812d7074e5e20760fc5086 Mon Sep 17 00:00:00 2001 From: Jiahao XU Date: Sat, 2 Mar 2024 20:37:49 +1100 Subject: [PATCH 3/3] Impl `Client::try_acquire` With suggestions from @weihanglo Co-authored-by: Weihang Lo Signed-off-by: Jiahao XU --- .github/actions/compile-make/action.yml | 1 - .gitignore | 1 - Cargo.lock | 23 ++- Cargo.toml | 5 +- src/lib.rs | 142 ++++++++++++------ src/unix.rs | 189 +++++++++++++++++++++++- src/wasm.rs | 10 ++ src/windows.rs | 21 ++- 8 files changed, 333 insertions(+), 59 deletions(-) diff --git a/.github/actions/compile-make/action.yml b/.github/actions/compile-make/action.yml index e4beff5..bdac338 100644 --- a/.github/actions/compile-make/action.yml +++ b/.github/actions/compile-make/action.yml @@ -20,7 +20,6 @@ runs: path: /usr/local/bin/make-${{ inputs.version }} key: v1-${{ runner.os }}-make-${{ inputs.version }} - # Compile it from source (temporarily) - name: Make GNU Make from source if: ${{ !startsWith(runner.os, 'windows') && steps.cache-make.outputs.cache-hit != 'true' }} env: diff --git a/.gitignore b/.gitignore index 4308d82..324c57f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ target/ **/*.rs.bk -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 7531774..3f77740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + [[package]] name = "errno" version = "0.3.8" @@ -35,14 +41,15 @@ name = "jobserver" version = "0.1.28" dependencies = [ "libc", + "nix", "tempfile", ] [[package]] name = "libc" -version = "0.2.152" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "linux-raw-sys" @@ -50,6 +57,18 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "rustix" version = "0.38.31" diff --git a/Cargo.toml b/Cargo.toml index 42e72ad..d70d666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,10 @@ edition = "2021" rust-version = "1.63" [target.'cfg(unix)'.dependencies] -libc = "0.2.72" +libc = "0.2.87" + +[target.'cfg(unix)'.dev-dependencies] +nix = { version = "0.28.0", features = ["fs"] } [dev-dependencies] tempfile = "3.10.1" diff --git a/src/lib.rs b/src/lib.rs index 693fd28..e01c026 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -324,6 +324,32 @@ impl Client { }) } + /// Acquires a token from this jobserver client in a non-blocking way. + /// + /// # Return value + /// + /// On successful acquisition of a token an instance of [`Acquired`] is + /// returned. This structure, when dropped, will release the token back to + /// the jobserver. It's recommended to avoid leaking this value. + /// + /// # Errors + /// + /// If an I/O error happens while acquiring a token then this function will + /// return immediately with the error. If an error is returned then a token + /// was not acquired. + /// + /// If non-blocking acquire is not supported, the return error will have its `kind()` + /// set to [`io::ErrorKind::Unsupported`]. + pub fn try_acquire(&self) -> io::Result> { + let ret = self.inner.try_acquire()?; + + Ok(ret.map(|data| Acquired { + client: self.inner.clone(), + data, + disabled: false, + })) + } + /// Returns amount of tokens in the read-side pipe. /// /// # Return value @@ -607,52 +633,76 @@ fn find_jobserver_auth(var: &str) -> Option<&str> { .and_then(|s| s.split(' ').next()) } -#[test] -fn no_helper_deadlock() { - let x = crate::Client::new(32).unwrap(); - let _y = x.clone(); - std::mem::drop(x.into_helper_thread(|_| {}).unwrap()); -} +#[cfg(test)] +mod test { + use super::*; + + pub(super) fn run_named_fifo_try_acquire_tests(client: &Client) { + assert!(client.try_acquire().unwrap().is_none()); + client.release_raw().unwrap(); -#[test] -fn test_find_jobserver_auth() { - let cases = [ - ("", None), - ("-j2", None), - ("-j2 --jobserver-auth=3,4", Some("3,4")), - ("--jobserver-auth=3,4 -j2", Some("3,4")), - ("--jobserver-auth=3,4", Some("3,4")), - ("--jobserver-auth=fifo:/myfifo", Some("fifo:/myfifo")), - ("--jobserver-auth=", Some("")), - ("--jobserver-auth", None), - ("--jobserver-fds=3,4", Some("3,4")), - ("--jobserver-fds=fifo:/myfifo", Some("fifo:/myfifo")), - ("--jobserver-fds=", Some("")), - ("--jobserver-fds", None), - ( - "--jobserver-auth=auth-a --jobserver-auth=auth-b", - Some("auth-b"), - ), - ( - "--jobserver-auth=auth-b --jobserver-auth=auth-a", - Some("auth-a"), - ), - ("--jobserver-fds=fds-a --jobserver-fds=fds-b", Some("fds-b")), - ("--jobserver-fds=fds-b --jobserver-fds=fds-a", Some("fds-a")), - ( - "--jobserver-auth=auth-a --jobserver-fds=fds-a --jobserver-auth=auth-b", - Some("auth-b"), - ), - ( - "--jobserver-fds=fds-a --jobserver-auth=auth-a --jobserver-fds=fds-b", - Some("auth-a"), - ), - ]; - for (var, expected) in cases { - let actual = find_jobserver_auth(var); - assert_eq!( - actual, expected, - "expect {expected:?}, got {actual:?}, input `{var:?}`" - ); + let acquired = client.try_acquire().unwrap().unwrap(); + assert!(client.try_acquire().unwrap().is_none()); + + drop(acquired); + client.try_acquire().unwrap().unwrap(); + } + + #[cfg(not(unix))] + #[test] + fn test_try_acquire() { + let client = Client::new(0).unwrap(); + + run_named_fifo_try_acquire_tests(&client); + } + + #[test] + fn no_helper_deadlock() { + let x = crate::Client::new(32).unwrap(); + let _y = x.clone(); + std::mem::drop(x.into_helper_thread(|_| {}).unwrap()); + } + + #[test] + fn test_find_jobserver_auth() { + let cases = [ + ("", None), + ("-j2", None), + ("-j2 --jobserver-auth=3,4", Some("3,4")), + ("--jobserver-auth=3,4 -j2", Some("3,4")), + ("--jobserver-auth=3,4", Some("3,4")), + ("--jobserver-auth=fifo:/myfifo", Some("fifo:/myfifo")), + ("--jobserver-auth=", Some("")), + ("--jobserver-auth", None), + ("--jobserver-fds=3,4", Some("3,4")), + ("--jobserver-fds=fifo:/myfifo", Some("fifo:/myfifo")), + ("--jobserver-fds=", Some("")), + ("--jobserver-fds", None), + ( + "--jobserver-auth=auth-a --jobserver-auth=auth-b", + Some("auth-b"), + ), + ( + "--jobserver-auth=auth-b --jobserver-auth=auth-a", + Some("auth-a"), + ), + ("--jobserver-fds=fds-a --jobserver-fds=fds-b", Some("fds-b")), + ("--jobserver-fds=fds-b --jobserver-fds=fds-a", Some("fds-a")), + ( + "--jobserver-auth=auth-a --jobserver-fds=fds-a --jobserver-auth=auth-b", + Some("auth-b"), + ), + ( + "--jobserver-fds=fds-a --jobserver-auth=auth-a --jobserver-fds=fds-b", + Some("auth-a"), + ), + ]; + for (var, expected) in cases { + let actual = find_jobserver_auth(var); + assert_eq!( + actual, expected, + "expect {expected:?}, got {actual:?}, input `{var:?}`" + ); + } } } diff --git a/src/unix.rs b/src/unix.rs index 6dd9355..aed2813 100644 --- a/src/unix.rs +++ b/src/unix.rs @@ -9,7 +9,10 @@ use std::os::unix::prelude::*; use std::path::{Path, PathBuf}; use std::process::Command; use std::ptr; -use std::sync::{Arc, Once}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Once, +}; use std::thread::{self, Builder, JoinHandle}; use std::time::Duration; @@ -18,7 +21,13 @@ pub enum Client { /// `--jobserver-auth=R,W` Pipe { read: File, write: File }, /// `--jobserver-auth=fifo:PATH` - Fifo { file: File, path: PathBuf }, + Fifo { + file: File, + path: PathBuf, + /// it can only go from false -> true but not the other way around, since that + /// could cause a race condition. + is_non_blocking: AtomicBool, + }, } #[derive(Debug)] @@ -58,8 +67,6 @@ impl Client { // with as many kernels/glibc implementations as possible. #[cfg(target_os = "linux")] { - use std::sync::atomic::{AtomicBool, Ordering}; - static PIPE2_AVAILABLE: AtomicBool = AtomicBool::new(true); if PIPE2_AVAILABLE.load(Ordering::SeqCst) { match libc::syscall(libc::SYS_pipe2, pipes.as_mut_ptr(), libc::O_CLOEXEC) { @@ -109,9 +116,11 @@ impl Client { .write(true) .open(path) .map_err(|err| FromEnvErrorInner::CannotOpenPath(path_str.to_string(), err))?; + Ok(Some(Client::Fifo { file, path: path.into(), + is_non_blocking: AtomicBool::new(false), })) } @@ -156,10 +165,17 @@ impl Client { read_err?; write_err?; - #[cfg(target_os = "linux")] // Optimization: Try converting it to a fifo by using /dev/fd + // + // On linux, opening `/dev/fd/$fd` returns a fd with a new file description, + // so we can set `O_NONBLOCK` on it without affecting other processes. + // + // On macOS, opening `/dev/fd/$fd` seems to be the same as `File::try_clone`. + // + // I tested this on macOS 14 and Linux 6.5.13 + #[cfg(target_os = "linux")] if let Ok(Some(jobserver)) = - Self::from_fifo(&format!("/dev/fd/{}", read.as_raw_fd())) + Self::from_fifo(&format!("fifo:/dev/fd/{}", read.as_raw_fd())) { return Ok(Some(jobserver)); } @@ -238,7 +254,7 @@ impl Client { Ok(1) => return Ok(Some(Acquired { byte: buf[0] })), Ok(_) => { return Err(io::Error::new( - io::ErrorKind::Other, + io::ErrorKind::UnexpectedEof, "early EOF on jobserver pipe", )); } @@ -266,6 +282,65 @@ impl Client { } } + pub fn try_acquire(&self) -> io::Result> { + let mut buf = [0]; + + // On Linux, we can use preadv2 to do non-blocking read, + // even if `O_NONBLOCK` is not set. + #[cfg(target_os = "linux")] + { + let read = self.read().as_raw_fd(); + loop { + match non_blocking_read(read, &mut buf) { + Ok(1) => return Ok(Some(Acquired { byte: buf[0] })), + Ok(_) => { + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "early EOF on jobserver pipe", + )) + } + + Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Ok(None), + Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) if e.kind() == io::ErrorKind::Unsupported => break, + + Err(err) => return Err(err), + } + } + } + + let (mut fifo, is_non_blocking) = match self { + Self::Fifo { + file, + is_non_blocking, + .. + } => (file, is_non_blocking), + _ => return Err(io::ErrorKind::Unsupported.into()), + }; + + if !is_non_blocking.load(Ordering::Relaxed) { + set_nonblocking(fifo.as_raw_fd(), true)?; + is_non_blocking.store(true, Ordering::Relaxed); + } + + loop { + match fifo.read(&mut buf) { + Ok(1) => break Ok(Some(Acquired { byte: buf[0] })), + Ok(_) => { + break Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "early EOF on jobserver pipe", + )) + } + + Err(e) if e.kind() == io::ErrorKind::WouldBlock => break Ok(None), + Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, + + Err(err) => break Err(err), + } + } + } + pub fn release(&self, data: Option<&Acquired>) -> io::Result<()> { // Note that the fd may be nonblocking but we're going to go ahead // and assume that the writes here are always nonblocking (we can @@ -501,3 +576,103 @@ extern "C" fn sigusr1_handler( ) { // nothing to do } + +#[cfg(target_os = "linux")] +fn cvt_ssize(t: libc::ssize_t) -> io::Result { + if t == -1 { + Err(io::Error::last_os_error()) + } else { + Ok(t) + } +} + +#[cfg(target_os = "linux")] +fn non_blocking_read(fd: c_int, buf: &[u8]) -> io::Result { + static IS_NONBLOCKING_READ_UNSUPPORTED: AtomicBool = AtomicBool::new(false); + + if IS_NONBLOCKING_READ_UNSUPPORTED.load(Ordering::Relaxed) { + return Err(io::ErrorKind::Unsupported.into()); + } + + match cvt_ssize(unsafe { + libc::preadv2( + fd, + &libc::iovec { + iov_base: buf.as_ptr() as *mut _, + iov_len: buf.len(), + }, + 1, + -1, + libc::RWF_NOWAIT, + ) + }) { + Ok(cnt) => Ok(cnt.try_into().unwrap()), + Err(err) if err.raw_os_error() == Some(libc::EOPNOTSUPP) => { + IS_NONBLOCKING_READ_UNSUPPORTED.store(true, Ordering::Relaxed); + Err(io::ErrorKind::Unsupported.into()) + } + Err(err) if err.kind() == io::ErrorKind::Unsupported => { + IS_NONBLOCKING_READ_UNSUPPORTED.store(true, Ordering::Relaxed); + Err(err) + } + Err(err) => Err(err), + } +} + +#[cfg(test)] +mod test { + use super::Client as ClientImp; + + use crate::{test::run_named_fifo_try_acquire_tests, Client}; + + use std::{ + fs::File, + io::{self, Write}, + os::unix::io::AsRawFd, + sync::Arc, + }; + + fn from_imp_client(imp: ClientImp) -> Client { + Client { + inner: Arc::new(imp), + } + } + + #[test] + fn test_try_acquire_named_fifo() { + let file = tempfile::NamedTempFile::new().unwrap(); + let fifo_path = file.path().to_owned(); + file.close().unwrap(); // Remove the NamedTempFile to create fifo + + nix::unistd::mkfifo(&fifo_path, nix::sys::stat::Mode::S_IRWXU).unwrap(); + + let client = ClientImp::from_fifo(&format!("fifo:{}", fifo_path.to_str().unwrap())) + .unwrap() + .map(from_imp_client) + .unwrap(); + + run_named_fifo_try_acquire_tests(&client); + } + + #[cfg(not(target_os = "linux"))] + #[test] + fn test_try_acquire_annoymous_pipe_linux_specific_optimization() { + let (read, write) = nix::unistd::pipe().unwrap(); + let read = File::from(read); + let mut write = File::from(write); + + write.write_all(b"1").unwrap(); + + let client = unsafe { + ClientImp::from_pipe(&format!("{},{}", read.as_raw_fd(), write.as_raw_fd()), true) + } + .unwrap() + .map(from_imp_client) + .unwrap(); + + assert_eq!( + client.try_acquire().unwrap_err().kind(), + io::ErrorKind::Unsupported + ); + } +} diff --git a/src/wasm.rs b/src/wasm.rs index fe92d72..930cfe6 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -45,6 +45,16 @@ impl Client { Ok(Acquired(())) } + pub fn try_acquire(&self) -> io::Result> { + let mut lock = self.inner.count.lock().unwrap_or_else(|e| e.into_inner()); + if *lock == 0 { + None + } else { + *lock -= 1; + Ok(Acquired(())) + } + } + pub fn release(&self, _data: Option<&Acquired>) -> io::Result<()> { let mut lock = self.inner.count.lock().unwrap_or_else(|e| e.into_inner()); *lock += 1; diff --git a/src/windows.rs b/src/windows.rs index 6fae7b2..997696c 100644 --- a/src/windows.rs +++ b/src/windows.rs @@ -26,7 +26,11 @@ const INFINITE: DWORD = 0xffffffff; const SEMAPHORE_MODIFY_STATE: DWORD = 0x2; const SYNCHRONIZE: DWORD = 0x00100000; const TRUE: BOOL = 1; -const WAIT_OBJECT_0: DWORD = 0; + +const WAIT_ABANDONED: DWORD = 128u32; +const WAIT_FAILED: DWORD = 4294967295u32; +const WAIT_OBJECT_0: DWORD = 0u32; +const WAIT_TIMEOUT: DWORD = 258u32; extern "system" { fn CloseHandle(handle: HANDLE) -> BOOL; @@ -159,6 +163,21 @@ impl Client { } } + pub fn try_acquire(&self) -> io::Result> { + match unsafe { WaitForSingleObject(self.sem.0, 0) } { + WAIT_OBJECT_0 => Ok(Some(Acquired)), + WAIT_TIMEOUT => Ok(None), + WAIT_FAILED => Err(io::Error::last_os_error()), + // We believe this should be impossible for a semaphore, but still + // check the error code just in case it happens. + WAIT_ABANDONED => Err(io::Error::new( + io::ErrorKind::Other, + "Wait on jobserver semaphore returned WAIT_ABANDONED", + )), + _ => unreachable!("Unexpected return value from WaitForSingleObject"), + } + } + pub fn release(&self, _data: Option<&Acquired>) -> io::Result<()> { unsafe { let r = ReleaseSemaphore(self.sem.0, 1, ptr::null_mut());