feat: 新增 Frpc 一键启动与实时日志回显功能

新增一键启动/停止 Frpc 的命令及后端进程管理模块,支持生成配置文件、独立进程拉起、日志实时推送与退出事件通知。新增 Frpc 状态与日志类型定义,同步更新前端仪表盘逻辑以展示运行状态与实时日志。新增中英文启动失败提示文案,完善应用退出时自动终止 Frpc 子进程的逻辑。
This commit is contained in:
Yuhang Wu
2026-10-13 17:35:50 +08:00
parent 85a52dfbe9
commit a6276680cc
13 changed files with 1105 additions and 40 deletions
+3 -2
View File
@@ -1,4 +1,5 @@
pub mod tunnel;
pub mod frp;
pub mod runner;
pub mod server;
pub mod system;
pub mod frp;
pub mod tunnel;
+171
View File
@@ -0,0 +1,171 @@
//! 一键启动 / 停止 Frpc 相关命令。
//!
//! 启动流程:读取所选 frpc 版本 → 汇总已启用的服务器与隧道 → 生成 frpc.toml
//! → 交给 `FrpcRunner` 以独立进程方式拉起,并实时回传日志。
use std::path::PathBuf;
use sqlx::SqlitePool;
use tauri::{AppHandle, Manager, State};
use tauri_helper::auto_collect_command;
use crate::models::{ApiResponse, FrpDownload, FrpcStatus, Server, Tunnel};
use crate::services::config::generate_frpc_config;
use crate::services::runner::FrpcRunner;
// 统一查询字段,使用 concat! 保证结果为 'static str(sqlx 0.9 要求)
macro_rules! server_fields {
() => {
"id, name, addr, port, token, protocol, tls_enable, remark, sort, pinned, enabled, created_at, updated_at"
};
}
macro_rules! tunnel_fields {
() => {
"id, name, proxy_type, local_ip, local_port, remote_port, custom_domains, subdomain, locations, secret_key, use_encryption, use_compression, bandwidth_limit, proxy_protocol_version, health_check_type, health_check_timeout, health_check_max_failed, health_check_interval, health_check_path, load_balancer_group, load_balancer_group_key, remark, sort, pinned, enabled, created_at, updated_at"
};
}
macro_rules! download_fields {
() => {
"id, version, os, arch, platform, file_name, archive_path, frpc_path, frps_path, file_size, source, created_at, updated_at"
};
}
/// 本次运行生成的 frpc.toml 路径(应用数据目录/frp/run/frpc.toml)
fn config_file_path(app: &AppHandle) -> Result<PathBuf, String> {
let dir = app
.path()
.app_data_dir()
.map_err(|e| e.to_string())?
.join("frp")
.join("run");
std::fs::create_dir_all(&dir).map_err(|e| format!("创建配置目录失败:{e}"))?;
Ok(dir.join("frpc.toml"))
}
/// 获取 Frpc 当前运行状态
#[tauri::command]
#[auto_collect_command]
pub async fn get_frpc_status(
runner: State<'_, FrpcRunner>,
) -> Result<ApiResponse<FrpcStatus>, String> {
Ok(ApiResponse::success(runner.status()))
}
/// 一键启动 Frpc
#[tauri::command]
#[auto_collect_command]
pub async fn start_frpc(
app: AppHandle,
pool: State<'_, SqlitePool>,
runner: State<'_, FrpcRunner>,
download_id: i64,
) -> Result<ApiResponse<FrpcStatus>, String> {
if runner.is_running() {
return Ok(ApiResponse::error(409, "Frpc 已在运行中,请先停止后再启动"));
}
// 1. 读取所选 Frpc 版本记录
let download = match sqlx::query_as::<_, FrpDownload>(concat!(
"SELECT ",
download_fields!(),
" FROM frp_downloads WHERE id = ?"
))
.bind(download_id)
.fetch_optional(&*pool)
.await
{
Ok(Some(row)) => row,
Ok(None) => return Ok(ApiResponse::error(404, "所选 Frpc 版本不存在,请刷新后重试")),
Err(e) => return Ok(ApiResponse::error(500, &e.to_string())),
};
let Some(frpc_path) = download
.frpc_path
.clone()
.filter(|path| !path.trim().is_empty())
else {
return Ok(ApiResponse::error(
400,
"所选版本缺少 frpc 可执行文件,请重新下载或导入",
));
};
if !std::path::Path::new(&frpc_path).is_file() {
return Ok(ApiResponse::error(
400,
&format!("frpc 可执行文件不存在:{frpc_path}"),
));
}
// 2. 已启用的服务器(全局同时只启用一个)
let server = match sqlx::query_as::<_, Server>(concat!(
"SELECT ",
server_fields!(),
" FROM servers WHERE enabled = 1 ORDER BY pinned DESC, sort ASC, id ASC LIMIT 1"
))
.fetch_optional(&*pool)
.await
{
Ok(Some(row)) => row,
Ok(None) => {
return Ok(ApiResponse::error(
400,
"尚未启用任何服务器,请先在服务器页面启用一个服务器",
));
}
Err(e) => return Ok(ApiResponse::error(500, &e.to_string())),
};
// 3. 已启用的隧道(可同时启用多个)
let tunnels = match sqlx::query_as::<_, Tunnel>(concat!(
"SELECT ",
tunnel_fields!(),
" FROM tunnels WHERE enabled = 1 ORDER BY pinned DESC, sort ASC, id ASC"
))
.fetch_all(&*pool)
.await
{
Ok(list) => list,
Err(e) => return Ok(ApiResponse::error(500, &e.to_string())),
};
if tunnels.is_empty() {
return Ok(ApiResponse::error(
400,
"尚未启用任何隧道,请先在隧道列表页面启用至少一个隧道",
));
}
// 4. 生成 frpc.toml
let config = generate_frpc_config(&server, &tunnels);
let config_path = match config_file_path(&app) {
Ok(path) => path,
Err(e) => return Ok(ApiResponse::error(500, &e)),
};
if let Err(e) = std::fs::write(&config_path, config) {
return Ok(ApiResponse::error(
500,
&format!("写入 frpc.toml 失败:{e}"),
));
}
// 5. 启动 frpc 进程
let config_path_str = config_path.to_string_lossy().to_string();
match runner.spawn(&app, &frpc_path, &config_path_str, &download.version) {
Ok(status) => Ok(ApiResponse::success(status)),
Err(e) => Ok(ApiResponse::error(500, &e)),
}
}
/// 停止 Frpc
#[tauri::command]
#[auto_collect_command]
pub async fn stop_frpc(
app: AppHandle,
runner: State<'_, FrpcRunner>,
) -> Result<ApiResponse<FrpcStatus>, String> {
match runner.stop(&app) {
Ok(status) => Ok(ApiResponse::success(status)),
Err(e) => Ok(ApiResponse::error(500, &e)),
}
}