Skip to content

第 3 章:启动链路——从 main 到可用应用

桌面应用启动不是一个函数,而是一组有依赖、有并行、有降级的阶段。本章追踪 Rust 入口、Tauri setup、配置初始化、窗口创建和 Mihomo 启动,解释哪些步骤必须串行,哪些可以并行,以及为什么 setup panic 被局部隔离。

一、入口先控制 Tokio,而不是直接 app_lib::run()

src-tauri/src/main.rs 首先创建多线程 Tokio runtime:

rust
let worker_limit = min(available_parallelism, 8);
let blocking_limit = 2 * worker_limit;

tokio::runtime::Builder::new_multi_thread()
    .worker_threads(worker_limit)
    .max_blocking_threads(blocking_limit)
    .enable_all()
    .build()?;

随后把 handle 交给 tauri::async_runtime::set,最后调用 app_lib::run()

为什么不让 Tauri 使用默认 runtime?项目同时存在网络 I/O、文件读写、WebSocket、sidecar 日志和若干 spawn_blocking 任务。显式限制 worker 和 blocking 线程能避免桌面客户端在高核机器上无节制创建线程,也保证所有 Tauri async command 共享同一 runtime。

二、run() 之前的最早防线

进入 src-tauri/src/lib.rs::run() 后,真正创建 Tauri Builder 之前完成三件事:

  1. macOS 发布构建执行 launch guard,必要时提前退出;
  2. 解析 portable 模式,决定应用数据目录;
  3. 启动单例检查,若已有主实例则把请求交给它并退出。

这三件事必须足够早。目录模式若晚确定,日志和配置可能写错地方;单例若晚检查,两个实例可能同时启动核心和修改系统代理。

三、插件层先建立能力,再进入业务初始化

setup_plugins() 注册了四类插件:

  • 系统能力:notification、clipboard、fs、dialog、shell、process;
  • 桌面能力:global-shortcut、deep-link、window-state、updater;
  • 网络能力:http;
  • Mihomo 能力:tauri-plugin-mihomo,通过本地 socket 连接核心。

其中 Mihomo 插件的 socket path 来自 IClashTemp::guard_external_controller_ipc()。这意味着前后端访问 Mihomo API 不必暴露一个任意 TCP 控制端口,生产路径可以走 Unix socket 或 Windows named pipe。

四、Tauri setup 被拆成两个可降级阶段

setup 闭包内部使用两次 catch_unwind

4.1 pre-init

第一阶段负责:

  1. 写入全局 APP_HANDLE
  2. 初始化工作目录与 logger;
  3. 注册 autostart;
  4. 注册 deep link;
  5. 注册 window-state 插件。

4.2 window-core

第二阶段无论第一阶段是否 panic,都会尝试:

  • resolve_setup_async():主业务异步启动;
  • resolve_setup_sync():scheme 与嵌入服务器;
  • init_signal():退出信号处理。

为什么要分两次捕获?如果 logger 或某个插件初始化 panic,窗口与核心仍可能以降级模式启动。若把全部 setup 包在一次捕获里,pre-init 的一个问题会跳过后续所有启动工作,用户只看到应用消失或白屏。

降级不等于忽略错误

捕获 panic 后会同时写 stderr 和应用日志。设计目标是“尽量给用户一个可恢复界面”,不是把错误吞掉。

五、resolve_setup_async() 的依赖图

异步初始化的前半段严格串行:

text
记录版本

macOS Dock 策略

启动脚本

探测 Service 状态

读取 Verge/Profile 配置并同步语言

创建窗口

复制资源 + 初始化 DNS 文件

生成并校验 Runtime 配置

二次确认 Runtime 已生成

顺序的原因:

  • Service 状态会影响后续核心启动决策;
  • 配置决定窗口是否静默启动、语言和主题;
  • Runtime 配置必须先生成,CoreManager 才能启动 Mihomo;
  • 资源和 DNS 文件是增强流程的输入。

六、核心启动与外围能力并行

前置依赖完成后,系统并行初始化:

rust
join!(
    init_core_manager(),
    init_tray(),
    init_timer(),
    init_hotkey(),
    init_auto_lightweight_boot(),
    init_auto_backup(),
    init_silent_updater(),
);

这些任务没有严格的先后依赖,串行会直接拉长冷启动。join! 结束后才:

  • 向前端发送 Clash 配置刷新;
  • 更新托盘菜单;
  • 标记 resolve 完成。

这里的设计原则是:先完成“生成可运行状态”的串行主链,再并发启动互相独立的长期能力。

七、Runtime 配置初始化的容错

Config::init_runtime_config() 不是简单调用 enhance()

  1. 检查 Mixed Port 是否被外部占用;
  2. 必要时寻找候选端口并跨层持久化;
  3. 生成运行时 Mapping;
  4. 写入检查文件;
  5. 调用 Mihomo 校验;
  6. 校验失败则切换到最小默认配置;
  7. 提交 runtime Draft;
  8. 清理孤儿 Profile 文件。

启动阶段的目标不是“配置必须完美”,而是“尽可能让核心用安全最小配置启动,并明确通知用户原配置的问题”。

八、CoreManager 启动还有端口重试环

CoreManager::init() 调用 start_core()。若失败且当前仍是 NotRunning,它最多三次尝试 Mixed Port fallback:

text
start_core 失败
  ├─ 核心其实已运行 → 保留运行状态并返回错误
  └─ 核心未运行

     再扫描可用端口
       ├─ 找到并应用 → 重试 start_core
       ├─ 无需 fallback → 返回原错误
       └─ fallback 自身失败 → 阻断本次启动

“核心已运行却返回错误”这个分支很重要。错误可能发生在启动后的代理恢复阶段,不能因为函数返回 Err 就假设没有核心,再启动第二个实例。

九、窗口和核心是相对独立的

窗口先于核心完全就绪被创建。这允许:

  • 展示 Service 需要安装/修复的对话框;
  • 在核心失败时仍提供设置和诊断入口;
  • 静默启动时只保留托盘和核心;
  • 轻量模式销毁窗口但保持后台代理运行。

因此“应用是否运行”“窗口是否存在”“核心是否运行”是三个不同状态,不能用一个布尔值代替。

十、运行事件与退出

Tauri RunEvent 处理四类关键事件:

  • Ready/Resumed:标记命令服务器 ready;
  • macOS Reopen:退出轻量模式或重新显示窗口;
  • ExitRequested:通常阻止默认退出,转入产品级清理;
  • 主窗口 CloseRequested:隐藏窗口而不是结束核心。

真正退出会调用 feat::quit(),其责任包括:

  • 设置 exiting latch,避免重复退出;
  • 停止代理守卫和核心;
  • 恢复系统代理/DNS;
  • 提交并保存 Draft;
  • 关闭嵌入服务器;
  • 最终让 Tauri 退出。

操作系统 session ending 可能绕过正常 ExitRequested,代码因此还有 best-effort 清理分支。这是桌面应用与普通 Web 服务不同的地方:退出协议本身也可能不可靠。

十一、启动设计的四条原则

  1. 越早决定全局环境越好:portable、单例和日志目录在 Builder 前完成。
  2. 依赖链串行,外围能力并行:不是所有初始化都塞进一个顺序列表。
  3. 窗口是恢复面:核心失败不应自动导致 UI 消失。
  4. 每个 start 都必须有 settled:PAC 等临时关闭的能力要在所有返回路径恢复到与 RunningMode 一致。

本章源码索引

  • src-tauri/src/main.rs:Tokio runtime 建立
  • src-tauri/src/lib.rs::run:Builder、setup、RunEvent 与退出
  • src-tauri/src/lib.rs::app_init:插件、deep link、window state
  • src-tauri/src/utils/resolve/mod.rs::resolve_setup_async:业务启动总序
  • src-tauri/src/utils/init.rs:目录、配置文件、资源与平台 scheme
  • src-tauri/src/config/config.rs::init_runtime_config:启动配置生成
  • src-tauri/src/core/manager/mod.rs::CoreManager::init:核心启动与端口重试
  • src-tauri/src/feat/mod.rs:退出与清理用例导出

原创文档 CC BY-SA 4.0 · 站点代码 MIT