(all):增加桌面程序

This commit is contained in:
Pine
2026-05-30 12:51:19 +08:00
parent 86abeeb5cc
commit 1790b496b3
39 changed files with 9167 additions and 185 deletions
+167
View File
@@ -0,0 +1,167 @@
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use tauri::Manager;
struct StreamlitProcess(Mutex<Option<Child>>);
/// 等待指定端口可连接,最多等待 timeout_secs 秒
fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
let start = std::time::Instant::now();
while start.elapsed() < std::time::Duration::from_secs(timeout_secs) {
if std::net::TcpStream::connect((host, port)).is_ok() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
/// 停止 Streamlit 子进程
///
/// - Unix: 先 SIGTERM(优雅退出),等待 3 秒无响应后 SIGKILL
/// - Windows: 直接 TerminateProcess
fn kill_streamlit(child: &mut Child) {
#[cfg(unix)]
{
// 先发 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;
}
log::warn!("Streamlit 进程未响应 SIGTERM,强制终止...");
}
// 强制终止
let _ = child.kill();
let _ = child.wait();
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let app = tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
let child = if cfg!(debug_assertions) {
// ── 开发模式 ── 使用系统安装的 streamlit ──
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()
} else {
// ── 生产模式 ── 使用 PyInstaller 打包的独立可执行文件 ──
let resource_dir = app
.path()
.resource_dir()
.expect("无法获取资源目录");
let exe_name = if cfg!(target_os = "windows") {
"geo_tool_app.exe"
} else {
"geo_tool_app"
};
let exe_path = resource_dir.join("_up_").join("dist").join(exe_name);
log::info!(
"[生产模式] 启动打包应用: {:?}",
exe_path
);
let mut cmd = Command::new(&exe_path);
cmd.current_dir(&resource_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Windows: 不显示控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000u32;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.spawn()
};
match child {
Ok(child) => {
log::info!("Streamlit 进程已启动 (PID: {})", child.id());
app.manage(StreamlitProcess(Mutex::new(Some(child))));
}
Err(e) => {
log::error!("启动 Streamlit 失败: {}", e);
eprintln!(
"错误:无法启动 Streamlit 服务。请确保已安装 streamlit。\n {}",
e
);
}
}
// 等待 Streamlit 就绪(最多 15 秒)
if !wait_for_port("127.0.0.1", 8501, 15) {
log::warn!("Streamlit 在 15 秒内未能就绪");
} else {
log::info!("Streamlit 已就绪 (127.0.0.1:8501)");
}
Ok(())
})
.build(tauri::generate_context!())
.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 进程已停止");
}
}
}
// 确保应用进程完全退出
std::process::exit(0);
}
});
}
+167
View File
@@ -0,0 +1,167 @@
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use tauri::Manager;
struct StreamlitProcess(Mutex<Option<Child>>);
/// 等待指定端口可连接,最多等待 timeout_secs 秒
fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
let start = std::time::Instant::now();
while start.elapsed() < std::time::Duration::from_secs(timeout_secs) {
if std::net::TcpStream::connect((host, port)).is_ok() {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
/// 停止 Streamlit 子进程
///
/// - Unix: 先 SIGTERM(优雅退出),等待 3 秒无响应后 SIGKILL
/// - Windows: 直接 TerminateProcess
fn kill_streamlit(child: &mut Child) {
#[cfg(unix)]
{
// 先发 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;
}
log::warn!("Streamlit 进程未响应 SIGTERM,强制终止...");
}
// 强制终止
let _ = child.kill();
let _ = child.wait();
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let app = tauri::Builder::default()
.setup(|app| {
if cfg!(debug_assertions) {
app.handle().plugin(
tauri_plugin_log::Builder::default()
.level(log::LevelFilter::Info)
.build(),
)?;
}
let child = if cfg!(debug_assertions) {
// ── 开发模式 ── 使用系统安装的 streamlit ──
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()
} else {
// ── 生产模式 ── 使用 PyInstaller 打包的独立可执行文件 ──
let resource_dir = app
.path()
.resource_dir()
.expect("无法获取资源目录");
let exe_name = if cfg!(target_os = "windows") {
"geo_tool_app.exe"
} else {
"geo_tool_app"
};
let exe_path = resource_dir.join(exe_name);
log::info!(
"[生产模式] 启动打包应用: {:?}",
exe_path
);
let mut cmd = Command::new(&exe_path);
cmd.current_dir(&resource_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
// Windows: 不显示控制台窗口
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000u32;
cmd.creation_flags(CREATE_NO_WINDOW);
}
cmd.spawn()
};
match child {
Ok(child) => {
log::info!("Streamlit 进程已启动 (PID: {})", child.id());
app.manage(StreamlitProcess(Mutex::new(Some(child))));
}
Err(e) => {
log::error!("启动 Streamlit 失败: {}", e);
eprintln!(
"错误:无法启动 Streamlit 服务。请确保已安装 streamlit。\n {}",
e
);
}
}
// 等待 Streamlit 就绪(最多 15 秒)
if !wait_for_port("127.0.0.1", 8501, 15) {
log::warn!("Streamlit 在 15 秒内未能就绪");
} else {
log::info!("Streamlit 已就绪 (127.0.0.1:8501)");
}
Ok(())
})
.build(tauri::generate_context!())
.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 进程已停止");
}
}
}
// 确保应用进程完全退出
std::process::exit(0);
}
});
}
+6
View File
@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}