diff --git a/src/bootstrap.rs b/src/bootstrap.rs index 27e590d..403f55f 100644 --- a/src/bootstrap.rs +++ b/src/bootstrap.rs @@ -1,4 +1,5 @@ pub fn initialize_process_runtime() { + crate::platform::disable_console_quick_edit(); let _ = rustls::crypto::ring::default_provider().install_default(); // Install a global panic hook so that any panic in a spawned task diff --git a/src/logging.rs b/src/logging.rs index 0332e71..a2613d1 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,6 +1,8 @@ use chrono::Utc; use chrono_tz::Tz; use std::path::PathBuf; +use std::sync::OnceLock; +use tracing_appender::non_blocking::WorkerGuard; use tracing_appender::rolling::{RollingFileAppender, Rotation}; use tracing_subscriber::{ EnvFilter, Layer, fmt, fmt::time::FormatTime, layer::SubscriberExt, util::SubscriberInitExt, @@ -8,6 +10,13 @@ use tracing_subscriber::{ use crate::config::LogFormat; +/// Keeps the non-blocking writer worker threads alive for the process lifetime. +/// +/// Dropping a [`WorkerGuard`] shuts down its worker, after which all log +/// writes are silently discarded. `init_logging` runs exactly once per +/// process, so the guards are parked in a process-wide static. +static NON_BLOCKING_GUARDS: OnceLock> = OnceLock::new(); + #[derive(Clone, Copy, Debug)] struct ConfiguredTimestamp { timezone: Tz, @@ -74,13 +83,24 @@ pub fn init_logging(timezone: Tz, log_format: LogFormat) { // Create file appender with daily rotation let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, "picobot.log"); + // Wrap both writers in non-blocking mode: the actual console/file writes + // happen on dedicated worker threads. On Windows the console can freeze + // (QuickEdit/mark mode, or a selection in Windows Terminal blocks every + // console write until Enter is pressed); with synchronous writers that + // would stall whichever tokio worker thread emits a log and cascade into + // a full gateway freeze. Here only the log worker thread blocks while + // the runtime keeps serving requests. + let (file_writer, file_guard) = tracing_appender::non_blocking(file_appender); + let (console_writer, console_guard) = tracing_appender::non_blocking(std::io::stdout()); + let _ = NON_BLOCKING_GUARDS.set(vec![file_guard, console_guard]); + // Build subscriber with both console and file output let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); // 文件层:根据 log_format 选择 text 或 json let file_layer = match log_format { LogFormat::Json => fmt::layer() - .with_writer(file_appender) + .with_writer(file_writer) .with_timer(ConfiguredTimestamp { timezone }) .with_ansi(false) .with_target(true) @@ -89,7 +109,7 @@ pub fn init_logging(timezone: Tz, log_format: LogFormat) { .json() .boxed(), LogFormat::Text => fmt::layer() - .with_writer(file_appender) + .with_writer(file_writer) .with_timer(ConfiguredTimestamp { timezone }) .with_ansi(false) .with_target(true) @@ -100,6 +120,7 @@ pub fn init_logging(timezone: Tz, log_format: LogFormat) { // 控制台层:始终文本格式 let console_layer = fmt::layer() + .with_writer(console_writer) .with_timer(ConfiguredTimestamp { timezone }) .with_target(true) .with_level(true); @@ -121,7 +142,13 @@ pub fn init_logging(timezone: Tz, log_format: LogFormat) { pub fn init_logging_console_only(timezone: Tz) { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + // See init_logging: non-blocking writes keep a frozen Windows console + // from stalling the runtime. + let (console_writer, console_guard) = tracing_appender::non_blocking(std::io::stdout()); + let _ = NON_BLOCKING_GUARDS.set(vec![console_guard]); + let console_layer = fmt::layer() + .with_writer(console_writer) .with_timer(ConfiguredTimestamp { timezone }) .with_target(true) .with_level(true); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index f6e87c7..55fb2e8 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -329,6 +329,99 @@ fn windows_is_process_waiting_on_stdin(pid: u32) -> Option { } } +/// Disable Windows console QuickEdit mode. +/// +/// With QuickEdit enabled (the default), clicking the console window enters +/// mark/selection mode and silently blocks every console write from this +/// process until the user presses Enter — making the gateway look frozen. +/// Disabling it keeps keyboard input working while preventing mouse-click +/// freezes. No-op on non-Windows platforms or when no console is attached. +/// +/// If stdin is redirected (so `GetStdHandle(STD_INPUT_HANDLE)` is not a +/// console handle), falls back to opening `CONIN$` to reach the attached +/// console directly. Note that a manual "Edit → Mark" from the title-bar +/// menu can still freeze console writes; non-blocking log writers in +/// `logging.rs` keep the runtime alive in that case. +pub fn disable_console_quick_edit() { + #[cfg(target_os = "windows")] + windows_disable_console_quick_edit(); +} + +#[cfg(target_os = "windows")] +fn windows_disable_console_quick_edit() { + const STD_INPUT_HANDLE: i32 = -10; + const ENABLE_QUICK_EDIT_MODE: u32 = 0x0040; + const ENABLE_EXTENDED_FLAGS: u32 = 0x0080; + const GENERIC_READ: u32 = 0x8000_0000; + const GENERIC_WRITE: u32 = 0x4000_0000; + const FILE_SHARE_READ: u32 = 0x0000_0001; + const FILE_SHARE_WRITE: u32 = 0x0000_0002; + const OPEN_EXISTING: u32 = 3; + const INVALID_HANDLE_VALUE: isize = -1; + + #[allow(non_snake_case)] + unsafe extern "system" { + fn GetStdHandle(n_std_handle: i32) -> isize; + fn GetConsoleMode(h_console_handle: isize, lp_mode: *mut u32) -> i32; + fn SetConsoleMode(h_console_handle: isize, dw_mode: u32) -> i32; + fn CreateFileW( + lp_file_name: *const u16, + dw_desired_access: u32, + dw_share_mode: u32, + lp_security_attributes: *const std::ffi::c_void, + dw_creation_disposition: u32, + dw_flags_and_attributes: u32, + h_template_file: isize, + ) -> isize; + fn CloseHandle(h_object: isize) -> i32; + } + + unsafe { + let mut mode: u32 = 0; + let mut opened_conin = false; + + let stdin = GetStdHandle(STD_INPUT_HANDLE); + let console = + if stdin != 0 && stdin != INVALID_HANDLE_VALUE && GetConsoleMode(stdin, &mut mode) != 0 + { + stdin + } else { + // stdin is redirected/closed but a console may still be attached + // (e.g. output shown in a window started via `start /B`). Open + // CONIN$ to reach the console input buffer directly. + let name: Vec = "CONIN$\0".encode_utf16().collect(); + let conin = CreateFileW( + name.as_ptr(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + std::ptr::null(), + OPEN_EXISTING, + 0, + 0, + ); + if conin == INVALID_HANDLE_VALUE || GetConsoleMode(conin, &mut mode) == 0 { + if conin != INVALID_HANDLE_VALUE { + let _ = CloseHandle(conin); + } + return; + } + opened_conin = true; + conin + }; + + if mode & ENABLE_QUICK_EDIT_MODE != 0 { + let new_mode = (mode & !ENABLE_QUICK_EDIT_MODE) | ENABLE_EXTENDED_FLAGS; + let _ = SetConsoleMode(console, new_mode); + } + + // The mode change is a property of the console itself, so the + // CONIN$ handle can be released immediately. + if opened_conin { + let _ = CloseHandle(console); + } + } +} + /// Get the user's home directory. /// /// Supports environment variable overrides for testing: