win系统适配完成

This commit is contained in:
PineWin
2026-05-30 15:39:42 +08:00
parent b8debfcd46
commit 304f4b54ff
67 changed files with 323 additions and 111 deletions
+19
View File
@@ -0,0 +1,19 @@
{
"permissions": {
"allow": [
"Bash(cargo clean *)",
"Bash(cargo build *)",
"Bash(awk '{print $NF}')",
"Bash(xxd \"D:/MyCode/ChouJuGEO/build_scripts/tauri.py\")",
"Bash(cargo tauri *)",
"Bash(cargo install *)",
"Read(//c/Users/Administrator/AppData/Local/tauri/WixTools314/**)",
"Bash(powershell *)",
"Bash(dism /online /get-featureinfo /featurename:NetFx3)",
"Bash(reg query *)",
"Bash(cmd *)",
"Bash(\"C:/Users/Administrator/AppData/Local/tauri/WixTools314/light.exe\")",
"Bash(C:/Users/Administrator/AppData/Local/tauri/WixTools314/light.exe *)"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 953 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+190 -75
View File
@@ -2,7 +2,30 @@ use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use tauri::Manager;
struct StreamlitProcess(Mutex<Option<Child>>);
/// 包装 Child,在 Drop 时强制终止整个进程树(防止 panic/crash 时泄漏)
struct ProcessGuard {
child: Option<Child>,
}
impl ProcessGuard {
fn new(child: Child) -> Self {
Self { child: Some(child) }
}
fn take(&mut self) -> Option<Child> {
self.child.take()
}
}
impl Drop for ProcessGuard {
fn drop(&mut self) {
if let Some(ref mut child) = self.child.take() {
kill_process_tree(child);
}
}
}
struct StreamlitProcess(Mutex<Option<ProcessGuard>>);
/// 等待指定端口可连接,最多等待 timeout_secs 秒
fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
@@ -16,31 +39,135 @@ fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
false
}
/// 停止 Streamlit 子进程
/// 终止整个进程树(跨平台)
///
/// - Unix: 先 SIGTERM(优雅退出),等待 3 秒无响应后 SIGKILL
/// - Windows: 直接 TerminateProcess
fn kill_streamlit(child: &mut Child) {
#[cfg(unix)]
/// - Windows: taskkill /F /T → 终止目标进程及其所有子进程
/// - Unix: SIGTERM → 等待 3s → SIGKILL
fn kill_process_tree(child: &mut Child) {
#[cfg(windows)]
{
// 先发 SIGTERM 请求优雅退出
let _ = Command::new("kill")
.arg("-TERM")
.arg(child.id().to_string())
.spawn();
std::thread::sleep(std::time::Duration::from_secs(3));
// 尝试 wait(如果已退出则返回 Ok(Some(status))
if let Ok(Some(_)) = child.try_wait() {
log::info!("Streamlit 进程已优雅退出");
return;
let pid = child.id();
// taskkill /T 递归终止整个进程树,确保 Streamlit worker 等孙进程也被清理
match Command::new("taskkill")
.args(["/F", "/T", "/PID", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(mut tk) => {
let _ = tk.wait();
log::info!("[清理] taskkill /T /PID {} 完成", pid);
}
Err(e) => {
log::warn!("[清理] taskkill 调用失败: {},回退到直接 kill", e);
let _ = child.kill();
}
}
log::warn!("Streamlit 进程未响应 SIGTERM,强制终止...");
let _ = child.wait();
log::info!("[清理] Streamlit 进程树已终止 (Windows, PID: {})", pid);
}
// 强制终止
let _ = child.kill();
let _ = child.wait();
#[cfg(unix)]
{
let pid = child.id();
unsafe {
extern "C" {
fn kill(pid: i32, sig: i32) -> i32;
}
kill(pid as i32, 15); // SIGTERM
}
std::thread::sleep(std::time::Duration::from_secs(3));
if let Ok(Some(_)) = child.try_wait() {
log::info!("[清理] Streamlit 进程已优雅退出 (SIGTERM, PID: {})", pid);
return;
}
log::warn!("[清理] Streamlit 进程未响应 SIGTERM,强制终止...");
let _ = child.kill();
let _ = child.wait();
log::info!("[清理] Streamlit 进程已强制终止 (Unix, PID: {})", pid);
}
#[cfg(not(any(windows, unix)))]
{
let _ = child.kill();
let _ = child.wait();
}
}
/// 从 StreamlitProcess 状态中取出进程并终止
fn cleanup_streamlit(state: &StreamlitProcess) {
if let Ok(mut guard) = state.0.lock() {
if let Some(ref mut pg) = *guard {
if let Some(ref mut child) = pg.take() {
kill_process_tree(child);
}
}
// 清除空壳,避免 Drop 时二次处理
*guard = None;
}
}
/// 在 debug 模式下,尝试多种方式启动 Streamlit
fn spawn_streamlit_debug(project_root: &std::path::Path) -> Result<Child, String> {
let script_path = project_root.join("geo_tool.py");
let script = script_path.to_str().unwrap_or("geo_tool.py");
let args = [
"run",
script,
"--server.port",
"8501",
"--server.address",
"127.0.0.1",
"--server.headless",
"true",
"--browser.gatherUsageStats",
"false",
"--logger.level",
"error",
];
log::info!("[开发模式] 启动 Streamlit: {:?}", script_path);
let result = Command::new("streamlit")
.args(&args)
.current_dir(project_root)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn();
if let Ok(child) = result {
log::info!("[开发模式] 通过 'streamlit' 启动成功 (PID: {})", child.id());
return Ok(child);
}
log::warn!("[开发模式] 'streamlit' 命令未找到,尝试 python -m streamlit ...");
Command::new("python")
.arg("-m")
.arg("streamlit")
.args(&args)
.current_dir(project_root)
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.map(|child| {
log::info!("[开发模式] 通过 'python -m streamlit' 启动成功 (PID: {})", child.id());
child
})
.map_err(|e| {
let msg = format!(
"无法启动 Streamlit。已尝试:\n \
1) streamlit run ...\n \
2) python -m streamlit run ...\n \
请确认已安装 streamlit: pip install streamlit\n \
最终错误: {}",
e
);
log::error!("[开发模式] {}", msg);
msg
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -55,39 +182,12 @@ pub fn run() {
)?;
}
let child = if cfg!(debug_assertions) {
// ── 开发模式 ── 使用系统安装的 streamlit ──
let child_result = if cfg!(debug_assertions) {
let manifest_dir =
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let project_root = manifest_dir.parent().unwrap_or(&manifest_dir);
let script_path = project_root.join("geo_tool.py");
log::info!(
"[开发模式] 启动 Streamlit: {:?}",
script_path
);
Command::new("streamlit")
.args([
"run",
script_path.to_str().unwrap_or("geo_tool.py"),
"--server.port",
"8501",
"--server.address",
"127.0.0.1",
"--server.headless",
"true",
"--browser.gatherUsageStats",
"false",
"--logger.level",
"error",
])
.current_dir(project_root)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
spawn_streamlit_debug(project_root)
} else {
// ── 生产模式 ── 使用 PyInstaller 打包的独立可执行文件 ──
let resource_dir = app
.path()
.resource_dir()
@@ -98,12 +198,26 @@ pub fn run() {
} else {
"geo_tool_app"
};
let exe_path = resource_dir.join("_up_").join("dist_python").join(exe_name);
log::info!(
"[生产模式] 启动打包应用: {:?}",
exe_path
);
let exe_path_legacy =
resource_dir.join("_up_").join("dist_python").join(exe_name);
let exe_path_direct = resource_dir.join("dist_python").join(exe_name);
let exe_path = if exe_path_legacy.exists() {
exe_path_legacy
} else if exe_path_direct.exists() {
exe_path_direct
} else {
let msg = format!(
"未找到 Streamlit 打包应用:\n 尝试1: {:?}\n 尝试2: {:?}",
exe_path_legacy, exe_path_direct
);
log::error!("[生产模式] {}", msg);
eprintln!("错误:{}", msg);
return Ok(());
};
log::info!("[生产模式] 启动打包应用: {:?}", exe_path);
let mut cmd = Command::new(&exe_path);
cmd.current_dir(&resource_dir)
@@ -111,7 +225,6 @@ pub fn run() {
.stdout(Stdio::null())
.stderr(Stdio::null());
// Windows: 不显示控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
@@ -119,26 +232,25 @@ pub fn run() {
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.spawn()
cmd.spawn().map_err(|e| {
format!("无法启动打包应用 {:?}: {}", exe_path, e)
})
};
match child {
match child_result {
Ok(child) => {
log::info!("Streamlit 进程已启动 (PID: {})", child.id());
app.manage(StreamlitProcess(Mutex::new(Some(child))));
let guard = ProcessGuard::new(child);
app.manage(StreamlitProcess(Mutex::new(Some(guard))));
}
Err(e) => {
log::error!("启动 Streamlit 失败: {}", e);
eprintln!(
"错误:无法启动 Streamlit 服务。请确保已安装 streamlit。\n {}",
e
);
eprintln!("错误:{}", e);
}
}
// 等待 Streamlit 就绪(最多 15 秒)
if !wait_for_port("127.0.0.1", 8501, 15) {
log::warn!("Streamlit 在 15 秒内未能就绪");
log::warn!("Streamlit 在 15 秒内未能就绪,请检查是否已安装 streamlit");
} else {
log::info!("Streamlit 已就绪 (127.0.0.1:8501)");
}
@@ -149,19 +261,22 @@ pub fn run() {
.expect("error while building tauri application");
app.run(|app_handle, event| {
// 窗口关闭 → 停止子进程 → 退出应用
if let tauri::RunEvent::ExitRequested { .. } = event {
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
if let Ok(mut guard) = state.0.lock() {
if let Some(ref mut child) = *guard {
log::info!("正在停止 Streamlit 进程...");
kill_streamlit(child);
log::info!("Streamlit 进程已停止");
match event {
tauri::RunEvent::WindowEvent { event, .. } => {
if let tauri::WindowEvent::CloseRequested { .. } = event {
log::info!("窗口关闭请求,正在清理子进程...");
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
cleanup_streamlit(&*state);
}
}
}
// 确保应用进程完全退出
std::process::exit(0);
tauri::RunEvent::Exit => {
log::info!("应用退出,执行最终清理...");
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
cleanup_streamlit(&*state);
}
}
_ => {}
}
});
}
+8 -4
View File
@@ -4,8 +4,7 @@
"version": "0.1.0",
"identifier": "com.pinesound.geo-tool",
"build": {
"frontendDist": "../frontend",
"devUrl": "http://localhost:8501"
"frontendDist": "../frontend"
},
"app": {
"windows": [
@@ -35,7 +34,12 @@
"icons/icon.ico"
],
"resources": [
"../dist_python/geo_tool_app"
]
"../dist_python/geo_tool_app.exe"
],
"windows": {
"wix": {
"language": "zh-CN"
}
}
}
}