diff --git a/docs/project_context.md b/docs/project_context.md index 9336152..90ac46f 100644 --- a/docs/project_context.md +++ b/docs/project_context.md @@ -1,4 +1,11 @@ # 项目上下文记录 +## 2026-05-21:桌面端 sidecar 进程改为跨平台管理 + +- Tauri 桌面端启动/关闭后端 sidecar 不再调用 Windows `netstat`、`tasklist`、`taskkill` 等控制台命令,避免启动和退出时闪过 DOS 窗口。 +- 桌面端启动 sidecar 后会在应用数据目录写入 `server.pid`;下次启动前读取该 pid,并通过 `sysinfo` 跨平台确认进程名为 `capareport-server` 后再终止残留进程,pid 被复用为其他程序时不会误杀。 +- 关闭桌面端时只使用 Tauri shell 的 `CommandChild.kill()` 停止当前 sidecar,并删除 `server.pid`;若 `9081` 被非本程序占用,则启动前直接报端口占用错误。 +- 新增 Rust 依赖 `sysinfo`,仅启用 `system` feature;已验证 `npm run build` 和带临时 sidecar 占位文件的 `cargo check --manifest-path src-tauri\Cargo.toml` 通过,验证产物已清理。 + ## 2026-05-21:收紧系统设置页头部空间 - 系统设置页移除了内容卡片内重复的“系统设置/更新时间”标题栏,保留外层统一页面标题,减少首屏垂直空间占用。 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9d8621d..a1aa819 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -239,6 +239,7 @@ version = "3.0.0" dependencies = [ "reqwest", "serde", + "sysinfo", "tauri", "tauri-build", "tauri-plugin-dialog", @@ -1853,6 +1854,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -2018,6 +2028,16 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "objc2-io-surface" version = "0.3.2" @@ -3043,6 +3063,20 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "sysinfo" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.62.2", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3090,7 +3124,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3161,7 +3195,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -3327,7 +3361,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -3352,7 +3386,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -4121,7 +4155,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -4145,7 +4179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -4201,11 +4235,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -4217,6 +4263,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4251,7 +4306,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -4298,6 +4364,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -4427,6 +4503,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -4746,7 +4831,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fee9745..6bb2921 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -11,6 +11,7 @@ tauri-build = { version = "2", features = [] } [dependencies] reqwest = { version = "0.13", default-features = false, features = ["stream"] } serde = { version = "1", features = ["derive"] } +sysinfo = { version = "0.38.4", default-features = false, features = ["system"] } tauri = { version = "2", features = [] } tauri-plugin-dialog = "2" tauri-plugin-shell = "2" diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ff0ac2d..34dd098 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -9,6 +9,7 @@ use std::sync::Mutex; use std::thread; use std::time::{Duration, Instant}; +use sysinfo::{Pid, ProcessesToUpdate, Signal, System}; use tauri::{Manager, Runtime}; use tauri_plugin_dialog::DialogExt; use tauri_plugin_shell::process::CommandChild; @@ -17,12 +18,12 @@ use tauri_plugin_shell::ShellExt; const SERVER_PORT: u16 = 9081; const SERVER_ADDR: &str = "127.0.0.1:9081"; const HTTP_ERROR_PREFIX: &str = "CAPAREPORT_HTTP_ERROR:"; -#[cfg(target_os = "windows")] -const SERVER_PROCESS_NAME: &str = "capareport-server.exe"; +const SERVER_PROCESS_NAME: &str = "capareport-server"; +const SERVER_PID_FILE: &str = "server.pid"; struct ServerProcess { child: CommandChild, - pid: u32, + pid_file: PathBuf, } struct ServerState(Mutex>); @@ -98,7 +99,9 @@ fn start_server(app: &tauri::App) -> Result<(), Box> { fs::create_dir_all(data_dir.join("logs"))?; copy_resource(app, "Configure.json", &data_dir)?; copy_resource(app, "ReportScript.sql", &data_dir)?; - stop_existing_server_on_port(SERVER_PORT); + let pid_file = data_dir.join(SERVER_PID_FILE); + stop_recorded_server(&pid_file); + ensure_server_port_available(SERVER_ADDR)?; let port_arg = SERVER_PORT.to_string(); let (mut rx, child) = app @@ -110,17 +113,27 @@ fn start_server(app: &tauri::App) -> Result<(), Box> { ) .args(["--host", "127.0.0.1", "--port", &port_arg]) .spawn()?; + let pid = child.pid(); + fs::write(&pid_file, pid.to_string())?; if let Err(error) = wait_for_server(SERVER_ADDR, Duration::from_secs(60)) { let _ = child.kill(); + let _ = fs::remove_file(&pid_file); return Err(error); } tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); - let pid = child.pid(); let state = app.state::(); - *state.0.lock().expect("server state lock poisoned") = Some(ServerProcess { child, pid }); + *state.0.lock().expect("server state lock poisoned") = Some(ServerProcess { child, pid_file }); + Ok(()) +} + +fn ensure_server_port_available(addr: &str) -> Result<(), Box> { + let socket_addr: SocketAddr = addr.parse()?; + if TcpStream::connect_timeout(&socket_addr, Duration::from_millis(300)).is_ok() { + return Err(format!("Port {SERVER_PORT} is already in use").into()); + } Ok(()) } @@ -146,66 +159,68 @@ fn health_check(socket_addr: &SocketAddr) -> Result> { Ok(response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200")) } -#[cfg(target_os = "windows")] -fn stop_existing_server_on_port(port: u16) { - let Ok(output) = std::process::Command::new("netstat") - .args(["-ano", "-p", "tcp"]) - .output() - else { +fn stop_recorded_server(pid_file: &Path) { + let Ok(content) = fs::read_to_string(pid_file) else { return; }; - let marker = format!(":{port}"); - let stdout = String::from_utf8_lossy(&output.stdout); - for line in stdout.lines() { - if !line.contains(&marker) || !line.to_ascii_uppercase().contains("LISTENING") { - continue; + if let Ok(pid) = content.trim().parse::() { + if !terminate_server_process(pid) { + let _ = fs::remove_file(pid_file); + return; } - - let Some(pid) = line.split_whitespace().last() else { - continue; - }; - if pid.parse::().is_err() { - continue; - } - if !is_capareport_server_pid(pid) { - continue; - } - - let _ = std::process::Command::new("taskkill") - .args(["/F", "/T", "/PID", pid]) - .status(); + wait_for_process_exit(pid, Duration::from_secs(5)); } + let _ = fs::remove_file(pid_file); } -#[cfg(target_os = "windows")] -fn is_capareport_server_pid(pid: &str) -> bool { - let Ok(output) = std::process::Command::new("tasklist") - .args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"]) - .output() - else { +fn terminate_server_process(pid: u32) -> bool { + let mut system = System::new(); + let pid = Pid::from_u32(pid); + system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + + let Some(process) = system.process(pid) else { return false; }; - String::from_utf8_lossy(&output.stdout) - .to_ascii_lowercase() - .contains(SERVER_PROCESS_NAME) + if !is_server_process_name(process.name()) { + return false; + } + + if process.kill_with(Signal::Kill) != Some(true) { + let _ = process.kill(); + } + true } -#[cfg(not(target_os = "windows"))] -fn stop_existing_server_on_port(_port: u16) {} +fn wait_for_process_exit(pid: u32, timeout: Duration) { + let started_at = Instant::now(); + while started_at.elapsed() < timeout { + if !is_process_running(pid) { + return; + } + thread::sleep(Duration::from_millis(100)); + } +} + +fn is_process_running(pid: u32) -> bool { + let mut system = System::new(); + let pid = Pid::from_u32(pid); + system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); + system.process(pid).is_some() +} + +fn is_server_process_name(name: &std::ffi::OsStr) -> bool { + let normalized = name.to_string_lossy().to_ascii_lowercase(); + normalized == SERVER_PROCESS_NAME || normalized == format!("{SERVER_PROCESS_NAME}.exe") +} fn stop_server(app: &tauri::AppHandle) { let state = app.state::(); let child = state.0.lock().expect("server state lock poisoned").take(); if let Some(process) = child { - #[cfg(target_os = "windows")] - { - let _ = std::process::Command::new("taskkill") - .args(["/F", "/T", "/PID", &process.pid.to_string()]) - .status(); - } let _ = process.child.kill(); + let _ = fs::remove_file(process.pid_file); } }