feat(01-02): implement signal handling and logging stub
- SignalFlags struct wraps Arc<AtomicBool> for SIGHUP/SIGTERM - register_signals() registers SIGHUP and SIGTERM only (not SIGINT) - SIGINT deliberately excluded — handled via crossterm key events for double-press Ctrl+C - should_terminate() polls flag with Ordering::Relaxed for event loop use - init_logging() is a Phase 1 no-op stub with LIFE-03 documentation
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
mod config;
|
||||
mod signals;
|
||||
mod terminal;
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
//! UNIX signal handling for bbs-md.
|
||||
//!
|
||||
//! # Design
|
||||
//!
|
||||
//! SIGHUP and SIGTERM are registered via `signal-hook` to set a shared `AtomicBool` flag.
|
||||
//! The event loop polls this flag at the top of each iteration and exits cleanly when set.
|
||||
//!
|
||||
//! SIGINT (Ctrl+C) is deliberately NOT registered here. The double-press Ctrl+C state
|
||||
//! machine lives in the event loop and handles SIGINT via crossterm key events
|
||||
//! (`KeyCode::Char('c') + KeyModifiers::CONTROL`). Registering SIGINT via signal-hook
|
||||
//! AND crossterm would cause double-handling.
|
||||
//!
|
||||
//! # Stdout/Stderr Rule (LIFE-03)
|
||||
//!
|
||||
//! Phase 1: no file logging is needed yet. The rule is:
|
||||
//! do not write to stdout/stderr after terminal init except through ratatui's draw cycle
|
||||
//! or after calling `restore_terminal()`.
|
||||
//!
|
||||
//! When LIFE-03 becomes relevant in Phase 2+, replace `init_logging()` with file-based
|
||||
//! logging (e.g., `tracing` or `log` + `simplelog` writing to an append-mode file).
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use signal_hook::consts::signal::{SIGHUP, SIGTERM};
|
||||
use signal_hook::flag as signal_flag;
|
||||
|
||||
/// Shared flag handles for the registered UNIX signals.
|
||||
///
|
||||
/// Create with `register_signals()` and poll `should_terminate()` in the event loop.
|
||||
pub struct SignalFlags {
|
||||
/// Set to `true` by the OS when SIGHUP or SIGTERM is received.
|
||||
///
|
||||
/// SIGHUP fires when the controlling terminal closes (SSH disconnect).
|
||||
/// SIGTERM fires on `kill <pid>` or `systemctl stop`.
|
||||
pub terminate: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl SignalFlags {
|
||||
/// Returns `true` when a SIGHUP or SIGTERM has been received.
|
||||
///
|
||||
/// Uses `Ordering::Relaxed` — the flag only ever transitions from `false` to `true`,
|
||||
/// and a one-iteration delay before detecting the signal is acceptable.
|
||||
pub fn should_terminate(&self) -> bool {
|
||||
self.terminate.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
/// Register SIGHUP and SIGTERM to set a shared `AtomicBool` flag.
|
||||
///
|
||||
/// The returned `SignalFlags` must be kept alive for the duration of the process.
|
||||
/// Dropping it does not unregister the handlers, but the backing `Arc<AtomicBool>`
|
||||
/// will be dropped, which is safe (signal-hook holds its own `Arc` clone internally).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the OS refuses to register a signal handler (extremely rare).
|
||||
pub fn register_signals() -> std::io::Result<SignalFlags> {
|
||||
let terminate = Arc::new(AtomicBool::new(false));
|
||||
|
||||
// SIGHUP: SSH disconnect / terminal hangup
|
||||
signal_flag::register(SIGHUP, Arc::clone(&terminate))?;
|
||||
|
||||
// SIGTERM: systemd stop, `kill <pid>`, graceful OS shutdown
|
||||
signal_flag::register(SIGTERM, Arc::clone(&terminate))?;
|
||||
|
||||
// SIGINT (Ctrl+C) is NOT registered here — handled via crossterm key events
|
||||
// in the event loop to enable the double-press confirmation state machine.
|
||||
|
||||
Ok(SignalFlags { terminate })
|
||||
}
|
||||
|
||||
/// Initialize logging.
|
||||
///
|
||||
/// Phase 1 stub: this is a no-op. The rule for LIFE-03 is enforced by convention:
|
||||
/// after `init_terminal()` is called, nothing in the application writes to stdout
|
||||
/// or stderr directly. All output goes through the ratatui draw cycle.
|
||||
///
|
||||
/// The one exception is the panic hook in `terminal.rs`, which calls
|
||||
/// `restore_terminal()` first (reverting to normal terminal mode) before
|
||||
/// using `eprintln!()`.
|
||||
///
|
||||
/// When Phase 2+ introduces persistent logging, replace this with a file-based
|
||||
/// logger (e.g., `tracing_subscriber::fmt().with_writer(File::create(...))`).
|
||||
pub fn init_logging() {
|
||||
// No-op for Phase 1.
|
||||
}
|
||||
Reference in New Issue
Block a user