use std::sync::Mutex; use std::time::Duration; use tauri::Manager; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; // ===== 配置常量 ===== /// Sidecar Server 启动超时时间(秒) const STARTUP_TIMEOUT_SECS: u64 = 5; /// 默认起始端口 const DEFAULT_PORT: u16 = 3000; /// 开发模式使用的端口 const DEV_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 is_port_in_use(port: u16) -> bool { use tokio::io::AsyncWriteExt; match tokio::net::TcpStream::connect(format!("127.0.0.1:{}", port)).await { Ok(mut stream) => { let _ = stream.shutdown().await; true } Err(_) => false, } } // 查找可用端口 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) { tauri::async_runtime::spawn(async move { // 检测是否为开发模式 let is_dev = cfg!(debug_assertions); if is_dev { // 开发模式:直接连接到 localhost:3000 println!("🔧 开发模式:使用本地开发服务器 (localhost:{})", DEV_PORT); // 等待开发服务器就绪(可选:添加重试逻辑) let max_retries = 10; let retry_delay = Duration::from_millis(500); let mut server_ready = false; for attempt in 1..=max_retries { if is_port_in_use(DEV_PORT).await { server_ready = true; println!("✓ 开发服务器已就绪 (端口 {})", DEV_PORT); break; } println!( "⏳ 等待开发服务器启动... (尝试 {}/{})", attempt, max_retries ); tokio::time::sleep(retry_delay).await; } if !server_ready { eprintln!("✗ 开发服务器未就绪,请确保运行了 `bun run dev`"); std::process::exit(1); } // 创建主窗口 let url = format!("http://localhost:{}", DEV_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("创建窗口失败"); } else { // 生产模式:启动 sidecar 二进制 println!("🚀 生产模式:启动 Sidecar Server"); // 查找可用端口 let port = find_available_port(DEFAULT_PORT).await; println!("使用端口: {}", port); // 启动 sidecar let sidecar = app_handle .shell() .sidecar("server") .expect("无法找到 server") .env("NITRO_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 server_ready = false; while let Some(event) = rx.recv().await { if let CommandEvent::Stdout(line) = event { let output = String::from_utf8_lossy(&line); println!("Server: {}", output); // 检测服务器启动成功的标志 if output.contains("Listening on:") || output.contains("localhost") { server_ready = true; println!("✓ Server 启动成功!"); // 创建主窗口 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!( "✗ 启动超时: Server 未能在 {} 秒内启动", STARTUP_TIMEOUT_SECS ); break; } } if !server_ready { eprintln!("✗ Server 启动失败"); std::process::exit(1); } } }); } /// 清理 Sidecar 进程 (在应用退出时调用) pub fn cleanup_sidecar_process(app_handle: &tauri::AppHandle) { 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 进程已终止"); } } } }