fix(gateway): 禁用 Windows 控制台 QuickEdit 并改为非阻塞日志,修复终端输出卡住需按回车的问题

This commit is contained in:
oudecheng 2026-08-17 20:16:44 +08:00
parent b0c24d64f0
commit accdfeed3b
3 changed files with 123 additions and 2 deletions

View File

@ -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

View File

@ -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<Vec<WorkerGuard>> = 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);

View File

@ -329,6 +329,99 @@ fn windows_is_process_waiting_on_stdin(pid: u32) -> Option<bool> {
}
}
/// 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<u16> = "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: