use std::sync::Mutex; use std::time::Duration; use tauri::Manager; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; // ===== 配置常量 ===== /// Sidecar App 启动超时时间(秒) const STARTUP_TIMEOUT_SECS: u64 = 5; /// 默认起始端口 const DEFAULT_PORT: u16 = 3000; /// 端口扫描范围(从起始端口开始扫描的端口数量) const PORT_SCAN_RANGE: u16 = 100; /// 窗口默认宽度 const DEFAULT_WINDOW_WIDTH: f64 = 1200.0; /// 窗口默认高度 const DEFAULT_WINDOW_HEIGHT: f64 = 800.0; /// 窗口标题 const WINDOW_TITLE: &str = "Tauri App"; // ===== 数据结构 ===== /// 全局状态:存储 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 // 回退到起始端口 } /// 启动 Sidecar 进程并创建主窗口 pub fn spawn_sidecar(app_handle: tauri::AppHandle) { // 检测是否为开发模式 let is_dev = cfg!(debug_assertions); if is_dev { // 开发模式:直接创建窗口连接到 Vite 开发服务器 println!("🔧 开发模式"); match tauri::WebviewWindowBuilder::new( &app_handle, "main", tauri::WebviewUrl::External("http://localhost:3000".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(DEFAULT_PORT).await; println!("使用端口: {}", port); // 启动 sidecar let sidecar = app_handle .shell() .sidecar("server") .expect("无法找到 app") .env("PORT", port.to_string()); let (mut rx, child) = sidecar.spawn().expect("启动 sidecar 失败"); // 保存进程句柄到全局状态 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 { if let CommandEvent::Stdout(line) = event { 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); 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() .expect("创建窗口失败"); break; } } // 超时检查 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 进程已终止"); } } } }