monochromatic-git-policy-cli 0.5.11

Repository-owned Git policy wrapper: forwards Git commands and checks commits against configured policies.
//! What:
//!  The service's last line when a signal ends it:
//! the service manager stops a unit with `SIGTERM`,
//! and a terminal sends
//!  `SIGINT` or `SIGHUP`;
//! the handler writes one line with the signal and the uptime,
//! then lets the signal end the process as
//!  it would have without a handler.
//! Why:
//!  The journal of the unit with the 17-second commit had no line for the first service's end,
//! and the manager's own lines
//!  about the unit cannot be relied on.
//! A signal gives the process no chance to log from ordinary code,
//! so the line is
//!  built and written with nothing but calls that are safe in a handler (`clock_gettime`,
//! `write`,
//! `signal`,
//! `raise`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! process.on('SIGTERM', () => { console.error(`cli-git-service: exiting reason=signal-15 uptime_ms=${uptime()}`); process.exit(143); });
//! ```

use super::service_timing::LOG_PREFIX;
use std::sync::atomic::{AtomicU64, Ordering};

/// The signals that end the service without a word otherwise.
pub const HANDLED: [libc::c_int; 3] = [libc::SIGTERM, libc::SIGINT, libc::SIGHUP];

/// The words before the signal number in the line.
const BEFORE_SIGNAL: &str = "exiting reason=signal-";

/// The words between the signal number and the uptime.
const BEFORE_UPTIME: &str = " uptime_ms=";

/// The room the line needs:
/// the prefix,
/// the words,
/// two numbers of at most twenty digits,
/// and the newline.
const LINE_CAPACITY: usize = 160;

/// The monotonic clock,
/// in nanoseconds,
/// when the handlers were installed;
/// zero before that.
static STARTED_NANOS: AtomicU64 = AtomicU64::new(0);

/// The monotonic clock in nanoseconds,
/// zero when the clock cannot be read.
fn monotonic_nanos() -> u64 {
    let mut now: libc::timespec = libc::timespec { tv_sec: 0, tv_nsec: 0 };
    // SAFETY: `now` is valid for the call to write one `timespec`, and `clock_gettime` is safe in a signal handler.
    let status: libc::c_int = unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &raw mut now) };
    if status != 0 {
        return 0;
    }
    return u64::try_from(now.tv_sec).unwrap_or(0).saturating_mul(1_000_000_000).saturating_add(u64::try_from(now.tv_nsec).unwrap_or(0));
}

/// What:
///  Appends the decimal digits of `value` to `line` at `used`,
/// and returns the new length.
/// Why:
///   Formatting in a signal handler may not allocate,
/// so the digits go into a fixed buffer from the least significant end and
///  are copied in order.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// line.write(String(value), used)
/// ```
fn push_number(line: &mut [u8; LINE_CAPACITY], used: usize, value: u64) -> usize {
    let mut digits: [u8; 20] = [0; 20];
    let mut count: usize = 0;
    let mut rest: u64 = value;
    loop {
        digits[count] = b'0' + u8::try_from(rest % 10).unwrap_or(0);
        count += 1;
        rest /= 10;
        if rest == 0 {
            break;
        }
    }
    let mut end: usize = used;
    for index in (0..count).rev() {
        line[end] = digits[index];
        end += 1;
    }
    return end;
}

/// Appends `text` to `line` at `used`,
/// and returns the new length.
fn push_text(line: &mut [u8; LINE_CAPACITY], used: usize, text: &str) -> usize {
    let bytes: &[u8] = text.as_bytes();
    line[used..used + bytes.len()].copy_from_slice(bytes);
    return used + bytes.len();
}

/// What:
///  The line the handler writes for `signal` after `uptime_nanos` of running,
/// and its length.
/// Why:
///   Kept apart from the handler so a test can read the bytes without sending a signal.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// `cli-git-service: exiting reason=signal-${signal} uptime_ms=${uptimeMs}\n`
/// ```
pub fn signal_line(signal: libc::c_int, uptime_nanos: u64) -> ([u8; LINE_CAPACITY], usize) {
    let mut line: [u8; LINE_CAPACITY] = [0; LINE_CAPACITY];
    let mut used: usize = push_text(&mut line, 0, LOG_PREFIX);
    used = push_text(&mut line, used, BEFORE_SIGNAL);
    used = push_number(&mut line, used, u64::try_from(signal).unwrap_or(0));
    used = push_text(&mut line, used, BEFORE_UPTIME);
    used = push_number(&mut line, used, uptime_nanos / 1_000_000);
    line[used] = b'\n';
    return (line, used + 1);
}

/// The handler:
/// one `write` of the line to standard error,
/// then the signal's default action.
extern "C" fn on_signal(signal: libc::c_int) {
    let started: u64 = STARTED_NANOS.load(Ordering::Relaxed);
    let uptime: u64 = monotonic_nanos().saturating_sub(started);
    let (line, length) = signal_line(signal, uptime);
    // SAFETY: `line` holds `length` initialized bytes, and `write`, `signal` and `raise` are safe in a signal handler. The
    //  write's result is dropped on purpose: a closed standard error is no reason to keep the process alive.
    unsafe {
        let _written = libc::write(libc::STDERR_FILENO, line.as_ptr().cast(), length);
        libc::signal(signal, libc::SIG_DFL);
        libc::raise(signal);
    }
}

/// What:
///  Installs the handler for the signals that end the service.
/// Why:
///   Without it the process dies from `SIGTERM` with no line in the journal.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// installExitLines()
/// ```
pub fn install() {
    STARTED_NANOS.store(monotonic_nanos(), Ordering::Relaxed);
    for signal in HANDLED {
        // SAFETY: `on_signal` has the signature a handler needs and only calls functions that are safe in a handler.
        unsafe {
            libc::signal(signal, on_signal as *const () as libc::sighandler_t);
        }
    }
}

/// The line's bytes.
#[cfg(test)]
#[path = "service_signal_tests.rs"]
mod tests;