Files
openbridge-token-usage-viewer/src-tauri/src/sidecar.rs
MAO Dongyang f2db4bff9d chore: 统一开发服务器端口为 13098
- 更新 .env.example、env.ts、vite.config.ts 默认端口
- 同步更新 sidecar.rs Rust 端口常量
- 更新 README、AGENTS.md 等文档中的端口引用
2026-01-27 11:10:21 +08:00

231 lines
7.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Option<CommandChild>>);
// 检查端口是否可用(未被占用)
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::<SidecarProcess>() {
*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::<SidecarProcess>() {
if let Ok(mut process) = state.0.lock() {
if let Some(child) = process.take() {
let _ = child.kill();
println!("✓ Sidecar 进程已终止");
}
}
}
}