win系统适配完成
@@ -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 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 7.1 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 5.3 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 9.2 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
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>
|
||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 953 B |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 152 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||