#![allow(unsafe_code)]
#[cfg(unix)]
use std::io::Write;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use anyhow::{Context, Result};
use crate::constants::{EXIT_BROKEN_PIPE, EXIT_SIGHUP, EXIT_SIGINT, EXIT_SIGTERM};
use crate::error::AtomwriteError;
pub const CANCEL_BLOCK_BYTES: u64 = crate::constants::CANCEL_BLOCK_BYTES;
pub const CANCEL_BLOCK_MATCHES: u64 = crate::constants::CANCEL_BLOCK_MATCHES;
const CANCEL_REASON: &str = "cancelled by signal or --timeout-secs";
static GLOBAL_SHUTDOWN: OnceLock<Arc<ShutdownSignal>> = OnceLock::new();
static SHUTDOWN_RAISED_AT: OnceLock<std::time::Instant> = OnceLock::new();
fn note_shutdown_raised() {
SHUTDOWN_RAISED_AT.get_or_init(std::time::Instant::now);
}
#[must_use]
pub fn shutdown_raised_ago() -> Option<std::time::Duration> {
SHUTDOWN_RAISED_AT.get().map(std::time::Instant::elapsed)
}
const EXIT_TIMEOUT: u8 = crate::constants::EXIT_TIMEOUT as u8;
pub struct ShutdownSignal {
flag: Arc<AtomicBool>,
count: AtomicU8,
signal_code: AtomicU8,
}
#[inline]
pub fn is_global_shutdown() -> bool {
GLOBAL_SHUTDOWN.get().is_some_and(|s| s.is_shutdown())
}
#[inline]
pub fn is_global_hard_shutdown() -> bool {
GLOBAL_SHUTDOWN.get().is_some_and(|s| s.is_hard_shutdown())
}
#[inline]
pub fn is_global_broken_pipe() -> bool {
GLOBAL_SHUTDOWN.get().is_some_and(|s| s.is_broken_pipe())
}
#[inline]
pub fn current_exit_code() -> u8 {
GLOBAL_SHUTDOWN
.get()
.map(|s| s.exit_code())
.unwrap_or(EXIT_SIGINT as u8)
}
#[inline]
pub fn current_cancel_reason() -> &'static str {
GLOBAL_SHUTDOWN
.get()
.map(|s| s.cancel_reason())
.unwrap_or(CANCEL_REASON)
}
include!("signal_cancel.inc.rs");
impl ShutdownSignal {
#[inline]
pub fn is_shutdown(&self) -> bool {
self.flag.load(Ordering::Acquire)
}
#[inline]
pub fn exit_code(&self) -> u8 {
match self.signal_code.load(Ordering::Acquire) {
c if c == EXIT_SIGTERM as u8
|| c == EXIT_SIGHUP as u8
|| c == EXIT_BROKEN_PIPE as u8
|| c == EXIT_TIMEOUT =>
{
c
}
_ => EXIT_SIGINT as u8,
}
}
#[inline]
pub fn is_hard_shutdown(&self) -> bool {
self.is_shutdown() && !self.is_broken_pipe()
}
#[inline]
pub fn is_broken_pipe(&self) -> bool {
self.signal_code.load(Ordering::Acquire) == EXIT_BROKEN_PIPE as u8
}
#[inline]
pub fn is_timeout(&self) -> bool {
self.signal_code.load(Ordering::Acquire) == EXIT_TIMEOUT
}
#[inline]
#[must_use]
pub fn signal_name(&self) -> Option<&'static str> {
match self.signal_code.load(Ordering::Acquire) {
c if c == EXIT_SIGINT as u8 => Some("SIGINT"),
c if c == EXIT_SIGTERM as u8 => Some("SIGTERM"),
c if c == EXIT_SIGHUP as u8 => Some("SIGHUP"),
c if c == EXIT_BROKEN_PIPE as u8 => Some("SIGPIPE"),
_ => None,
}
}
#[inline]
#[must_use]
pub fn cancel_reason(&self) -> &'static str {
if self.is_timeout() {
"global --timeout-secs deadline elapsed"
} else if self.is_broken_pipe() {
"the reader of stdout closed the pipe"
} else {
"cancelled by signal"
}
}
#[inline]
pub fn flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.flag)
}
#[cfg(unix)]
fn record_broken_pipe(&self) {
self.signal_code
.compare_exchange(
0,
EXIT_BROKEN_PIPE as u8,
Ordering::AcqRel,
Ordering::Acquire,
)
.ok();
self.flag.store(true, Ordering::Release);
}
fn record_signal(&self, code: u8) {
self.signal_code
.compare_exchange(0, code, Ordering::AcqRel, Ordering::Acquire)
.ok();
note_shutdown_raised();
self.flag.store(true, Ordering::Release);
let prev = self.count.fetch_add(1, Ordering::AcqRel);
if prev >= 1 {
#[cfg(unix)]
{
unsafe { libc::_exit(self.exit_code() as i32) };
}
#[cfg(not(unix))]
{
std::process::exit(self.exit_code() as i32);
}
}
}
}
include!("signal_handlers.inc.rs");
#[cfg(test)]
mod tests {
use super::*;
fn fresh() -> ShutdownSignal {
ShutdownSignal {
flag: Arc::new(AtomicBool::new(false)),
count: AtomicU8::new(0),
signal_code: AtomicU8::new(0),
}
}
#[cfg(unix)]
#[test]
fn broken_pipe_stops_new_work_but_not_the_write_in_flight() {
let signal = fresh();
assert!(!signal.is_shutdown() && !signal.is_hard_shutdown());
signal.record_broken_pipe();
assert!(signal.is_shutdown());
assert!(!signal.is_hard_shutdown());
assert!(signal.is_broken_pipe());
assert_eq!(signal.exit_code(), EXIT_BROKEN_PIPE as u8);
assert_eq!(signal.signal_name(), Some("SIGPIPE"));
}
#[cfg(unix)]
#[test]
fn repeated_broken_pipe_never_force_exits() {
let signal = fresh();
for _ in 0..5 {
signal.record_broken_pipe();
}
assert_eq!(signal.count.load(Ordering::Acquire), 0);
assert!(signal.is_broken_pipe());
}
#[test]
fn every_other_cancel_still_aborts_the_write_in_flight() {
for code in [
EXIT_SIGINT as u8,
EXIT_SIGTERM as u8,
EXIT_SIGHUP as u8,
EXIT_TIMEOUT,
] {
let signal = fresh();
signal.record_signal(code);
assert!(signal.is_hard_shutdown(), "code {code}");
assert!(!signal.is_broken_pipe(), "code {code}");
assert_eq!(signal.exit_code(), code);
}
}
#[cfg(unix)]
#[test]
fn a_cancel_recorded_first_keeps_its_own_code_over_a_broken_pipe() {
let signal = fresh();
signal.record_signal(EXIT_SIGTERM as u8);
signal.record_broken_pipe();
assert_eq!(signal.exit_code(), EXIT_SIGTERM as u8);
assert!(signal.is_hard_shutdown());
}
}