diff --git a/ostp-client/src/logging.rs b/ostp-client/src/logging.rs index b273e0d..95bfa74 100644 --- a/ostp-client/src/logging.rs +++ b/ostp-client/src/logging.rs @@ -3,6 +3,53 @@ use std::io::Write; use std::path::PathBuf; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +/// The single canonical log file for the whole core. Every process (CLI daemon, +/// GUI, TUN helper) and every subsystem (tracing, the core event logger, the +/// helper IPC, panics) writes here — no more per-binary / per-subsystem sprawl +/// (`ostp-cli.log` + `ostp-core.log` + `ostp-helper.log` + `ostp-crash.log`). +pub const LOG_FILE_NAME: &str = "ostp.log"; + +/// Absolute path to the shared log file, next to the running executable. +pub fn log_file_path() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|d| d.join(LOG_FILE_NAME))) + .unwrap_or_else(|| PathBuf::from(LOG_FILE_NAME)) +} + +/// True if this invocation is the long-running daemon (a client/server run), +/// as opposed to a one-shot subcommand (`gk`, `check`, `init`, `-V`, ...). +/// +/// Used to gate log truncation: only the daemon clears the log at startup, so a +/// one-shot command run while a daemon is live can never wipe the daemon's log. +/// A daemon invocation is simply one that carries none of the one-shot tokens +/// (`ostp`, `ostp run`, `ostp connect ` → daemon; everything else → one-shot). +pub fn invocation_is_daemon>(args: I) -> bool { + const ONE_SHOT: &[&str] = &[ + "gk", "generate-key", "check", "init", "setup", "links", "import", + "update", "migrate", "prober", "proxy-env", "proxy-env-clear", + "uninstall", "-V", "--version", "-h", "--help", "help", + ]; + !args + .into_iter() + .skip(1) // program name + .any(|a| ONE_SHOT.contains(&a.as_str())) +} + +/// Append a single timestamped line to the shared log file. Used by the manual +/// writers (core event logger, TUN helper IPC) so their output lands in the same +/// `ostp.log` as the tracing subscriber instead of a separate file. +pub fn append_line(msg: &str) { + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_file_path()) { + let _ = writeln!( + file, + "[{}] {}", + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + msg + ); + } +} + pub fn setup_panic_hook() { std::panic::set_hook(Box::new(|info| { let payload = info.payload(); @@ -16,7 +63,7 @@ pub fn setup_panic_hook() { let location = info.location().unwrap_or_else(|| std::panic::Location::caller()); let backtrace = std::backtrace::Backtrace::force_capture(); - + let crash_msg = format!( "[{}] PANIC at {}:{}\nMessage: {}\nBacktrace:\n{:?}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), @@ -29,19 +76,16 @@ pub fn setup_panic_hook() { eprintln!("{}", crash_msg); tracing::error!("{}", crash_msg); - let path = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|d| d.join("ostp-crash.log"))) - .unwrap_or_else(|| PathBuf::from("ostp-crash.log")); - - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + // Crashes land in the same shared log file (append — a crash must never + // truncate, and the tracing worker may already be dead so we write direct). + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(log_file_path()) { let _ = file.write_all(crash_msg.as_bytes()); let _ = file.write_all(b"\n===================================================\n"); } })); } -/// Initialises tracing and writes to `.log` next to the executable. +/// Initialises tracing and writes to the shared `ostp.log` next to the executable. /// /// The `level` parameter controls the minimum log level: /// - `"error"` — only errors @@ -51,7 +95,17 @@ pub fn setup_panic_hook() { /// - `"trace"` — all messages including very verbose internal state /// /// The environment variable `RUST_LOG` overrides this value if set. -pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option { +/// +/// `truncate`: clear the log at startup. Honoured **only on Windows** — Linux +/// servers keep their history (OS-rotated). Pass `true` only from the daemon's +/// own entrypoint; one-shot commands and child processes (the TUN helper) pass +/// `false` so they append instead of wiping a running daemon's log. +pub fn init_tracing( + level: &str, + app_name: &str, + version: &str, + truncate: bool, +) -> Option { // RUST_LOG overrides the config-derived level let env_filter = EnvFilter::try_from_default_env() .unwrap_or_else(|_| { @@ -66,12 +120,20 @@ pub fn init_tracing(level: &str, app_name: &str, version: &str) -> Option = OsStr::new(&format!("OSTP GUI crashed:\n\n{}\n\nSee ostp-gui.log for details.", msg)) + let msg_w: Vec = OsStr::new(&format!("OSTP GUI crashed:\n\n{}\n\nSee ostp.log for details.", msg)) .encode_wide().chain(Some(0)).collect(); let title_w: Vec = OsStr::new("OSTP GUI — Fatal Error").encode_wide().chain(Some(0)).collect(); #[link(name = "user32")] extern "system" { diff --git a/ostp-tun-helper/src/main.rs b/ostp-tun-helper/src/main.rs index 3691655..d8ae30e 100644 --- a/ostp-tun-helper/src/main.rs +++ b/ostp-tun-helper/src/main.rs @@ -14,10 +14,8 @@ use portable_atomic::Ordering; fn log_to_file(msg: &str) { let msg = msg.to_string(); tokio::task::spawn_blocking(move || { - let path = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|d| d.join("ostp-helper.log"))) - .unwrap_or_else(|| std::path::PathBuf::from("ostp-helper.log")); + // Same shared ostp.log as everything else — not a separate ostp-helper.log. + let path = ostp_client::logging::log_file_path(); if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(path) { let _ = writeln!(file, "[{}] {}", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), msg); } @@ -53,7 +51,10 @@ struct TunnelState { #[tokio::main] async fn main() -> Result<()> { ostp_client::logging::setup_panic_hook(); - let _log_guard = ostp_client::logging::init_tracing("info", "ostp-helper", env!("CARGO_PKG_VERSION")); + // The helper is a child of the GUI, which already truncated the shared log at + // its own startup — pass false so the helper APPENDS instead of wiping the + // GUI's session log. + let _log_guard = ostp_client::logging::init_tracing("info", "ostp-helper", env!("CARGO_PKG_VERSION"), false); if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { diff --git a/ostp/src/main.rs b/ostp/src/main.rs index a69eaed..1878533 100644 --- a/ostp/src/main.rs +++ b/ostp/src/main.rs @@ -251,7 +251,11 @@ async fn main() -> Result<()> { // where it does not apply. let _ = rlimit::increase_nofile_limit(1048576); ostp_client::logging::setup_panic_hook(); - let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION")); + // Clear the shared log at startup only when THIS invocation is the daemon — + // a one-shot command (`ostp gk`, `ostp check`, ...) must not wipe a running + // daemon's log. (Truncation itself is additionally Windows-only.) + let is_daemon = ostp_client::logging::invocation_is_daemon(std::env::args()); + let _log_guard = ostp_client::logging::init_tracing("info", "ostp-cli", env!("CARGO_PKG_VERSION"), is_daemon); let res = run_app().await; if let Err(e) = res {