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>
This commit is contained in:
1
build.rs
1
build.rs
@@ -7,4 +7,5 @@ fn main() {
|
||||
.unwrap_or_default();
|
||||
println!("cargo:rustc-env=GIT_HASH={}", hash.trim());
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs/heads");
|
||||
}
|
||||
|
||||
@@ -76,7 +76,9 @@ impl Cli {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
args.iter()
|
||||
.any(|a| a == "--robot" || a == "-J" || a == "--json")
|
||||
|| std::env::var("LORE_ROBOT").is_ok()
|
||||
|| std::env::var("LORE_ROBOT")
|
||||
.ok()
|
||||
.is_some_and(|v| !v.is_empty() && v != "0" && v != "false")
|
||||
|| !std::io::stdout().is_terminal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ impl ErrorCode {
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
match self {
|
||||
Self::InternalError => 1,
|
||||
Self::ConfigNotFound => 2,
|
||||
Self::ConfigNotFound => 20,
|
||||
Self::ConfigInvalid => 3,
|
||||
Self::TokenNotSet => 4,
|
||||
Self::GitLabAuthFailed => 5,
|
||||
|
||||
@@ -19,11 +19,11 @@ impl ShutdownSignal {
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
self.cancelled.store(true, Ordering::Relaxed);
|
||||
self.cancelled.store(true, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.cancelled.load(Ordering::Relaxed)
|
||||
self.cancelled.load(Ordering::Acquire)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
17
src/main.rs
17
src/main.rs
@@ -134,7 +134,9 @@ async fn main() {
|
||||
"never" => console::set_colors_enabled(false),
|
||||
"always" => console::set_colors_enabled(true),
|
||||
"auto" => {}
|
||||
_ => unreachable!(),
|
||||
other => {
|
||||
eprintln!("Warning: unknown color mode '{}', using auto", other);
|
||||
}
|
||||
}
|
||||
|
||||
let quiet = cli.quiet;
|
||||
@@ -664,7 +666,10 @@ async fn handle_ingest(
|
||||
let signal_for_handler = signal.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
eprintln!("\nInterrupted, finishing current batch... (Ctrl+C again to force quit)");
|
||||
signal_for_handler.cancel();
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
std::process::exit(130);
|
||||
});
|
||||
|
||||
let ingest_result: std::result::Result<(), Box<dyn std::error::Error>> = async {
|
||||
@@ -1264,7 +1269,9 @@ fn handle_completions(shell: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
"zsh" => Shell::Zsh,
|
||||
"fish" => Shell::Fish,
|
||||
"powershell" => Shell::PowerShell,
|
||||
_ => unreachable!(),
|
||||
other => {
|
||||
return Err(format!("Unsupported shell: {other}").into());
|
||||
}
|
||||
};
|
||||
|
||||
let mut cmd = Cli::command();
|
||||
@@ -1522,7 +1529,10 @@ async fn handle_embed(
|
||||
let signal_for_handler = signal.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
eprintln!("\nInterrupted, finishing current batch... (Ctrl+C again to force quit)");
|
||||
signal_for_handler.cancel();
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
std::process::exit(130);
|
||||
});
|
||||
|
||||
let result = run_embed(&config, full, retry_failed, None, &signal).await?;
|
||||
@@ -1573,7 +1583,10 @@ async fn handle_sync_cmd(
|
||||
let signal_for_handler = signal.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
eprintln!("\nInterrupted, finishing current batch... (Ctrl+C again to force quit)");
|
||||
signal_for_handler.cancel();
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
std::process::exit(130);
|
||||
});
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
Reference in New Issue
Block a user