mirror of
https://github.com/psmux/psmux.git
synced 2026-06-19 09:46:56 +08:00
The reboot/port-reuse diagnosis was hard because nothing recorded why a session was reaped or kept. Add a dedicated, env-gated logger so the next time the issue recurs the decisions are captured. - New `PSMUX_SESSION_DEBUG=1` -> ~/.psmux/session_debug.log logger in debug_log.rs, matching the existing facility (off by default, capped, timestamped). It appends instead of truncating, because cleanup runs in many short-lived CLI processes that would otherwise clobber the log. - Instrument stale-port cleanup and the identity probe: pre-boot reaps, auth-rejected (reused-port) reaps, connection-refused reaps, unparseable port files, and the kept "inconclusive"/"alive" verdicts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
280 lines
11 KiB
Rust
280 lines
11 KiB
Rust
//! Centralized debug logging for psmux.
|
|
//!
|
|
//! All logs write to `~/.psmux/` and are gated by environment variables.
|
|
//! Nothing is stored in the repo or source tree — only in the user's
|
|
//! home directory under `.psmux/`.
|
|
//!
|
|
//! ## Environment Variables
|
|
//!
|
|
//! | Variable | Log file | Description |
|
|
//! |------------------------|-----------------------------------|--------------------------------------|
|
|
//! | `PSMUX_CLIENT_DEBUG=1` | `~/.psmux/client_debug.log` | Client TUI rendering, draw, status |
|
|
//! | `PSMUX_STYLE_DEBUG=1` | `~/.psmux/style_debug.log` | Style/theme parsing, inline styles |/// | `PSMUX_INPUT_DEBUG=1` | `~/.psmux/input_debug.log` | Every crossterm event + console mode |//! | `PSMUX_MOUSE_DEBUG=1` | `~/.psmux/mouse_debug.log` | Mouse injection (existing) |
|
|
//! | `PSMUX_SSH_DEBUG=1` | `~/.psmux/ssh_input.log` | SSH input handling (existing) |
|
|
//! | `PSMUX_LATENCY_LOG=1` | `~/.psmux/latency.log` | Keypress-to-render latency (existing)|
|
|
//! | `PSMUX_SESSION_DEBUG=1`| `~/.psmux/session_debug.log` | Session-registry stale-port cleanup |
|
|
//!
|
|
//! All loggers are:
|
|
//! - **Off by default** — zero overhead when disabled (one atomic load per call)
|
|
//! - **Capped** — auto-stop after N entries to prevent disk fill
|
|
//! - **Thread-safe** — use `LazyLock<Mutex<Option<File>>>`
|
|
//! - **Timestamped** — `[HH:MM:SS.mmm]` prefix on every line
|
|
//! - **Truncated on startup** — fresh log each session (no stale data)
|
|
|
|
use std::io::Write;
|
|
use std::sync::{LazyLock, Mutex};
|
|
use std::sync::atomic::{AtomicU32, Ordering};
|
|
|
|
/// Resolve the psmux data directory (`~/.psmux/`).
|
|
fn psmux_dir() -> String {
|
|
let home = std::env::var("USERPROFILE")
|
|
.or_else(|_| std::env::var("HOME"))
|
|
.unwrap_or_default();
|
|
format!("{}/.psmux", home)
|
|
}
|
|
|
|
/// Open a log file in the psmux data directory, creating the directory if needed.
|
|
/// Returns `None` if the file cannot be created.
|
|
fn open_log(filename: &str) -> Option<std::fs::File> {
|
|
let dir = psmux_dir();
|
|
let _ = std::fs::create_dir_all(&dir);
|
|
std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.truncate(true) // fresh log each session
|
|
.write(true)
|
|
.open(format!("{}/{}", dir, filename))
|
|
.ok()
|
|
}
|
|
|
|
/// Check if an env var is set to a truthy value ("1" or "true").
|
|
fn env_enabled(var: &str) -> bool {
|
|
std::env::var(var).map_or(false, |v| v == "1" || v.eq_ignore_ascii_case("true"))
|
|
}
|
|
|
|
// ─── Client debug log ───────────────────────────────────────────────────────
|
|
|
|
/// Client debug log file, gated by `PSMUX_CLIENT_DEBUG=1`.
|
|
/// Covers: frame receive, JSON parse, draw lifecycle, status bar rendering.
|
|
static CLIENT_LOG: LazyLock<Mutex<Option<std::fs::File>>> = LazyLock::new(|| {
|
|
if !env_enabled("PSMUX_CLIENT_DEBUG") { return Mutex::new(None); }
|
|
Mutex::new(open_log("client_debug.log"))
|
|
});
|
|
|
|
static CLIENT_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
|
|
|
|
/// Maximum log entries per session to prevent disk fill.
|
|
const CLIENT_LOG_CAP: u32 = 5000;
|
|
|
|
/// Log a client debug message. No-op unless `PSMUX_CLIENT_DEBUG=1`.
|
|
///
|
|
/// # Arguments
|
|
/// * `component` — short tag like `"frame"`, `"draw"`, `"status"`, `"parse"`
|
|
/// * `msg` — the log message (should not contain newlines)
|
|
pub fn client_log(component: &str, msg: &str) {
|
|
let n = CLIENT_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
if n >= CLIENT_LOG_CAP {
|
|
if n == CLIENT_LOG_CAP {
|
|
// Log one final "cap reached" message
|
|
if let Ok(mut guard) = CLIENT_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][log] --- log cap reached ({} entries), further logging suppressed ---",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"), CLIENT_LOG_CAP);
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if let Ok(mut guard) = CLIENT_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][{}] {}",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"), component, msg);
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if client debug logging is active.
|
|
pub fn client_log_enabled() -> bool {
|
|
CLIENT_LOG.lock().ok().map_or(false, |g| g.is_some())
|
|
}
|
|
|
|
// ─── Style debug log ────────────────────────────────────────────────────────
|
|
|
|
/// Style/theme parsing debug log, gated by `PSMUX_STYLE_DEBUG=1`.
|
|
/// Covers: inline style parsing, unclosed directives, color mapping.
|
|
static STYLE_LOG: LazyLock<Mutex<Option<std::fs::File>>> = LazyLock::new(|| {
|
|
if !env_enabled("PSMUX_STYLE_DEBUG") { return Mutex::new(None); }
|
|
Mutex::new(open_log("style_debug.log"))
|
|
});
|
|
|
|
static STYLE_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
|
|
const STYLE_LOG_CAP: u32 = 2000;
|
|
|
|
/// Log a style debug message. No-op unless `PSMUX_STYLE_DEBUG=1`.
|
|
pub fn style_log(component: &str, msg: &str) {
|
|
let n = STYLE_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
if n >= STYLE_LOG_CAP {
|
|
if n == STYLE_LOG_CAP {
|
|
if let Ok(mut guard) = STYLE_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][log] --- log cap reached ---",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"));
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if let Ok(mut guard) = STYLE_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][{}] {}",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"), component, msg);
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if style debug logging is active.
|
|
pub fn style_log_enabled() -> bool {
|
|
STYLE_LOG.lock().ok().map_or(false, |g| g.is_some())
|
|
}
|
|
|
|
// ─── Input debug log ────────────────────────────────────────────────────────
|
|
|
|
/// Input event debug log, gated by `PSMUX_INPUT_DEBUG=1`.
|
|
/// Traces every crossterm event + console input mode at startup.
|
|
static INPUT_LOG: LazyLock<Mutex<Option<std::fs::File>>> = LazyLock::new(|| {
|
|
if !env_enabled("PSMUX_INPUT_DEBUG") { return Mutex::new(None); }
|
|
Mutex::new(open_log("input_debug.log"))
|
|
});
|
|
|
|
static INPUT_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
|
|
const INPUT_LOG_CAP: u32 = 10000;
|
|
|
|
/// Log an input debug message. No-op unless `PSMUX_INPUT_DEBUG=1`.
|
|
pub fn input_log(component: &str, msg: &str) {
|
|
let n = INPUT_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
if n >= INPUT_LOG_CAP {
|
|
if n == INPUT_LOG_CAP {
|
|
if let Ok(mut guard) = INPUT_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][log] --- log cap reached ---",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"));
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if let Ok(mut guard) = INPUT_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][{}] {}",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"), component, msg);
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if input debug logging is active.
|
|
pub fn input_log_enabled() -> bool {
|
|
INPUT_LOG.lock().ok().map_or(false, |g| g.is_some())
|
|
}
|
|
|
|
// ─── Server debug log ───────────────────────────────────────────────────────
|
|
|
|
/// Server debug log, gated by `PSMUX_SERVER_DEBUG=1`.
|
|
/// Traces active_idx changes, command dispatch, etc.
|
|
static SERVER_LOG: LazyLock<Mutex<Option<std::fs::File>>> = LazyLock::new(|| {
|
|
if !env_enabled("PSMUX_SERVER_DEBUG") { return Mutex::new(None); }
|
|
Mutex::new(open_log("server_debug.log"))
|
|
});
|
|
|
|
static SERVER_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
|
|
const SERVER_LOG_CAP: u32 = 10000;
|
|
|
|
/// Log a server debug message. No-op unless `PSMUX_SERVER_DEBUG=1`.
|
|
pub fn server_log(component: &str, msg: &str) {
|
|
let n = SERVER_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
if n >= SERVER_LOG_CAP {
|
|
if n == SERVER_LOG_CAP {
|
|
if let Ok(mut guard) = SERVER_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][log] --- log cap reached ---",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"));
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if let Ok(mut guard) = SERVER_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][{}] {}",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"), component, msg);
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if server debug logging is active.
|
|
pub fn server_log_enabled() -> bool {
|
|
SERVER_LOG.lock().ok().map_or(false, |g| g.is_some())
|
|
}
|
|
|
|
// ─── Session-registry debug log ─────────────────────────────────────────────
|
|
|
|
/// Session-registry lifecycle log, gated by `PSMUX_SESSION_DEBUG=1`.
|
|
/// Covers stale-port cleanup decisions: boot-time reaps, auth-rejected
|
|
/// (reused-port) reaps, unparseable port files, and live/inconclusive
|
|
/// probe verdicts — exactly the path that decided whether a session shows
|
|
/// up as a `(not responding)` zombie.
|
|
///
|
|
/// Unlike the other loggers this **appends** rather than truncates, because
|
|
/// registry cleanup runs in many short-lived CLI processes (every `psmux`
|
|
/// invocation calls it at startup); truncating on open would clobber the
|
|
/// log before it could be read.
|
|
static SESSION_LOG: LazyLock<Mutex<Option<std::fs::File>>> = LazyLock::new(|| {
|
|
if !env_enabled("PSMUX_SESSION_DEBUG") { return Mutex::new(None); }
|
|
let dir = psmux_dir();
|
|
let _ = std::fs::create_dir_all(&dir);
|
|
let f = std::fs::OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(format!("{}/session_debug.log", dir))
|
|
.ok();
|
|
Mutex::new(f)
|
|
});
|
|
|
|
static SESSION_LOG_COUNT: AtomicU32 = AtomicU32::new(0);
|
|
const SESSION_LOG_CAP: u32 = 5000;
|
|
|
|
/// Log a session-registry message. No-op unless `PSMUX_SESSION_DEBUG=1`.
|
|
pub fn session_log(component: &str, msg: &str) {
|
|
let n = SESSION_LOG_COUNT.fetch_add(1, Ordering::Relaxed);
|
|
if n >= SESSION_LOG_CAP {
|
|
if n == SESSION_LOG_CAP {
|
|
if let Ok(mut guard) = SESSION_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][log] --- log cap reached ---",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"));
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if let Ok(mut guard) = SESSION_LOG.lock() {
|
|
if let Some(ref mut f) = *guard {
|
|
let _ = writeln!(f, "[{}][{}] {}",
|
|
chrono::Local::now().format("%H:%M:%S%.3f"), component, msg);
|
|
let _ = f.flush();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if session-registry debug logging is active.
|
|
pub fn session_log_enabled() -> bool {
|
|
SESSION_LOG.lock().ok().map_or(false, |g| g.is_some())
|
|
}
|