use std::sync::Mutex; use std::time::Duration; use tauri::Manager; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; // ===== 项目配置 ===== /// Sidecar 二进制名称 const SIDECAR_NAME: &str = "openbridgeTokenUsageViewerServer"; /// 默认服务器端口 const DEFAULT_PORT: u16 = 13098; /// 从环境变量获取端口 (PROJECT_SERVER_PORT),默认 13098 fn get_project_port() -> u16 { std::env::var("PROJECT_SERVER_PORT") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(DEFAULT_PORT) } // ===== 配置常量 ===== /// Sidecar App 启动超时时间(秒) const STARTUP_TIMEOUT_SECS: u64 = 30; /// 端口扫描范围(从起始端口开始扫描的端口数量) const PORT_SCAN_RANGE: u16 = 100; /// 窗口默认宽度 const DEFAULT_WINDOW_WIDTH: f64 = 1200.0; /// 窗口默认高度 const DEFAULT_WINDOW_HEIGHT: f64 = 800.0; /// 窗口标题 const WINDOW_TITLE: &str = "OpenBridge Token Usage Viewer"; // ===== 数据结构 ===== /// 全局状态:存储 Sidecar 进程句柄 pub struct SidecarProcess(pub Mutex>); // 检查端口是否可用(未被占用) async fn is_port_available(port: u16) -> bool { tokio::net::TcpListener::bind(format!("127.0.0.1:{}", port)) .await .is_ok() } // 查找可用端口 async fn find_available_port(start: u16) -> u16 { for port in start..start + PORT_SCAN_RANGE { if is_port_available(port).await { return port; } } start // 回退到起始端口 } // 显示错误对话框 fn show_error_dialog(message: &str) { #[cfg(target_os = "windows")] { use std::process::Command; let _ = Command::new("powershell") .args([ "-Command", &format!( "[System.Windows.Forms.MessageBox]::Show('{}', '启动错误', 'OK', 'Error')", message.replace('\'', "''") ), ]) .spawn(); } #[cfg(not(target_os = "windows"))] { eprintln!("错误: {}", message); } } /// 启动 Sidecar 进程并创建主窗口 pub fn spawn_sidecar(app_handle: tauri::AppHandle) { // 检测是否为开发模式 let is_dev = cfg!(debug_assertions); // 获取项目专用端口 let project_port = get_project_port(); if is_dev { // 开发模式:直接创建窗口连接到 Vite 开发服务器 println!("🔧 开发模式"); println!("📌 端口: {}", project_port); let dev_url = format!("http://localhost:{}", project_port); match tauri::WebviewWindowBuilder::new( &app_handle, "main", tauri::WebviewUrl::External(dev_url.parse().unwrap()), ) .title(WINDOW_TITLE) .inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT) .center() .build() { Ok(_) => println!("✓ 开发窗口创建成功"), Err(e) => { eprintln!("✗ 窗口创建失败: {}", e); } } return; } // 生产模式:启动 sidecar 二进制 tauri::async_runtime::spawn(async move { println!("🚀 生产模式"); // 查找可用端口 (从项目端口开始扫描) let port = find_available_port(project_port).await; println!("📌 端口: {}", port); // 启动 sidecar let sidecar = match app_handle.shell().sidecar(SIDECAR_NAME) { Ok(cmd) => cmd.env("PORT", port.to_string()), Err(e) => { eprintln!("✗ 无法找到 sidecar: {}", e); show_error_dialog("无法找到后端服务程序,请重新安装应用。"); std::process::exit(1); } }; let (mut rx, child) = match sidecar.spawn() { Ok(result) => result, Err(e) => { eprintln!("✗ 启动 sidecar 失败: {}", e); show_error_dialog(&format!("后端服务启动失败: {}", e)); std::process::exit(1); } }; // 保存进程句柄到全局状态 if let Some(state) = app_handle.try_state::() { *state.0.lock().unwrap() = Some(child); } // 监听 stdout,等待服务器就绪信号 let start_time = std::time::Instant::now(); let timeout = Duration::from_secs(STARTUP_TIMEOUT_SECS); let mut app_ready = false; while let Some(event) = rx.recv().await { match event { CommandEvent::Stdout(line) => { let output = String::from_utf8_lossy(&line); println!("App: {}", output); // 检测 App 启动成功的标志 if output.contains("Listening on:") || output.contains("localhost") { app_ready = true; println!("✓ App 启动成功!"); // 创建主窗口 let url = format!("http://localhost:{}", port); match tauri::WebviewWindowBuilder::new( &app_handle, "main", tauri::WebviewUrl::External(url.parse().unwrap()), ) .title(WINDOW_TITLE) .inner_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT) .center() .build() { Ok(_) => println!("✓ 窗口创建成功"), Err(e) => { eprintln!("✗ 窗口创建失败: {}", e); show_error_dialog(&format!("窗口创建失败: {}", e)); } } break; } } CommandEvent::Stderr(line) => { let output = String::from_utf8_lossy(&line); eprintln!("App Error: {}", output); } CommandEvent::Error(e) => { eprintln!("Sidecar 错误: {}", e); } _ => {} } // 超时检查 if start_time.elapsed() > timeout { eprintln!("✗ 启动超时: App 未能在 {} 秒内启动", STARTUP_TIMEOUT_SECS); break; } } if !app_ready { eprintln!("✗ App 启动失败"); std::process::exit(1); } }); } /// 清理 Sidecar 进程 (在应用退出时调用) pub fn cleanup_sidecar_process(app_handle: &tauri::AppHandle) { let is_dev = cfg!(debug_assertions); if is_dev { // 开发模式:退出时发送异常信号(exit 1),让 Turbo 停止 Vite 服务器 println!("🔧 开发模式退出,终止所有依赖任务..."); std::process::exit(1); } // 生产模式:正常清理 sidecar 进程 println!("应用退出,正在清理 Sidecar 进程..."); if let Some(state) = app_handle.try_state::() { if let Ok(mut process) = state.0.lock() { if let Some(child) = process.take() { let _ = child.kill(); println!("✓ Sidecar 进程已终止"); } } } }