Files
gitlore/src/core/shutdown.rs
Taylor Eernisse a855759bf8 fix: shutdown safety, CLI hardening, exit code collision
Shutdown signal improvements:
- Upgrade ShutdownSignal from Relaxed to Release/Acquire ordering.
  Relaxed was technically sufficient for a single flag but
  Release/Acquire is the textbook correct pattern and ensures
  visibility guarantees across threads without relying on x86 TSO.
- Add double Ctrl+C support to all three signal handlers (ingest,
  embed, sync). First Ctrl+C sets cooperative flag with user message;
  second Ctrl+C force-exits with code 130 (standard SIGINT convention).

CLI hardening:
- LORE_ROBOT env var now checks for truthy values (!empty, !="0",
  !="false") instead of mere existence. Setting LORE_ROBOT=0 or
  LORE_ROBOT=false no longer activates robot mode.
- Replace unreachable!() in color mode match with defensive warning
  and fallback to auto. Clap validates the values but defense in depth
  prevents panics if the value_parser is ever changed.
- Replace unreachable!() in completions shell match with proper error
  return for unsupported shells.

Exit code collision fix:
- ConfigNotFound was mapped to exit code 2 (error.rs:56) which
  collided with handle_clap_error() also using exit code 2 for parse
  errors. Agents calling lore --robot could not distinguish "bad
  arguments" from "missing config file."
- Restore ConfigNotFound to exit code 20 (its original dedicated code).
- Update robot-docs exit code table: code 2 = "Usage error", code 20 =
  "Config not found".

Build script:
- Track .git/refs/heads directory for Cargo rebuild triggers. Ensures
  GIT_HASH env var updates when branch refs change, not just HEAD.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-06 22:42:59 -05:00

64 lines
1.4 KiB
Rust

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
/// A cooperative cancellation token for graceful shutdown.
///
/// Clone-able and cheaply checkable from any thread or async task.
/// When `cancel()` is called (typically from a Ctrl+C signal handler),
/// all clones observe the cancellation via `is_cancelled()`.
#[derive(Clone)]
pub struct ShutdownSignal {
cancelled: Arc<AtomicBool>,
}
impl ShutdownSignal {
pub fn new() -> Self {
Self {
cancelled: Arc::new(AtomicBool::new(false)),
}
}
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Release);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Acquire)
}
}
impl Default for ShutdownSignal {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_starts_uncancelled() {
let signal = ShutdownSignal::new();
assert!(!signal.is_cancelled());
}
#[test]
fn cancel_sets_flag() {
let signal = ShutdownSignal::new();
signal.cancel();
assert!(signal.is_cancelled());
}
#[test]
fn clone_propagates_cancellation() {
let signal = ShutdownSignal::new();
let clone = signal.clone();
signal.cancel();
assert!(
clone.is_cancelled(),
"clone should see cancellation from original"
);
}
}