diff --git a/AGENTS.md b/AGENTS.md index 9bf82be..e8a292d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,10 @@ ## 项目概览 -- **项目类型**: Tauri v2 桌面应用 +- **项目类型**: Tauri v2 桌面应用(轻量级壳子) - **后端**: Rust (Edition 2021) -- **架构**: Sidecar 模式集成 Nitro Server +- **架构**: Sidecar 模式 - Nitro Server 承载主要业务逻辑 +- **设计理念**: Tauri 仅提供原生桌面能力(文件对话框、系统通知等),Web 逻辑全部由 Nitro 处理 - **异步运行时**: Tokio - **Rust 版本**: 1.92.0+ @@ -80,8 +81,11 @@ cargo clean ``` tauri-demo/ ├── src/ -│ ├── lib.rs # 核心应用逻辑 (主要代码) -│ └── main.rs # 入口文件 (仅调用 lib::run) +│ ├── main.rs # 入口文件 (仅调用 lib::run) +│ ├── lib.rs # 核心应用逻辑 (注册插件、命令、状态) +│ ├── commands/ +│ │ └── mod.rs # 原生桌面功能命令 (文件对话框、通知等) +│ └── sidecar.rs # Nitro 进程管理 (启动、端口扫描、清理) ├── binaries/ # Sidecar 二进制文件 │ └── nitro-server-* # Nitro Server 可执行文件 ├── capabilities/ # Tauri v2 权限配置 @@ -219,6 +223,23 @@ async fn is_port_available(port: u16) -> bool { } ## Tauri 特定规范 +### 模块组织 + +- **`lib.rs`**: 主入口,负责注册插件、命令、状态管理 +- **`commands/mod.rs`**: 所有 Tauri 命令集中定义,命令必须是 `pub fn` +- **`sidecar.rs`**: Sidecar 进程管理逻辑,导出公共 API + +```rust +// lib.rs - 模块声明 +mod commands; +mod sidecar; + +use sidecar::NitroProcess; + +// 注册命令时使用模块路径 +.invoke_handler(tauri::generate_handler![commands::greet]) +``` + ### 命令定义 - 使用 `#[tauri::command]` 宏标记命令 @@ -287,8 +308,10 @@ match event { ```toml tauri = { version = "2", features = [] } -tokio = { version = "1", features = ["net"] } +tauri-plugin-opener = "2" +tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["net"] } ``` ## 开发工具 diff --git a/Cargo.lock b/Cargo.lock index 8956323..aa4bc65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3610,7 +3610,6 @@ name = "tauri-demo" version = "0.1.0" dependencies = [ "serde", - "serde_json", "tauri", "tauri-build", "tauri-plugin-opener", diff --git a/Cargo.toml b/Cargo.toml index e2066d6..e403a5c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,5 +22,4 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } -serde_json = "1" tokio = { version = "1", features = ["net"] } diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..9a899ff --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,8 @@ +// 原生桌面功能命令 +// 未来可能包含: 文件对话框、系统通知、剪贴板等 + +// 示例命令 (可根据需要删除或替换) +#[tauri::command] +pub fn greet(name: &str) -> String { + format!("Hello, {}! You've been greeted from Rust!", name) +} diff --git a/src/lib.rs b/src/lib.rs index d653631..fc116a9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,34 +1,10 @@ -use std::sync::Mutex; -use std::time::Duration; use tauri::Manager; -use tauri_plugin_shell::process::{CommandChild, CommandEvent}; -use tauri_plugin_shell::ShellExt; -// 全局状态:存储 Nitro 进程句柄 -struct NitroProcess(Mutex>); +// 模块声明 +mod commands; +mod sidecar; -// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ -#[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) -} - -// 检查端口是否可用 -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 + 100 { - if is_port_available(port).await { - return port; - } - } - start // 回退到起始端口 -} +use sidecar::NitroProcess; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -36,94 +12,23 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_shell::init()) .setup(|app| { - // 使用 Tauri 的状态管理存储进程句柄 - app.manage(NitroProcess(Mutex::new(None))); + // 注册全局状态 + app.manage(NitroProcess(std::sync::Mutex::new(None))); + // 启动 Nitro Sidecar 进程 let app_handle = app.handle().clone(); - - // 异步启动 Nitro sidecar - tauri::async_runtime::spawn(async move { - // 查找可用端口 - let port = find_available_port(3000).await; - println!("使用端口: {}", port); - - // 启动 sidecar - let sidecar = app_handle - .shell() - .sidecar("nitro-server") - .expect("无法找到 nitro-server sidecar") - .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(5); - 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!("Nitro: {}", output); - - // 检测服务器启动成功的标志 - if output.contains("Listening on:") || output.contains("localhost") { - server_ready = true; - println!("✓ Nitro 服务器启动成功!"); - - // 创建主窗口 - let url = format!("http://localhost:{}", port); - tauri::WebviewWindowBuilder::new( - &app_handle, - "main", - tauri::WebviewUrl::External(url.parse().unwrap()), - ) - .title("Nitro Application") - .inner_size(1200.0, 800.0) - .center() - .build() - .expect("创建窗口失败"); - - break; - } - } - - // 超时检查 - if start_time.elapsed() > timeout { - eprintln!("✗ 启动超时:Nitro 服务器未能在 5 秒内启动"); - break; - } - } - - if !server_ready { - eprintln!("✗ Nitro 服务器启动失败"); - std::process::exit(1); - } - }); + sidecar::spawn_nitro_sidecar(app_handle); Ok(()) }) - .invoke_handler(tauri::generate_handler![greet]) + .invoke_handler(tauri::generate_handler![commands::greet]) .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { // 监听应用退出事件,清理 Nitro 进程 match event { tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => { - println!("应用退出,正在清理 Nitro 进程..."); - 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!("✓ Nitro 进程已终止"); - } - } - } + sidecar::cleanup_nitro_process(app_handle); } _ => {} } diff --git a/src/sidecar.rs b/src/sidecar.rs new file mode 100644 index 0000000..527fec4 --- /dev/null +++ b/src/sidecar.rs @@ -0,0 +1,106 @@ +use std::sync::Mutex; +use std::time::Duration; + +use tauri::Manager; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; + +// 全局状态:存储 Nitro 进程句柄 +pub struct NitroProcess(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 + 100 { + if is_port_available(port).await { + return port; + } + } + start // 回退到起始端口 +} + +/// 启动 Nitro Sidecar 进程并创建主窗口 +pub fn spawn_nitro_sidecar(app_handle: tauri::AppHandle) { + tauri::async_runtime::spawn(async move { + // 查找可用端口 + let port = find_available_port(3000).await; + println!("使用端口: {}", port); + + // 启动 sidecar + let sidecar = app_handle + .shell() + .sidecar("nitro-server") + .expect("无法找到 nitro-server sidecar") + .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(5); + 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!("Nitro: {}", output); + + // 检测服务器启动成功的标志 + if output.contains("Listening on:") || output.contains("localhost") { + server_ready = true; + println!("✓ Nitro 服务器启动成功!"); + + // 创建主窗口 + let url = format!("http://localhost:{}", port); + tauri::WebviewWindowBuilder::new( + &app_handle, + "main", + tauri::WebviewUrl::External(url.parse().unwrap()), + ) + .title("Nitro Application") + .inner_size(1200.0, 800.0) + .center() + .build() + .expect("创建窗口失败"); + + break; + } + } + + // 超时检查 + if start_time.elapsed() > timeout { + eprintln!("✗ 启动超时:Nitro 服务器未能在 5 秒内启动"); + break; + } + } + + if !server_ready { + eprintln!("✗ Nitro 服务器启动失败"); + std::process::exit(1); + } + }); +} + +/// 清理 Nitro 进程 (在应用退出时调用) +pub fn cleanup_nitro_process(app_handle: &tauri::AppHandle) { + println!("应用退出,正在清理 Nitro 进程..."); + 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!("✓ Nitro 进程已终止"); + } + } + } +}