fix: 改为跨平台管理桌面端后端进程

This commit is contained in:
2026-05-21 18:19:19 +08:00
parent f2676a85b7
commit c2952d028f
4 changed files with 167 additions and 59 deletions
+7
View File
@@ -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:收紧系统设置页头部空间 ## 2026-05-21:收紧系统设置页头部空间
- 系统设置页移除了内容卡片内重复的“系统设置/更新时间”标题栏,保留外层统一页面标题,减少首屏垂直空间占用。 - 系统设置页移除了内容卡片内重复的“系统设置/更新时间”标题栏,保留外层统一页面标题,减少首屏垂直空间占用。
+96 -11
View File
@@ -239,6 +239,7 @@ version = "3.0.0"
dependencies = [ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"sysinfo",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-dialog", "tauri-plugin-dialog",
@@ -1853,6 +1854,15 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.2" version = "0.2.2"
@@ -2018,6 +2028,16 @@ dependencies = [
"objc2-core-foundation", "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]] [[package]]
name = "objc2-io-surface" name = "objc2-io-surface"
version = "0.3.2" version = "0.3.2"
@@ -3043,6 +3063,20 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "system-deps" name = "system-deps"
version = "6.2.2" version = "6.2.2"
@@ -3090,7 +3124,7 @@ dependencies = [
"tao-macros", "tao-macros",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
@@ -3161,7 +3195,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"window-vibrancy", "window-vibrancy",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@@ -3327,7 +3361,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
] ]
[[package]] [[package]]
@@ -3352,7 +3386,7 @@ dependencies = [
"url", "url",
"webkit2gtk", "webkit2gtk",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"wry", "wry",
] ]
@@ -4121,7 +4155,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
dependencies = [ dependencies = [
"webview2-com-macros", "webview2-com-macros",
"webview2-com-sys", "webview2-com-sys",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
@@ -4145,7 +4179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
dependencies = [ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
] ]
@@ -4201,11 +4235,23 @@ version = "0.61.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
dependencies = [ dependencies = [
"windows-collections", "windows-collections 0.2.0",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-future", "windows-future 0.2.1",
"windows-link 0.1.3", "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]] [[package]]
@@ -4217,6 +4263,15 @@ dependencies = [
"windows-core 0.61.2", "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]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -4251,7 +4306,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [ dependencies = [
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-link 0.1.3", "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]] [[package]]
@@ -4298,6 +4364,16 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"
@@ -4427,6 +4503,15 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "windows-version" name = "windows-version"
version = "0.1.7" version = "0.1.7"
@@ -4746,7 +4831,7 @@ dependencies = [
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
"webview2-com", "webview2-com",
"windows", "windows 0.61.3",
"windows-core 0.61.2", "windows-core 0.61.2",
"windows-version", "windows-version",
"x11-dl", "x11-dl",
+1
View File
@@ -11,6 +11,7 @@ tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
reqwest = { version = "0.13", default-features = false, features = ["stream"] } reqwest = { version = "0.13", default-features = false, features = ["stream"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
sysinfo = { version = "0.38.4", default-features = false, features = ["system"] }
tauri = { version = "2", features = [] } tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2" tauri-plugin-dialog = "2"
tauri-plugin-shell = "2" tauri-plugin-shell = "2"
+63 -48
View File
@@ -9,6 +9,7 @@ use std::sync::Mutex;
use std::thread; use std::thread;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use sysinfo::{Pid, ProcessesToUpdate, Signal, System};
use tauri::{Manager, Runtime}; use tauri::{Manager, Runtime};
use tauri_plugin_dialog::DialogExt; use tauri_plugin_dialog::DialogExt;
use tauri_plugin_shell::process::CommandChild; use tauri_plugin_shell::process::CommandChild;
@@ -17,12 +18,12 @@ use tauri_plugin_shell::ShellExt;
const SERVER_PORT: u16 = 9081; const SERVER_PORT: u16 = 9081;
const SERVER_ADDR: &str = "127.0.0.1:9081"; const SERVER_ADDR: &str = "127.0.0.1:9081";
const HTTP_ERROR_PREFIX: &str = "CAPAREPORT_HTTP_ERROR:"; const HTTP_ERROR_PREFIX: &str = "CAPAREPORT_HTTP_ERROR:";
#[cfg(target_os = "windows")] const SERVER_PROCESS_NAME: &str = "capareport-server";
const SERVER_PROCESS_NAME: &str = "capareport-server.exe"; const SERVER_PID_FILE: &str = "server.pid";
struct ServerProcess { struct ServerProcess {
child: CommandChild, child: CommandChild,
pid: u32, pid_file: PathBuf,
} }
struct ServerState(Mutex<Option<ServerProcess>>); struct ServerState(Mutex<Option<ServerProcess>>);
@@ -98,7 +99,9 @@ fn start_server<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(data_dir.join("logs"))?; fs::create_dir_all(data_dir.join("logs"))?;
copy_resource(app, "Configure.json", &data_dir)?; copy_resource(app, "Configure.json", &data_dir)?;
copy_resource(app, "ReportScript.sql", &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 port_arg = SERVER_PORT.to_string();
let (mut rx, child) = app let (mut rx, child) = app
@@ -110,17 +113,27 @@ fn start_server<R: Runtime>(app: &tauri::App<R>) -> Result<(), Box<dyn Error>> {
) )
.args(["--host", "127.0.0.1", "--port", &port_arg]) .args(["--host", "127.0.0.1", "--port", &port_arg])
.spawn()?; .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)) { if let Err(error) = wait_for_server(SERVER_ADDR, Duration::from_secs(60)) {
let _ = child.kill(); let _ = child.kill();
let _ = fs::remove_file(&pid_file);
return Err(error); return Err(error);
} }
tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} }); tauri::async_runtime::spawn(async move { while rx.recv().await.is_some() {} });
let pid = child.pid();
let state = app.state::<ServerState>(); let state = app.state::<ServerState>();
*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<dyn Error>> {
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(()) Ok(())
} }
@@ -146,66 +159,68 @@ fn health_check(socket_addr: &SocketAddr) -> Result<bool, Box<dyn Error>> {
Ok(response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200")) Ok(response.starts_with("HTTP/1.1 200") || response.starts_with("HTTP/1.0 200"))
} }
#[cfg(target_os = "windows")] fn stop_recorded_server(pid_file: &Path) {
fn stop_existing_server_on_port(port: u16) { let Ok(content) = fs::read_to_string(pid_file) else {
let Ok(output) = std::process::Command::new("netstat")
.args(["-ano", "-p", "tcp"])
.output()
else {
return; return;
}; };
let marker = format!(":{port}"); if let Ok(pid) = content.trim().parse::<u32>() {
let stdout = String::from_utf8_lossy(&output.stdout); if !terminate_server_process(pid) {
for line in stdout.lines() { let _ = fs::remove_file(pid_file);
if !line.contains(&marker) || !line.to_ascii_uppercase().contains("LISTENING") { return;
continue;
} }
wait_for_process_exit(pid, Duration::from_secs(5));
let Some(pid) = line.split_whitespace().last() else {
continue;
};
if pid.parse::<u32>().is_err() {
continue;
}
if !is_capareport_server_pid(pid) {
continue;
}
let _ = std::process::Command::new("taskkill")
.args(["/F", "/T", "/PID", pid])
.status();
} }
let _ = fs::remove_file(pid_file);
} }
#[cfg(target_os = "windows")] fn terminate_server_process(pid: u32) -> bool {
fn is_capareport_server_pid(pid: &str) -> bool { let mut system = System::new();
let Ok(output) = std::process::Command::new("tasklist") let pid = Pid::from_u32(pid);
.args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"]) system.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
.output()
else { let Some(process) = system.process(pid) else {
return false; return false;
}; };
String::from_utf8_lossy(&output.stdout) if !is_server_process_name(process.name()) {
.to_ascii_lowercase() return false;
.contains(SERVER_PROCESS_NAME) }
if process.kill_with(Signal::Kill) != Some(true) {
let _ = process.kill();
}
true
} }
#[cfg(not(target_os = "windows"))] fn wait_for_process_exit(pid: u32, timeout: Duration) {
fn stop_existing_server_on_port(_port: u16) {} 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<R: Runtime>(app: &tauri::AppHandle<R>) { fn stop_server<R: Runtime>(app: &tauri::AppHandle<R>) {
let state = app.state::<ServerState>(); let state = app.state::<ServerState>();
let child = state.0.lock().expect("server state lock poisoned").take(); let child = state.0.lock().expect("server state lock poisoned").take();
if let Some(process) = child { 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 _ = process.child.kill();
let _ = fs::remove_file(process.pid_file);
} }
} }