syd 3.41.7

rock-solid application kernel
Documentation
//
// Syd: rock-solid application kernel
// src/timer.rs: Per-thread SIGALRM timer
//
// Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{mem, time::Duration};

use nix::{
    errno::Errno,
    sys::{
        signal::Signal,
        timer::{Expiration, TimerSetTimeFlags},
    },
    time::ClockId,
    unistd::gettid,
};

use crate::fs::{block_signal, unblock_signal};

/// Per-thread SIGALRM timer that can be armed to interrupt a blocking syscall
/// in this *thread* after `keep_alive` nanoseconds.
///
/// Linux-only through SIGEV_THREAD_ID.
pub struct AlarmTimer {
    keep_alive: u64,
    timer_id: libc::timer_t,
}

impl AlarmTimer {
    /// Create a per-thread timer targeted at the *current* thread.
    /// `keep_alive` is in seconds; if 0, `start()` will be a no-op.
    pub fn from_seconds(keep_alive: u64) -> Result<Self, Errno> {
        Self::from_nanoseconds(keep_alive.checked_mul(1_000_000_000).ok_or(Errno::ERANGE)?)
    }

    /// Create a per-thread timer targeted at the *current* thread.
    /// `keep_alive` is in milliseconds; if 0, `start()` will be a no-op.
    pub fn from_milliseconds(keep_alive: u64) -> Result<Self, Errno> {
        Self::from_nanoseconds(keep_alive.checked_mul(1_000_000).ok_or(Errno::ERANGE)?)
    }

    /// Create a per-thread timer targeted at the *current* thread.
    /// `keep_alive` is in nanoseconds; if 0, `start()` will be a no-op.
    pub fn from_nanoseconds(keep_alive: u64) -> Result<Self, Errno> {
        // Route SIGALRM specifically to this thread (Linux extension).
        // nix does not implement SIGEV_THREAD_ID for musl yet so we use libc.
        const SIGEV_THREAD_ID: libc::c_int = 4;

        // SAFETY: sigevent is a plain C struct.
        let mut sev: libc::sigevent = unsafe { mem::zeroed() };
        sev.sigev_notify = SIGEV_THREAD_ID;
        sev.sigev_signo = libc::SIGALRM;
        sev.sigev_notify_thread_id = gettid().as_raw();

        // Monotonic clock; initially disarmed.
        let mut timer_id: mem::MaybeUninit<libc::timer_t> = mem::MaybeUninit::uninit();
        // SAFETY: In libc we trust, see above.
        Errno::result(unsafe {
            libc::timer_create(
                ClockId::CLOCK_MONOTONIC.as_raw(),
                std::ptr::addr_of_mut!(sev),
                timer_id.as_mut_ptr(),
            )
        })
        .map(|_| {
            Self {
                keep_alive,
                // SAFETY: timer_create initializes timer_id on success.
                timer_id: unsafe { timer_id.assume_init() },
            }
        })
    }

    /// Arm the timer for `self.keep_alive` seconds (if > 0) and make sure
    /// SIGALRM is unblocked *in this thread* so it can interrupt.
    /// If `keep_alive` == 0, does nothing and returns Ok(()).
    pub fn start(&mut self) -> Result<(), Errno> {
        if self.keep_alive == 0 {
            return Ok(());
        }

        // Allow delivery to this thread while we're blocking.
        unblock_signal(Signal::SIGALRM)?;

        // One-shot expiration at keep_alive nanoseconds.
        let dur = Duration::from_nanos(self.keep_alive);
        let exp = Expiration::OneShot(dur.into());
        self.set(exp, TimerSetTimeFlags::empty())
    }

    /// Disarm the timer and re-block SIGALRM in this thread.
    /// Idempotent: safe to call even if `start()` was a no-op.
    pub fn stop(&mut self) -> Result<(), Errno> {
        // Disarm: set a one-shot with zero interval and zero value.
        let exp = Expiration::OneShot(Duration::from_secs(0).into());
        self.set(exp, TimerSetTimeFlags::empty())?;

        // Restore per-thread policy: block SIGALRM again.
        block_signal(Signal::SIGALRM)
    }

    /// Change keep-alive seconds for future `start()` calls.
    pub fn set_keep_alive(&mut self, keep_alive: u64) {
        self.keep_alive = keep_alive;
    }

    /// Read current keep-alive.
    pub fn keep_alive(&self) -> u64 {
        self.keep_alive
    }

    fn set(&mut self, expiration: Expiration, flags: TimerSetTimeFlags) -> Result<(), Errno> {
        let timerspec: TimerSpec = expiration.into();

        // SAFETY: In libc we trust.
        Errno::result(unsafe {
            libc::timer_settime(
                self.timer_id,
                flags.bits(),
                timerspec.as_ref(),
                std::ptr::null_mut(),
            )
        })
        .map(drop)
    }
}

impl Drop for AlarmTimer {
    fn drop(&mut self) {
        #[expect(clippy::disallowed_methods)]
        // SAFETY: In libc we trust: We panic
        // here because something must be awfully
        // wrong if this call does not succeed.
        Errno::result(unsafe { libc::timer_delete(self.timer_id) })
            .map(drop)
            .expect("timer_delete")
    }
}

// Rest is borrowed from nix' src/sys/time.rs
// because this type is not exported :'(.
const fn zero_init_timespec() -> libc::timespec {
    // SAFETY: `std::mem::MaybeUninit::zeroed()` is not yet a const fn
    // (https://github.com/rust-lang/rust/issues/91850) so we will instead initialize an array of
    // the appropriate size to zero and then transmute it to a timespec value.
    unsafe { std::mem::transmute([0u8; std::mem::size_of::<libc::timespec>()]) }
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct TimerSpec(libc::itimerspec);

impl AsMut<libc::itimerspec> for TimerSpec {
    fn as_mut(&mut self) -> &mut libc::itimerspec {
        &mut self.0
    }
}

impl AsRef<libc::itimerspec> for TimerSpec {
    fn as_ref(&self) -> &libc::itimerspec {
        &self.0
    }
}

impl From<Expiration> for TimerSpec {
    fn from(expiration: Expiration) -> TimerSpec {
        match expiration {
            Expiration::OneShot(t) => TimerSpec(libc::itimerspec {
                it_interval: zero_init_timespec(),
                it_value: *t.as_ref(),
            }),
            Expiration::IntervalDelayed(start, interval) => TimerSpec(libc::itimerspec {
                it_interval: *interval.as_ref(),
                it_value: *start.as_ref(),
            }),
            Expiration::Interval(t) => TimerSpec(libc::itimerspec {
                it_interval: *t.as_ref(),
                it_value: *t.as_ref(),
            }),
        }
    }
}