diff --git a/src/gui.rs b/src/gui.rs index d9e7940..56be3d1 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -2619,7 +2619,23 @@ impl AcceleratorApp { } fn ensure_launcher_viewport(&self, ctx: &egui::Context) { + // Re-issuing these commands every frame makes winit emit resize events + // every frame, which marks the UI dirty and keeps egui repainting at + // full speed. Only send them when the window size actually drifted. let size = launcher_window_size(); + let current = ctx.input(|input| { + input + .viewport() + .inner_rect + .map(|rect| rect.size()) + }); + let drifted = match current { + Some(current) => (current - size).length() > 1.0, + None => true, + }; + if !drifted { + return; + } ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size)); ctx.send_viewport_cmd(egui::ViewportCommand::MinInnerSize(size)); ctx.send_viewport_cmd(egui::ViewportCommand::MaxInnerSize(size)); diff --git a/src/platform.rs b/src/platform.rs index 17aee8c..0e5f0bb 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -257,31 +257,59 @@ pub fn is_process_running(pid: u32) -> bool { let kill_probe = Command::new("kill").args(["-0", &pid.to_string()]).output(); - match kill_probe { - Ok(output) if output.status.success() => { - // Process exists — but check if it's a zombie (already dead, just - // waiting for the parent to reap it). Treat zombies as "not running" - // so that stop/cleanup can proceed without waiting for a kill timeout. - is_unix_zombie(pid) - } + let exists = match kill_probe { + Ok(output) if output.status.success() => true, Ok(output) => { let stderr = String::from_utf8_lossy(&output.stderr); if stderr.contains("Operation not permitted") { - // Can't signal the process but it exists; still check zombie state. - return !is_unix_zombie(pid); + // Can't signal the process but it exists. + true + } else { + Command::new("ps") + .args(["-p", &pid.to_string(), "-o", "pid="]) + .output() + .ok() + .map(|ps| { + ps.status.success() + && !String::from_utf8_lossy(&ps.stdout).trim().is_empty() + }) + .unwrap_or(false) } - - Command::new("ps") - .args(["-p", &pid.to_string(), "-o", "pid="]) - .output() - .ok() - .map(|ps| { - ps.status.success() && !String::from_utf8_lossy(&ps.stdout).trim().is_empty() - }) - .unwrap_or(false) } Err(_) => false, + }; + if !exists { + return false; } + + // Process exists — but check if it's a zombie (already dead, just + // waiting for the parent to reap it). Treat zombies as "not running" + // so that stop/cleanup can proceed without waiting for a kill timeout. + if is_unix_zombie(pid) { + return false; + } + + unix_process_is_ours(pid) +} + +/// Verify the pid actually belongs to this application. After a reboot a +/// stale pid file often points at a recycled pid owned by an unrelated +/// process (system daemons get the low pids first), which previously made +/// `state::refresh` resurrect a phantom "running" state and the GUI skip +/// the autostart. +#[cfg(unix)] +fn unix_process_is_ours(pid: u32) -> bool { + Command::new("ps") + .args(["-p", &pid.to_string(), "-o", "comm="]) + .output() + .ok() + .map(|out| { + // Linux truncates comm to 15 chars ("linuxdo-acceler"); match the + // shared prefix so macOS full paths and Linux comms both pass. + let comm = String::from_utf8_lossy(&out.stdout).to_ascii_lowercase(); + comm.contains("linuxdo-acceler") + }) + .unwrap_or(false) } /// Check if a process is a zombie (defunct) on Unix. diff --git a/src/runtime_log.rs b/src/runtime_log.rs index 1731d38..e5d5b8c 100644 --- a/src/runtime_log.rs +++ b/src/runtime_log.rs @@ -1,5 +1,5 @@ use std::fs::{self, OpenOptions}; -use std::io::Write; +use std::io::{Read, Seek, SeekFrom, Write}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{Context, Result, anyhow}; @@ -7,12 +7,24 @@ use anyhow::{Context, Result, anyhow}; use crate::paths::AppPaths; use crate::platform::sync_user_ownership; +/// Rotate operations.log before it grows unbounded (a few months of proxy +/// traffic was observed reaching >100MB, which the UI then re-read on every +/// refresh). Keep one previous generation next to the active file. +const MAX_LOG_SIZE: u64 = 8 * 1024 * 1024; + pub(crate) fn append(paths: &AppPaths, level: &str, action: &str, message: &str) -> Result<()> { if let Some(parent) = paths.runtime_log_path.parent() { fs::create_dir_all(parent) .with_context(|| format!("failed to create {}", parent.display()))?; } + if let Ok(metadata) = fs::metadata(&paths.runtime_log_path) { + if metadata.len() >= MAX_LOG_SIZE { + let rotated = paths.runtime_log_path.with_extension("log.1"); + let _ = fs::rename(&paths.runtime_log_path, rotated); + } + } + let mut file = OpenOptions::new() .create(true) .append(true) @@ -39,10 +51,29 @@ pub(crate) fn read_recent_lines(paths: &AppPaths, max_lines: usize) -> Result100MB) on every UI refresh burns noticeable + // CPU. Seek to the tail and parse only a fixed window instead. + const TAIL_WINDOW: u64 = 64 * 1024; + + let mut file = fs::File::open(&paths.runtime_log_path) + .with_context(|| format!("failed to open {}", paths.runtime_log_path.display()))?; + let size = file.metadata()?.len(); + let window = size.min(TAIL_WINDOW); + let start = size - window; + + file.seek(SeekFrom::Start(start))?; + let mut buf = vec![0u8; window as usize]; + file.read_exact(&mut buf) + .with_context(|| format!("failed to read tail of {}", paths.runtime_log_path.display()))?; + + let text = String::from_utf8_lossy(&buf); + // When the window starts mid-file the first line is likely a partial + // entry left over at the seek boundary, so skip it. + let skip_first = usize::from(start > 0); + let mut lines = text .lines() + .skip(skip_first) .map(str::trim) .filter(|line| !line.is_empty()) .map(ToOwned::to_owned) diff --git a/src/service.rs b/src/service.rs index b34b8b0..3c998e5 100644 --- a/src/service.rs +++ b/src/service.rs @@ -278,6 +278,18 @@ pub fn run_privileged_helper(config_path: Option) -> Result<()> { "特权辅助守护进程启动,开始监听 socket", ); + // After a reboot the previous session's state files survive. This helper + // has not started any proxy yet, so a leftover "running" state is always + // stale; reconcile it before the GUI reads it, otherwise the GUI's + // autostart sees a phantom running state and never starts acceleration. + if let Err(error) = state::refresh(&paths) { + log_error( + &paths, + "privileged-helper", + &format!("清理残留服务状态失败: {error:#}"), + ); + } + // Shared state: holds the running proxy's shutdown channel and join handle. let proxy_state: Arc>> = Arc::new(Mutex::new(None)); let socket = helper_ipc::socket_path();