From b00bb4f539c20860d379a2c4e223c90fe3ece48d Mon Sep 17 00:00:00 2001 From: Karl McDowall Date: Mon, 13 Jan 2025 11:17:50 -0700 Subject: [PATCH] Prototype changes to use semaphore signalling for Unix Very much in the quick-prototype phase. Demonstrate that the CTRL-C logic can be alternatively implemented using a unnamed POSIX semaphore to signal between signal-handler and blocking thread. --- Cargo.toml | 2 + src/platform/unix/mod.rs | 134 +++++++---------------------- src/platform/unix/pipe/mod.rs | 112 ++++++++++++++++++++++++ src/platform/unix/semaphore/mod.rs | 55 ++++++++++++ 4 files changed, 198 insertions(+), 105 deletions(-) create mode 100644 src/platform/unix/pipe/mod.rs create mode 100644 src/platform/unix/semaphore/mod.rs diff --git a/Cargo.toml b/Cargo.toml index f9678a4..e8599ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ rust-version = "1.69.0" [target.'cfg(unix)'.dependencies] nix = { version = "0.29", default-features = false, features = ["fs", "signal"]} +libc = { version = "^0.2" } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_System_Threading", "Win32_Security", "Win32_System_Console"] } @@ -25,6 +26,7 @@ windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem", "Win32 [features] termination = [] +unix_use_semaphore = [] [[test]] harness = false diff --git a/src/platform/unix/mod.rs b/src/platform/unix/mod.rs index b91ea76..1ad5735 100644 --- a/src/platform/unix/mod.rs +++ b/src/platform/unix/mod.rs @@ -7,13 +7,17 @@ // notice may not be copied, modified, or distributed except // according to those terms. -use crate::error::Error as CtrlcError; -use nix::unistd; -use std::os::fd::BorrowedFd; -use std::os::fd::IntoRawFd; -use std::os::unix::io::RawFd; +#[cfg(feature = "unix_use_semaphore")] +mod semaphore; -static mut PIPE: (RawFd, RawFd) = (-1, -1); +#[cfg(not(feature = "unix_use_semaphore"))] +mod pipe; + +#[cfg(feature = "unix_use_semaphore")] +pub use self::semaphore::*; + +#[cfg(not(feature = "unix_use_semaphore"))] +pub use self::pipe::*; /// Platform specific error type pub type Error = nix::Error; @@ -24,63 +28,13 @@ pub type Signal = nix::sys::signal::Signal; extern "C" fn os_handler(_: nix::libc::c_int) { // Assuming this always succeeds. Can't really handle errors in any meaningful way. unsafe { - let fd = BorrowedFd::borrow_raw(PIPE.1); - let _ = unistd::write(fd, &[0u8]); - } -} - -// pipe2(2) is not available on macOS, iOS, AIX or Haiku, so we need to use pipe(2) and fcntl(2) -#[inline] -#[cfg(any( - target_os = "ios", - target_os = "macos", - target_os = "haiku", - target_os = "aix", - target_os = "nto", -))] -fn pipe2(flags: nix::fcntl::OFlag) -> nix::Result<(RawFd, RawFd)> { - use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag}; - - let pipe = unistd::pipe()?; - let pipe = (pipe.0.into_raw_fd(), pipe.1.into_raw_fd()); - - let mut res = Ok(0); - - if flags.contains(OFlag::O_CLOEXEC) { - res = res - .and_then(|_| fcntl(pipe.0, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))) - .and_then(|_| fcntl(pipe.1, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))); - } - - if flags.contains(OFlag::O_NONBLOCK) { - res = res - .and_then(|_| fcntl(pipe.0, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))) - .and_then(|_| fcntl(pipe.1, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))); - } - - match res { - Ok(_) => Ok(pipe), - Err(e) => { - let _ = unistd::close(pipe.0); - let _ = unistd::close(pipe.1); - Err(e) - } + #[cfg(feature = "unix_use_semaphore")] + os_handler_sem(); + #[cfg(not(feature = "unix_use_semaphore"))] + os_handler_pipe(); } } -#[inline] -#[cfg(not(any( - target_os = "ios", - target_os = "macos", - target_os = "haiku", - target_os = "aix", - target_os = "nto", -)))] -fn pipe2(flags: nix::fcntl::OFlag) -> nix::Result<(RawFd, RawFd)> { - let pipe = unistd::pipe2(flags)?; - Ok((pipe.0.into_raw_fd(), pipe.1.into_raw_fd())) -} - /// Register os signal handler. /// /// Must be called before calling [`block_ctrl_c()`](fn.block_ctrl_c.html) @@ -91,24 +45,21 @@ fn pipe2(flags: nix::fcntl::OFlag) -> nix::Result<(RawFd, RawFd)> { /// #[inline] pub unsafe fn init_os_handler(overwrite: bool) -> Result<(), Error> { - use nix::fcntl; use nix::sys::signal; - PIPE = pipe2(fcntl::OFlag::O_CLOEXEC)?; + #[cfg(feature = "unix_use_semaphore")] + init_sem()?; + #[cfg(not(feature = "unix_use_semaphore"))] + init_pipe()?; - let close_pipe = |e: nix::Error| -> Error { - // Try to close the pipes. close() should not fail, - // but if it does, there isn't much we can do - let _ = unistd::close(PIPE.1); - let _ = unistd::close(PIPE.0); + let cleanup = |e: nix::Error| -> Error { + #[cfg(feature = "unix_use_semaphore")] + cleanup_sem(); + #[cfg(not(feature = "unix_use_semaphore"))] + cleanup_pipe(); e }; - // Make sure we never block on write in the os handler. - if let Err(e) = fcntl::fcntl(PIPE.1, fcntl::FcntlArg::F_SETFL(fcntl::OFlag::O_NONBLOCK)) { - return Err(close_pipe(e)); - } - let handler = signal::SigHandler::Handler(os_handler); #[cfg(not(target_os = "nto"))] let new_action = signal::SigAction::new( @@ -123,11 +74,11 @@ pub unsafe fn init_os_handler(overwrite: bool) -> Result<(), Error> { let sigint_old = match signal::sigaction(signal::Signal::SIGINT, &new_action) { Ok(old) => old, - Err(e) => return Err(close_pipe(e)), + Err(e) => return Err(cleanup(e)), }; if !overwrite && sigint_old.handler() != signal::SigHandler::SigDfl { signal::sigaction(signal::Signal::SIGINT, &sigint_old).unwrap(); - return Err(close_pipe(nix::Error::EEXIST)); + return Err(cleanup(nix::Error::EEXIST)); } #[cfg(feature = "termination")] @@ -136,54 +87,27 @@ pub unsafe fn init_os_handler(overwrite: bool) -> Result<(), Error> { Ok(old) => old, Err(e) => { signal::sigaction(signal::Signal::SIGINT, &sigint_old).unwrap(); - return Err(close_pipe(e)); + return Err(cleanup(e)); } }; if !overwrite && sigterm_old.handler() != signal::SigHandler::SigDfl { signal::sigaction(signal::Signal::SIGINT, &sigint_old).unwrap(); signal::sigaction(signal::Signal::SIGTERM, &sigterm_old).unwrap(); - return Err(close_pipe(nix::Error::EEXIST)); + return Err(cleanup(nix::Error::EEXIST)); } let sighup_old = match signal::sigaction(signal::Signal::SIGHUP, &new_action) { Ok(old) => old, Err(e) => { signal::sigaction(signal::Signal::SIGINT, &sigint_old).unwrap(); signal::sigaction(signal::Signal::SIGTERM, &sigterm_old).unwrap(); - return Err(close_pipe(e)); + return Err(cleanup(e)); } }; if !overwrite && sighup_old.handler() != signal::SigHandler::SigDfl { signal::sigaction(signal::Signal::SIGINT, &sigint_old).unwrap(); signal::sigaction(signal::Signal::SIGTERM, &sigterm_old).unwrap(); signal::sigaction(signal::Signal::SIGHUP, &sighup_old).unwrap(); - return Err(close_pipe(nix::Error::EEXIST)); - } - } - - Ok(()) -} - -/// Blocks until a Ctrl-C signal is received. -/// -/// Must be called after calling [`init_os_handler()`](fn.init_os_handler.html). -/// -/// # Errors -/// Will return an error if a system error occurred. -/// -#[inline] -pub unsafe fn block_ctrl_c() -> Result<(), CtrlcError> { - use std::io; - let mut buf = [0u8]; - - // TODO: Can we safely convert the pipe fd into a std::io::Read - // with std::os::unix::io::FromRawFd, this would handle EINTR - // and everything for us. - loop { - match unistd::read(PIPE.0, &mut buf[..]) { - Ok(1) => break, - Ok(_) => return Err(CtrlcError::System(io::ErrorKind::UnexpectedEof.into())), - Err(nix::errno::Errno::EINTR) => {} - Err(e) => return Err(e.into()), + return Err(cleanup(nix::Error::EEXIST)); } } diff --git a/src/platform/unix/pipe/mod.rs b/src/platform/unix/pipe/mod.rs new file mode 100644 index 0000000..b5283fd --- /dev/null +++ b/src/platform/unix/pipe/mod.rs @@ -0,0 +1,112 @@ +use nix::unistd; +use std::os::fd::BorrowedFd; +use std::os::fd::IntoRawFd; +use std::os::unix::io::RawFd; + +pub type Error = nix::Error; + +use crate::error::Error as CtrlcError; + +static mut PIPE: (RawFd, RawFd) = (-1, -1); + +#[inline] +pub unsafe fn os_handler_pipe() -> () { + let fd = BorrowedFd::borrow_raw(PIPE.1); + let _ = unistd::write(fd, &[0u8]); +} + +// pipe2(2) is not available on macOS, iOS, AIX or Haiku, so we need to use pipe(2) and fcntl(2) +#[inline] +#[cfg(any( + target_os = "ios", + target_os = "macos", + target_os = "haiku", + target_os = "aix", + target_os = "nto", +))] +fn pipe2(flags: nix::fcntl::OFlag) -> nix::Result<(RawFd, RawFd)> { + use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag}; + + let pipe = unistd::pipe()?; + let pipe = (pipe.0.into_raw_fd(), pipe.1.into_raw_fd()); + + let mut res = Ok(0); + + if flags.contains(OFlag::O_CLOEXEC) { + res = res + .and_then(|_| fcntl(pipe.0, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))) + .and_then(|_| fcntl(pipe.1, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC))); + } + + if flags.contains(OFlag::O_NONBLOCK) { + res = res + .and_then(|_| fcntl(pipe.0, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))) + .and_then(|_| fcntl(pipe.1, FcntlArg::F_SETFL(OFlag::O_NONBLOCK))); + } + + match res { + Ok(_) => Ok(pipe), + Err(e) => { + let _ = unistd::close(pipe.0); + let _ = unistd::close(pipe.1); + Err(e) + } + } +} + +#[inline] +#[cfg(not(any( + target_os = "ios", + target_os = "macos", + target_os = "haiku", + target_os = "aix", + target_os = "nto", +)))] +fn pipe2(flags: nix::fcntl::OFlag) -> nix::Result<(RawFd, RawFd)> { + let pipe = unistd::pipe2(flags)?; + Ok((pipe.0.into_raw_fd(), pipe.1.into_raw_fd())) +} + +#[inline] +pub unsafe fn init_pipe() -> Result<(), Error> { + use nix::fcntl; + PIPE = pipe2(fcntl::OFlag::O_CLOEXEC)?; + if let Err(e) = fcntl::fcntl(PIPE.1, fcntl::FcntlArg::F_SETFL(fcntl::OFlag::O_NONBLOCK)) { + cleanup_pipe(); + return Err(e); + } + Ok(()) +} + +#[inline] +pub unsafe fn cleanup_pipe() -> () { + // Try to close the pipes. close() should not fail, + // but if it does, there isn't much we can do + let _ = unistd::close(PIPE.1); + let _ = unistd::close(PIPE.0); + PIPE = (-1, -1); +} + +/// Blocks until a Ctrl-C signal is received. +/// +/// Must be called after calling [`init_os_handler()`](fn.init_os_handler.html). +/// +/// # Errors +/// Will return an error if a system error occurred. +/// +#[inline] +pub unsafe fn block_ctrl_c() -> Result<(), CtrlcError> { + use std::io; + let mut buf = [0u8]; + + loop { + match unistd::read(PIPE.0, &mut buf[..]) { + Ok(1) => break, + Ok(_) => return Err(CtrlcError::System(io::ErrorKind::UnexpectedEof.into())), + Err(nix::errno::Errno::EINTR) => {} + Err(e) => return Err(e.into()), + } + } + + Ok(()) +} diff --git a/src/platform/unix/semaphore/mod.rs b/src/platform/unix/semaphore/mod.rs new file mode 100644 index 0000000..d0c0729 --- /dev/null +++ b/src/platform/unix/semaphore/mod.rs @@ -0,0 +1,55 @@ +use crate::error::Error as CtrlcError; +use libc::{sem_destroy, sem_init, sem_post, sem_t, sem_wait}; +use std::mem::MaybeUninit; + +static mut SEM: MaybeUninit = MaybeUninit::uninit(); + +pub type Error = nix::Error; + +#[inline] +pub unsafe fn init_sem() -> Result<(), Error> { + match sem_init(SEM.as_mut_ptr(), 0, 0) { + 0 => Ok(()), + _ => Err(Error::last()), + } +} + +#[inline] +pub unsafe fn cleanup_sem() { + let _ = sem_destroy(SEM.as_mut_ptr()); +} + +#[inline] +unsafe fn wait_sem() -> Result<(), Error> { + match sem_wait(SEM.as_mut_ptr()) { + 0 => Ok(()), + _ => Err(Error::last()), + } +} + +#[inline] +unsafe fn post_sem() -> Result<(), Error> { + match sem_post(SEM.as_mut_ptr()) { + 0 => Ok(()), + _ => Err(Error::last()), + } +} + +#[inline] +pub unsafe fn os_handler_sem() { + // Assuming this always succeeds. Can't really handle errors in any meaningful way. + let _ = post_sem(); +} + +#[inline] +pub unsafe fn block_ctrl_c() -> Result<(), CtrlcError> { + loop { + match wait_sem() { + Ok(()) => break, + Err(nix::errno::Errno::EINTR) => {} + Err(e) => return Err(e.into()), + } + } + + Ok(()) +}