win系统适配完成
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(cargo check *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 7.1 KiB |
@@ -27,35 +27,93 @@
|
|||||||
}
|
}
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
p { color: #4a5568; font-size: 14px; }
|
p { color: #4a5568; font-size: 14px; }
|
||||||
|
.status { color: #718096; font-size: 13px; margin-top: 12px; }
|
||||||
|
.retry-btn {
|
||||||
|
display: none;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 8px 24px;
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.retry-btn:hover { background: #1d4ed8; }
|
||||||
|
.retry-btn.show { display: inline-block; }
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
// 等待 streamlit 服务就绪后跳转
|
|
||||||
(function() {
|
(function() {
|
||||||
|
var STREAMLIT_URL = 'http://localhost:8501';
|
||||||
var retries = 0;
|
var retries = 0;
|
||||||
var maxRetries = 50;
|
var maxRetries = 120; // 最多等待 120 次(约 60 秒)
|
||||||
|
var interval = 500; // 每 500ms 检查一次
|
||||||
|
var statusEl = null;
|
||||||
|
var retryBtn = null;
|
||||||
|
|
||||||
|
function showStatus(msg, isError) {
|
||||||
|
if (!statusEl) {
|
||||||
|
statusEl = document.createElement('p');
|
||||||
|
statusEl.className = 'status';
|
||||||
|
document.querySelector('.loader').appendChild(statusEl);
|
||||||
|
}
|
||||||
|
statusEl.textContent = msg;
|
||||||
|
if (isError) {
|
||||||
|
statusEl.style.color = '#dc2626';
|
||||||
|
} else {
|
||||||
|
statusEl.style.color = '#718096';
|
||||||
|
}
|
||||||
|
if (!retryBtn) {
|
||||||
|
retryBtn = document.createElement('button');
|
||||||
|
retryBtn.className = 'retry-btn';
|
||||||
|
retryBtn.textContent = '手动重试';
|
||||||
|
retryBtn.onclick = function() {
|
||||||
|
retries = 0;
|
||||||
|
retryBtn.classList.remove('show');
|
||||||
|
showStatus('正在重新连接...', false);
|
||||||
|
setTimeout(check, 500);
|
||||||
|
};
|
||||||
|
document.querySelector('.loader').appendChild(retryBtn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function check() {
|
function check() {
|
||||||
var xhr = new XMLHttpRequest();
|
// 使用 fetch no-cors 模式检测端口是否可达
|
||||||
xhr.open('GET', 'http://127.0.0.1:8501', true);
|
// no-cors: 请求成功则 resolve,连接失败则 reject
|
||||||
xhr.timeout = 2000;
|
var controller = new AbortController();
|
||||||
xhr.onload = function() {
|
var timeoutId = setTimeout(function() { controller.abort(); }, 3000);
|
||||||
window.location.href = 'http://127.0.0.1:8501';
|
|
||||||
};
|
fetch(STREAMLIT_URL, { mode: 'no-cors', signal: controller.signal })
|
||||||
xhr.onerror = function() {
|
.then(function() {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
// 服务可达 → 跳转
|
||||||
|
showStatus('服务已就绪,正在进入...');
|
||||||
|
window.location.replace(STREAMLIT_URL);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
retries++;
|
retries++;
|
||||||
if (retries < maxRetries) {
|
if (retries < maxRetries) {
|
||||||
setTimeout(check, 500);
|
showStatus('正在启动服务... (' + retries + '/' + maxRetries + ')');
|
||||||
|
setTimeout(check, interval);
|
||||||
|
} else {
|
||||||
|
showStatus('服务启动超时。请确认 Streamlit 已在 :8501 端口运行,或点击下方按钮重试。', true);
|
||||||
|
if (retryBtn) retryBtn.classList.add('show');
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
xhr.ontimeout = function() {
|
|
||||||
retries++;
|
|
||||||
if (retries < maxRetries) {
|
|
||||||
setTimeout(check, 500);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
xhr.send();
|
// 等待 DOM 就绪后再开始检测
|
||||||
|
function start() {
|
||||||
|
showStatus('正在启动服务...');
|
||||||
|
// 首次延迟 1.5 秒,给服务一点启动时间
|
||||||
|
setTimeout(check, 1500);
|
||||||
|
}
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', start);
|
||||||
|
} else {
|
||||||
|
start();
|
||||||
}
|
}
|
||||||
// 等待 500ms 后开始轮询(给 streamlit 启动时间)
|
|
||||||
setTimeout(check, 500);
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -128,7 +128,6 @@ def save_cfg_to_file(cfg: dict) -> None:
|
|||||||
- API Keys + 品牌信息 → .streamlit/secrets.toml
|
- API Keys + 品牌信息 → .streamlit/secrets.toml
|
||||||
"""
|
"""
|
||||||
import tomllib
|
import tomllib
|
||||||
import tomli_w # type: ignore
|
|
||||||
|
|
||||||
# ── 1. 非敏感配置 → config.json ──
|
# ── 1. 非敏感配置 → config.json ──
|
||||||
config_path = Path(__file__).with_name("config.json")
|
config_path = Path(__file__).with_name("config.json")
|
||||||
@@ -203,6 +202,7 @@ def save_cfg_to_file(cfg: dict) -> None:
|
|||||||
new_app_config["temperature"] = cfg["temperature"]
|
new_app_config["temperature"] = cfg["temperature"]
|
||||||
|
|
||||||
# 写出 TOML
|
# 写出 TOML
|
||||||
|
import tomli_w
|
||||||
secrets_path.parent.mkdir(parents=True, exist_ok=True)
|
secrets_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
with secrets_path.open("wb") as f:
|
with secrets_path.open("wb") as f:
|
||||||
content = {"api_keys": new_api_keys, "app_config": new_app_config}
|
content = {"api_keys": new_api_keys, "app_config": new_app_config}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ datas = [
|
|||||||
("config.json", "."),
|
("config.json", "."),
|
||||||
("geo_data.db", "."),
|
("geo_data.db", "."),
|
||||||
(".streamlit", ".streamlit"),
|
(".streamlit", ".streamlit"),
|
||||||
("knowledge_base", "knowledge_base"),
|
|
||||||
("modules", "modules"),
|
("modules", "modules"),
|
||||||
("platform_sync", "platform_sync"),
|
("platform_sync", "platform_sync"),
|
||||||
]
|
]
|
||||||
@@ -150,9 +149,10 @@ hiddenimports = [
|
|||||||
"watchdog",
|
"watchdog",
|
||||||
"sqlite3",
|
"sqlite3",
|
||||||
"importlib.metadata",
|
"importlib.metadata",
|
||||||
"json",
|
"tomli_w",
|
||||||
"csv",
|
# ---- Streamlit 内部依赖(动态子模块多,显式声明)----
|
||||||
"hashlib",
|
"altair",
|
||||||
|
"pydeck",
|
||||||
]
|
]
|
||||||
|
|
||||||
# ── 排除不必要的包(减小体积) ──────────────────────────────────
|
# ── 排除不必要的包(减小体积) ──────────────────────────────────
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ requires = ["setuptools"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["build_scripts*"]
|
include = ["scripts*"]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
@@ -35,6 +35,6 @@ dev = [
|
|||||||
package = true
|
package = true
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
python-build = "build_scripts.build:main"
|
python-build = "scripts.build:main"
|
||||||
tauri-run = "build_scripts.runtauri:main"
|
tauri-run = "scripts.runtauri:main"
|
||||||
tauri-build = "build_scripts.build:build_tauri"
|
tauri-build = "scripts.build:build_tauri"
|
||||||
|
|||||||
@@ -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 std::sync::Mutex;
|
||||||
use tauri::Manager;
|
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 秒
|
/// 等待指定端口可连接,最多等待 timeout_secs 秒
|
||||||
fn wait_for_port(host: &str, port: u16, timeout_secs: u64) -> bool {
|
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
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 停止 Streamlit 子进程
|
/// 终止整个进程树(跨平台)
|
||||||
///
|
///
|
||||||
/// - Unix: 先 SIGTERM(优雅退出),等待 3 秒无响应后 SIGKILL
|
/// - Windows: taskkill /F /T → 终止目标进程及其所有子进程
|
||||||
/// - Windows: 直接 TerminateProcess
|
/// - Unix: SIGTERM → 等待 3s → SIGKILL
|
||||||
fn kill_streamlit(child: &mut Child) {
|
fn kill_process_tree(child: &mut Child) {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = child.wait();
|
||||||
|
log::info!("[清理] Streamlit 进程树已终止 (Windows, PID: {})", pid);
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
// 先发 SIGTERM 请求优雅退出
|
let pid = child.id();
|
||||||
let _ = Command::new("kill")
|
unsafe {
|
||||||
.arg("-TERM")
|
extern "C" {
|
||||||
.arg(child.id().to_string())
|
fn kill(pid: i32, sig: i32) -> i32;
|
||||||
.spawn();
|
}
|
||||||
|
kill(pid as i32, 15); // SIGTERM
|
||||||
|
}
|
||||||
std::thread::sleep(std::time::Duration::from_secs(3));
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||||
|
|
||||||
// 尝试 wait(如果已退出则返回 Ok(Some(status)))
|
|
||||||
if let Ok(Some(_)) = child.try_wait() {
|
if let Ok(Some(_)) = child.try_wait() {
|
||||||
log::info!("Streamlit 进程已优雅退出");
|
log::info!("[清理] Streamlit 进程已优雅退出 (SIGTERM, PID: {})", pid);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::warn!("Streamlit 进程未响应 SIGTERM,强制终止...");
|
log::warn!("[清理] Streamlit 进程未响应 SIGTERM,强制终止...");
|
||||||
}
|
|
||||||
|
|
||||||
// 强制终止
|
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -55,39 +182,12 @@ pub fn run() {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let child = if cfg!(debug_assertions) {
|
let child_result = if cfg!(debug_assertions) {
|
||||||
// ── 开发模式 ── 使用系统安装的 streamlit ──
|
|
||||||
let manifest_dir =
|
let manifest_dir =
|
||||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
let project_root = manifest_dir.parent().unwrap_or(&manifest_dir);
|
let project_root = manifest_dir.parent().unwrap_or(&manifest_dir);
|
||||||
let script_path = project_root.join("geo_tool.py");
|
spawn_streamlit_debug(project_root)
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
// ── 生产模式 ── 使用 PyInstaller 打包的独立可执行文件 ──
|
|
||||||
let resource_dir = app
|
let resource_dir = app
|
||||||
.path()
|
.path()
|
||||||
.resource_dir()
|
.resource_dir()
|
||||||
@@ -98,12 +198,26 @@ pub fn run() {
|
|||||||
} else {
|
} else {
|
||||||
"geo_tool_app"
|
"geo_tool_app"
|
||||||
};
|
};
|
||||||
let exe_path = resource_dir.join("_up_").join("dist_python").join(exe_name);
|
|
||||||
|
|
||||||
log::info!(
|
let exe_path_legacy =
|
||||||
"[生产模式] 启动打包应用: {:?}",
|
resource_dir.join("_up_").join("dist_python").join(exe_name);
|
||||||
exe_path
|
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);
|
let mut cmd = Command::new(&exe_path);
|
||||||
cmd.current_dir(&resource_dir)
|
cmd.current_dir(&resource_dir)
|
||||||
@@ -111,7 +225,6 @@ pub fn run() {
|
|||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.stderr(Stdio::null());
|
.stderr(Stdio::null());
|
||||||
|
|
||||||
// Windows: 不显示控制台窗口
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
@@ -119,26 +232,25 @@ pub fn run() {
|
|||||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.spawn()
|
cmd.spawn().map_err(|e| {
|
||||||
|
format!("无法启动打包应用 {:?}: {}", exe_path, e)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
match child {
|
match child_result {
|
||||||
Ok(child) => {
|
Ok(child) => {
|
||||||
log::info!("Streamlit 进程已启动 (PID: {})", child.id());
|
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) => {
|
Err(e) => {
|
||||||
log::error!("启动 Streamlit 失败: {}", e);
|
log::error!("启动 Streamlit 失败: {}", e);
|
||||||
eprintln!(
|
eprintln!("错误:{}", e);
|
||||||
"错误:无法启动 Streamlit 服务。请确保已安装 streamlit。\n {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待 Streamlit 就绪(最多 15 秒)
|
|
||||||
if !wait_for_port("127.0.0.1", 8501, 15) {
|
if !wait_for_port("127.0.0.1", 8501, 15) {
|
||||||
log::warn!("Streamlit 在 15 秒内未能就绪");
|
log::warn!("Streamlit 在 15 秒内未能就绪,请检查是否已安装 streamlit");
|
||||||
} else {
|
} else {
|
||||||
log::info!("Streamlit 已就绪 (127.0.0.1:8501)");
|
log::info!("Streamlit 已就绪 (127.0.0.1:8501)");
|
||||||
}
|
}
|
||||||
@@ -149,19 +261,22 @@ pub fn run() {
|
|||||||
.expect("error while building tauri application");
|
.expect("error while building tauri application");
|
||||||
|
|
||||||
app.run(|app_handle, event| {
|
app.run(|app_handle, event| {
|
||||||
// 窗口关闭 → 停止子进程 → 退出应用
|
match event {
|
||||||
if let tauri::RunEvent::ExitRequested { .. } = event {
|
tauri::RunEvent::WindowEvent { event, .. } => {
|
||||||
|
if let tauri::WindowEvent::CloseRequested { .. } = event {
|
||||||
|
log::info!("窗口关闭请求,正在清理子进程...");
|
||||||
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
|
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
|
||||||
if let Ok(mut guard) = state.0.lock() {
|
cleanup_streamlit(&*state);
|
||||||
if let Some(ref mut child) = *guard {
|
|
||||||
log::info!("正在停止 Streamlit 进程...");
|
|
||||||
kill_streamlit(child);
|
|
||||||
log::info!("Streamlit 进程已停止");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 确保应用进程完全退出
|
tauri::RunEvent::Exit => {
|
||||||
std::process::exit(0);
|
log::info!("应用退出,执行最终清理...");
|
||||||
|
if let Some(state) = app_handle.try_state::<StreamlitProcess>() {
|
||||||
|
cleanup_streamlit(&*state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"identifier": "com.pinesound.geo-tool",
|
"identifier": "com.pinesound.geo-tool",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend"
|
||||||
"devUrl": "http://localhost:8501"
|
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
@@ -35,7 +34,12 @@
|
|||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"resources": [
|
"resources": [
|
||||||
"../dist_python/geo_tool_app"
|
"../dist_python/geo_tool_app.exe"
|
||||||
]
|
],
|
||||||
|
"windows": {
|
||||||
|
"wix": {
|
||||||
|
"language": "zh-CN"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||