From 9d31a8d0f523326fd62d518dfccebda62db7854e Mon Sep 17 00:00:00 2001 From: MFSGA Date: Tue, 3 Nov 2026 20:20:57 -0800 Subject: [PATCH 1/7] refactor(startup): reuse resolved paths across config logging and setup --- backend/tauri/src/lib.rs | 9 ++-- backend/tauri/src/setup.rs | 3 +- backend/tauri/src/utils/init/logging.rs | 49 +++++++++++++---- backend/tauri/src/utils/init/mod.rs | 72 +++++++++++++++++-------- docs/ref-alignment-progress.md | 44 +++++++++++++++ 5 files changed, 140 insertions(+), 37 deletions(-) diff --git a/backend/tauri/src/lib.rs b/backend/tauri/src/lib.rs index 7711094b3..117148cad 100644 --- a/backend/tauri/src/lib.rs +++ b/backend/tauri/src/lib.rs @@ -9,7 +9,7 @@ use tauri_plugin_deep_link::DeepLinkExt; use crate::{ config::core::Config, - utils::{init, resolve}, + utils::{init, path::PathResolver, resolve}, }; #[cfg(feature = "e2e")] @@ -80,7 +80,10 @@ fn setup_frontend_console_bridge(app: &mut tauri::App) { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() -> std::io::Result<()> { - crate::log_err!(init::init_config()); + // Resolve once before startup initialization, then pass the same roots to + // configuration and the Tauri composition root (as in the pinned ref). + let paths = PathResolver::from_env().map_err(std::io::Error::other)?; + init::init_config(&paths).map_err(std::io::Error::other)?; if let Some(lang) = Config::verge().latest().language.clone() { let locale = crate::bridge::verge::locale_key_from_legacy(&lang) .map(str::to_owned) @@ -195,7 +198,7 @@ pub fn run() -> std::io::Result<()> { .setup(move |app| { specta_builder.mount_events(app); - setup::setup(app)?; + setup::setup(app, paths.clone())?; core::updater::recover_interrupted_update(); core::clash::setup(app)?; diff --git a/backend/tauri/src/setup.rs b/backend/tauri/src/setup.rs index 102d12ae1..f3929ce3a 100644 --- a/backend/tauri/src/setup.rs +++ b/backend/tauri/src/setup.rs @@ -36,8 +36,7 @@ impl crate::service::profile_file::SelfProxyPortSource for LegacySelfProxyPort { } } -pub fn setup(app: &mut tauri::App) -> anyhow::Result<()> { - let paths = PathResolver::from_env().context("failed to resolve app paths")?; +pub fn setup(app: &mut tauri::App, paths: PathResolver) -> anyhow::Result<()> { let shutdown = tokio_util::sync::CancellationToken::new(); let tasks = tokio_util::task::TaskTracker::new(); let jobs = crate::startup_failure::report_boundary( diff --git a/backend/tauri/src/utils/init/logging.rs b/backend/tauri/src/utils/init/logging.rs index 5faabb5da..fa7718e24 100644 --- a/backend/tauri/src/utils/init/logging.rs +++ b/backend/tauri/src/utils/init/logging.rs @@ -1,6 +1,7 @@ use std::{ fs, io::IsTerminal, + path::Path, sync::{ OnceLock, mpsc::{self, Sender}, @@ -22,7 +23,7 @@ static JOB_LOG_CAPTURE: OnceLock = OnceLock::new(); use crate::{ config::{self, chimera::logging::LoggingLevel, core::Config}, - utils::dirs, + utils::path::PathResolver, }; pub type ReloadSignal = (Option, Option); @@ -36,12 +37,10 @@ impl Channel { } /// initial instance global logger -pub fn init() -> Result<()> { +pub fn init(paths: &PathResolver) -> Result<()> { let jobs = crate::client::jobs::capture(); - let log_dir = dirs::app_logs_dir().unwrap(); - if !log_dir.exists() { - let _ = fs::create_dir_all(&log_dir); - } + let log_dir = paths.app_logs_dir(); + fs::create_dir_all(&log_dir)?; let (log_level, log_max_files) = { (LoggingLevel::Debug, 7) }; // This is intended to capture config loading errors let (filter, filter_handle) = reload::Layer::new( EnvFilter::builder() @@ -55,7 +54,7 @@ pub fn init() -> Result<()> { ); // register the logger - let (appender, _guard) = get_file_appender(log_max_files)?; + let (appender, _guard) = get_file_appender(log_max_files, &log_dir)?; let (file_layer, file_handle) = reload::Layer::new( fmt::layer() .json() @@ -92,7 +91,7 @@ pub fn init() -> Result<()> { } if let Some(max_files) = signal.1 { - let (appender, guard) = match get_file_appender(max_files) { + let (appender, guard) = match get_file_appender(max_files, &log_dir) { Ok(x) => x, Err(e) => { error!("failed to create file appender: {}", e); @@ -150,8 +149,7 @@ pub fn jobs_capture() -> chimera_jobs::LogCapture { .clone() } -fn get_file_appender(max_files: usize) -> Result<(NonBlocking, WorkerGuard)> { - let log_dir = dirs::app_logs_dir().unwrap(); +fn get_file_appender(max_files: usize, log_dir: &Path) -> Result<(NonBlocking, WorkerGuard)> { let file_appender = tracing_appender::rolling::Builder::new() .filename_prefix("clash-chimera") .filename_suffix("app.log") @@ -171,3 +169,34 @@ pub fn refresh_logger(signal: ReloadSignal) -> Result<()> { None => bail!("no logger channel"), } } + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write as _; + + #[test] + fn file_appender_uses_injected_directory() { + let temp = tempfile::tempdir().unwrap(); + let log_dir = temp.path().join("only-these-logs"); + fs::create_dir_all(&log_dir).unwrap(); + + let (mut writer, guard) = get_file_appender(7, &log_dir).unwrap(); + writer.write_all(b"injected-log-root\n").unwrap(); + drop(writer); + drop(guard); + + let log_files: Vec<_> = fs::read_dir(&log_dir) + .unwrap() + .map(|entry| entry.unwrap().path()) + .collect(); + assert!(log_files.iter().any(|path| { + fs::read(path).is_ok_and(|bytes| { + bytes + .windows(b"injected-log-root".len()) + .any(|part| part == b"injected-log-root") + }) + })); + assert!(!temp.path().join("logs").exists()); + } +} diff --git a/backend/tauri/src/utils/init/mod.rs b/backend/tauri/src/utils/init/mod.rs index 32db669a6..4bf01f7f2 100644 --- a/backend/tauri/src/utils/init/mod.rs +++ b/backend/tauri/src/utils/init/mod.rs @@ -5,7 +5,7 @@ use serde::Serialize; use crate::{ config::{chimera::IVerge, clash::IClashTemp, profile::profiles::Profiles}, - utils::{dirs, help}, + utils::{dirs, help, path::PathResolver}, }; mod logging; @@ -14,30 +14,30 @@ pub use logging::refresh_logger; /// Initialize all the config files /// before tauri setup -pub fn init_config() -> Result<()> { - // init log - logging::init().unwrap(); +pub fn init_config(paths: &PathResolver) -> Result<()> { + // Initialize the logger against the same resolved data root used by setup. + logging::init(paths)?; tracing::info!("initializing logging config..."); - crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| { - if !profiles_dir.exists() { - let _ = fs::create_dir_all(&profiles_dir); - } - })); - - crate::log_err!( - dirs::clash_guard_overrides_path() - .and_then(|path| { initialize_default_yaml(&path, &IClashTemp::template().0) }) - ); + initialize_default_configs(paths) +} - crate::log_err!( - dirs::chimera_config_path() - .and_then(|path| { initialize_default_yaml(&path, &IVerge::template()) }) - ); +fn initialize_default_configs(paths: &PathResolver) -> Result<()> { + if let Err(error) = fs::create_dir_all(paths.app_profiles_dir()) { + tracing::error!(%error, "failed to create profiles directory"); + } - crate::log_err!( - dirs::profiles_path() - .and_then(|path| { initialize_default_yaml(&path, &Profiles::default()) }) - ); + crate::log_err!(initialize_default_yaml( + &paths.clash_guard_overrides_path(), + &IClashTemp::template().0, + )); + crate::log_err!(initialize_default_yaml( + &paths.chimera_config_path(), + &IVerge::template(), + )); + crate::log_err!(initialize_default_yaml( + &paths.profiles_path(), + &Profiles::default(), + )); Ok(()) } @@ -57,6 +57,34 @@ fn initialize_default_yaml(path: &Path, template: &T) -> Result<() mod startup_file_tests { use super::*; + #[test] + fn default_config_initialization_uses_only_injected_config_root() { + let temp = tempfile::tempdir().unwrap(); + let config_dir = temp.path().join("isolated-config"); + let data_dir = temp.path().join("untouched-data"); + let paths = PathResolver::with_base_dirs(config_dir.clone(), data_dir.clone()); + fs::create_dir_all(&config_dir).unwrap(); + + initialize_default_configs(&paths).unwrap(); + + assert!(paths.app_profiles_dir().is_dir()); + for path in [ + paths.clash_guard_overrides_path(), + paths.chimera_config_path(), + paths.profiles_path(), + ] { + assert!( + path.is_file(), + "startup did not initialize {}", + path.display() + ); + } + assert!( + !data_dir.exists(), + "defaults must not resolve or create a separate data root" + ); + } + #[test] fn missing_legacy_file_gets_its_original_default_header() { let temp = tempfile::tempdir().unwrap(); diff --git a/docs/ref-alignment-progress.md b/docs/ref-alignment-progress.md index 67dae25e4..7b47f474f 100644 --- a/docs/ref-alignment-progress.md +++ b/docs/ref-alignment-progress.md @@ -2114,3 +2114,47 @@ DIFF-001 至 DIFF-012 是此前基于 - 测试契约:`docs/testing/contracts/windows-core-graceful-shutdown.md` 定义初始状态、目标组与同控制台兄弟组的可观察结果、helper 失败注入、清理与未覆盖边界。用例使用真实 Windows Job Object 和控制台进程组,不把 fixture/unit 测试当成系统行为证据;它不覆盖生产隐藏控制台分配路径,也尚未验证实际 core 对 Ctrl+Break 的处理。runtime 子模块的测试尚未接入现有根仓 CI 门禁;本批不改 L3 workflow。 - 已执行:ProcessKit 3.3.4 API 适配后,`cargo +nightly test --manifest-path backend/chimera-runtime/Cargo.toml -p chimera-utils --features process` 在 macOS arm64 通过(30 tests、1 doctest);`cargo +nightly test --manifest-path backend/chimera-runtime/Cargo.toml -p chimera-core-manager --all-features` 通过(117 passed、1 ignored);Service `--all-targets` 检查通过;`cargo +nightly check --manifest-path backend/Cargo.toml -p chimera --all-targets` 通过。`chimera-utils` 含测试目标的 `x86_64-pc-windows-gnu` 交叉检查通过,确认 Windows helper 与两个集成测试可编译;macOS 上 Windows 测试被 `cfg(windows)` 排除,因此不能据此声称控制台事件已运行通过。Service 的 Windows 全量交叉检查在依赖 `aws-lc-sys` 的 C 编译阶段因本机缺少 `x86_64-w64-mingw32-gcc` 停止,未到 Service Rust 代码;实际 Windows 集成运行仍待有 Windows runner 的环境。一开始的 3.3.4 编译尝试暴露 API 适配不完整,已修正为 ProcessKit 3 的 `ProcessEvents`/`ErrorKind` API,最终本地测试与 utility Windows 交叉检查均通过。 - 收敛与 CI 后续:Windows runner 上确认两个真实子进程中仅目标收到 Ctrl+Break、缺失 helper 时快速走 Job Object 回退后,记录最终命令和结果。`pnpm test:backend` 不覆盖该 runtime 子模块,后续若要把该测试纳入 CI,需另行评估 L3 workflow 变更;本批不声称 CI 已执行这些断言。 + +## DIFF-069:启动顺序第一阶段——路径所有权(部分对齐) + +**Reference:** pinned `ref` at `518bf49843e1f735380e11da1ebe2a85eb8ce8c6` (read-only). +**Chimera base:** `19b1281de2fbc58186056699068931934decbe6b`. +**Scope and risk:** L2, backend initialization sequencing and filesystem roots. +**Goal:** prepare the Chimera launch path to use the *same* resolved config/data roots for pre-Tauri default configuration, logger creation and Tauri composition. This is **partial alignment**, not the complete upstream startup lifecycle. + +### Source mapping + +| `ref` implementation | Chimera implementation | Stage 1 outcome | +| --- | --- | --- | +| `backend/tauri/src/lib.rs::run`: `host_paths::discover` before other initialization | `backend/tauri/src/lib.rs::run`: `PathResolver::from_env` | Resolve once before `init_config`, pass to the Tauri setup closure | +| `backend/nyanpasu-paths/src/lib.rs::PathResolver` | `backend/tauri/src/utils/path.rs::PathResolver` | Reuse existing Chimera resolver; do **not** change its persisted paths or branding | +| `backend/tauri/src/utils/init/mod.rs::init_config(paths)` | `backend/tauri/src/utils/init/mod.rs::init_config(paths)` | Create legacy defaults only beneath the injected config root, retaining non-overwrite and best-effort semantics | +| `backend/tauri/src/utils/init/logging.rs::init(..., paths)` | `backend/tauri/src/utils/init/logging.rs::init(paths)` | Use the injected data/log root for both initial appender and future appender rotation | +| `backend/tauri/src/setup.rs::setup(..., paths, owners)` | `backend/tauri/src/setup.rs::setup(app, paths)` | Consume already-resolved paths; no independent path discovery in setup | +| `backend/tauri/src/lib.rs`: logging failure exits before building Tauri | `backend/tauri/src/lib.rs::run` | Return the startup error before continuing when path resolution or logger initialization fails | + +Only the product naming, existing config formats and Chimera Client wiring remain adapted here. No public IPC or persisted schema changes. + +### Behavior and non-goals + +1. The production path is now `run → PathResolver::from_env → init_config(&paths) → logging::init(&paths) → initialize_default_configs(&paths) → Tauri setup(&paths)`. +2. The initial defaults keep the existing `# Clash Chimera` header, existing YAML locations and corruption-evidence protections. Existing config files and symlinks must not be overwritten. +3. **Not changed:** native single-instance implementation, command parsing, pre-Tauri subprocess migration, root directory creation semantics, process exit-code policy, crash reporter, task ownership, core/service start ordering, traffic storage, silent startup, main/legacy UI and Agent. Do not mistake the partial integration for upstream parity. +4. **Known remaining difference:** Chimera's `PathResolver::from_env` still uses `utils::dirs`, which creates base directories during discovery; `ref` discovers without side effects, checks its single-instance lock, then creates roots. The current Chimera single-instance Tauri plugin remains unchanged. Aligning this boundary needs a separate stage and cold-start/double-launch platform testing. +5. **Known remaining difference:** Chimera `main` still calls `run().unwrap()` instead of the ref's typed `ExitCode` and one-shot failure presentation, and `resolve_setup` still runs service/core/storage/resource initialization. Those changes require dedicated failure, restart and host-effect tests; avoid broad changes in this slice. + +### Acceptance / tests + +- Existing isolated defaults tests and a new unit test ensure all legacy YAML defaults are written beneath the injected config root and no unrelated data root is created by default initialization. +- An unreadable config node, dangling symlink, and existing user config must retain the pre-existing recovery behavior. +- Local macOS Rust verification on this slice: full `cargo test --locked --manifest-path backend/Cargo.toml -p chimera --lib -- --test-threads=2` passed **778/778**, with 1 pre-existing ignored generated-bindings writer test. Targeted startup file safety tests **5/5**, injected log appender **1/1**, path tests **2/2** and startup failure tests **6/6** passed. Rust formatting, `cargo clippy --locked --manifest-path backend/Cargo.toml -p chimera --all-targets --all-features` (success with pre-existing warnings), tracked-document Prettier and `git diff --check` passed. After tightening the log-content assertion, its focused test passed again. An initial full-suite run failed because the isolated worktree lacked a `pnpm` executable for an existing Specta freshness test; after supplying a temporary Corepack-backed `pnpm` wrapper, the entire suite passed without changing that test or generated bindings. +- The full Windows E2E and macOS/Linux build matrix must be recorded against the new commit **before claiming a complete platform acceptance**. Pure Rust tests do not prove native single-instance or real service/TUN startup. +- Scope rollback: revert this stage's commit. No changes to `ref` or submodule gitlinks. + +### Future stages (separate changes) + +- **Stage 1b:** ref-style pure path discovery and single-instance placement; avoid directory writes from a losing instance. Needs explicit Windows/macOS/Linux double-launch and portable-mode tests. Do not copy upstream directory-file replacement without assessing user-data impact. +- **Stage 2:** migration order, safe preflight and structured failure reporting, preserving existing snapshots and recovery. +- **Stage 3:** setup lifetime owners and cancellation, explicit ownership transfer on partial failure. +- **Stage 4:** core/service startup reconciliation and runtime state, preserving Chimera Client and agent confirmation boundaries. +- **Stage 5:** main/legacy window readiness, deep links, tray and shutdown ordering. From ddad1aac71c36b01aac10f650abbf3cc8749b39e Mon Sep 17 00:00:00 2001 From: MFSGA Date: Tue, 3 Nov 2026 20:33:58 -0800 Subject: [PATCH 2/7] refactor(startup): separate path discovery from directory creation --- backend/tauri/src/lib.rs | 1 + backend/tauri/src/utils/dirs.rs | 33 ++++++----- backend/tauri/src/utils/path.rs | 98 ++++++++++++++++++++++++++++++++- docs/ref-alignment-progress.md | 13 +++++ 4 files changed, 130 insertions(+), 15 deletions(-) diff --git a/backend/tauri/src/lib.rs b/backend/tauri/src/lib.rs index 117148cad..d5b6676b4 100644 --- a/backend/tauri/src/lib.rs +++ b/backend/tauri/src/lib.rs @@ -83,6 +83,7 @@ pub fn run() -> std::io::Result<()> { // Resolve once before startup initialization, then pass the same roots to // configuration and the Tauri composition root (as in the pinned ref). let paths = PathResolver::from_env().map_err(std::io::Error::other)?; + paths.create_base_dirs().map_err(std::io::Error::other)?; init::init_config(&paths).map_err(std::io::Error::other)?; if let Some(lang) = Config::verge().latest().language.clone() { let locale = crate::bridge::verge::locale_key_from_legacy(&lang) diff --git a/backend/tauri/src/utils/dirs.rs b/backend/tauri/src/utils/dirs.rs index 4736a486d..f136cf9cf 100644 --- a/backend/tauri/src/utils/dirs.rs +++ b/backend/tauri/src/utils/dirs.rs @@ -46,10 +46,11 @@ pub fn get_portable_flag() -> bool { *crate::consts::IS_PORTABLE } -pub fn app_config_dir() -> Result { +/// Discover the config root without creating directories. The startup owner +/// explicitly creates base directories after resolving both roots. +pub fn discover_config_dir() -> Result { #[cfg(feature = "e2e")] if let Some(path) = e2e_dir("CHIMERA_E2E_CONFIG_DIR") { - create_dir_all(&path)?; return Ok(path); } @@ -76,10 +77,13 @@ pub fn app_config_dir() -> Result { None => suggest_config_dir(&APP_DIR_PLACEHOLDER) .ok_or(anyhow::anyhow!("failed to get the app config dir")), } - .and_then(|dir| { - create_dir_all(&dir)?; - Ok(dir) - }) +} + +/// Existing file/path consumers retain their original create-on-access behavior. +pub fn app_config_dir() -> Result { + let path = discover_config_dir()?; + create_dir_all(&path)?; + Ok(path) } /// profiles dir @@ -106,7 +110,7 @@ pub fn profiles_path() -> Result { Ok(app_config_dir()?.join(PROFILE_YAML)) } -fn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> { +pub(crate) fn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> { let meta = fs::metadata(dir); if let Ok(meta) = meta { if !meta.is_dir() { @@ -121,10 +125,10 @@ fn create_dir_all(dir: &PathBuf) -> Result<(), std::io::Error> { Ok(()) } -pub fn app_data_dir() -> Result { +/// Discover the data root without creating directories. +pub fn discover_data_dir() -> Result { #[cfg(feature = "e2e")] if let Some(path) = e2e_dir("CHIMERA_E2E_DATA_DIR") { - create_dir_all(&path)?; return Ok(path); } @@ -149,10 +153,13 @@ pub fn app_data_dir() -> Result { None => suggest_data_dir(&APP_DIR_PLACEHOLDER) .ok_or(anyhow::anyhow!("failed to get the app data dir")), } - .and_then(|dir| { - create_dir_all(&dir)?; - Ok(dir) - }) +} + +/// Existing file/path consumers retain their original create-on-access behavior. +pub fn app_data_dir() -> Result { + let path = discover_data_dir()?; + create_dir_all(&path)?; + Ok(path) } /// logs dir diff --git a/backend/tauri/src/utils/path.rs b/backend/tauri/src/utils/path.rs index d3683ea0a..5f57c0f60 100644 --- a/backend/tauri/src/utils/path.rs +++ b/backend/tauri/src/utils/path.rs @@ -16,8 +16,8 @@ pub struct PathResolver { impl PathResolver { pub fn from_env() -> Result { Ok(Self { - config_dir: dirs::app_config_dir()?, - data_dir: dirs::app_data_dir()?, + config_dir: dirs::discover_config_dir()?, + data_dir: dirs::discover_data_dir()?, }) } @@ -28,6 +28,26 @@ impl PathResolver { } } + /// As in ref, only the two base roots are required. Leaf directories are + /// best-effort and can be recreated by their individual consumers. + pub fn create_base_dirs(&self) -> Result<()> { + // Unlike the legacy directory helper, never delete a non-directory + // occupying a required root. Startup must fail without losing data. + std::fs::create_dir_all(&self.config_dir)?; + std::fs::create_dir_all(&self.data_dir)?; + for path in [ + self.app_profiles_dir(), + self.app_logs_dir(), + self.cache_dir(), + ] { + // Do not remove a user's file if an optional leaf is obstructed. + if let Err(error) = std::fs::create_dir_all(&path) { + tracing::error!(%error, path = %path.display(), "failed to prepare optional startup directory"); + } + } + Ok(()) + } + pub fn app_config_dir(&self) -> &Path { &self.config_dir } @@ -101,6 +121,80 @@ mod tests { PathResolver::with_base_dirs(PathBuf::from("/cfg"), PathBuf::from("/data")) } + #[test] + fn base_directory_creation_is_explicit_and_idempotent() { + let temp = tempfile::tempdir().unwrap(); + let config = temp.path().join("configuration"); + let data = temp.path().join("application-data"); + let r = PathResolver::with_base_dirs(config.clone(), data.clone()); + + assert!( + !config.exists(), + "resolving paths must not create configuration" + ); + assert!( + !data.exists(), + "resolving paths must not create application data" + ); + r.create_base_dirs().unwrap(); + r.create_base_dirs().unwrap(); + + assert!(config.is_dir()); + assert!(data.is_dir()); + assert!(r.app_profiles_dir().is_dir()); + assert!(r.app_logs_dir().is_dir()); + assert!(r.cache_dir().is_dir()); + } + + #[test] + fn required_root_failure_does_not_create_other_root() { + let temp = tempfile::tempdir().unwrap(); + let blocker = temp.path().join("not-a-directory"); + std::fs::write(&blocker, b"preserve me").unwrap(); + let data = temp.path().join("new-data"); + let r = PathResolver::with_base_dirs(blocker.join("config"), data.clone()); + + assert!(r.create_base_dirs().is_err()); + assert_eq!(std::fs::read(&blocker).unwrap(), b"preserve me"); + assert!(!data.exists()); + } + + #[test] + fn existing_required_root_files_are_not_removed() { + for blocked_root in ["config", "data"] { + let temp = tempfile::tempdir().unwrap(); + let config = temp.path().join("config"); + let data = temp.path().join("data"); + let blocked = temp.path().join(blocked_root); + std::fs::write(&blocked, b"preserve original user bytes").unwrap(); + let r = PathResolver::with_base_dirs(config, data); + + assert!(r.create_base_dirs().is_err()); + assert_eq!( + std::fs::read(&blocked).unwrap(), + b"preserve original user bytes" + ); + } + } + + #[test] + fn blocked_optional_leaf_is_preserved() { + let temp = tempfile::tempdir().unwrap(); + let config = temp.path().join("config"); + let data = temp.path().join("data"); + std::fs::create_dir_all(&data).unwrap(); + std::fs::write(data.join("cache"), b"original cache file").unwrap(); + let r = PathResolver::with_base_dirs(config, data.clone()); + + r.create_base_dirs().unwrap(); + assert_eq!( + std::fs::read(data.join("cache")).unwrap(), + b"original cache file" + ); + assert!(r.app_profiles_dir().is_dir()); + assert!(r.app_logs_dir().is_dir()); + } + #[test] fn config_derived_paths_join_config_dir() { let r = resolver(); diff --git a/docs/ref-alignment-progress.md b/docs/ref-alignment-progress.md index 7b47f474f..cf73d6544 100644 --- a/docs/ref-alignment-progress.md +++ b/docs/ref-alignment-progress.md @@ -2158,3 +2158,16 @@ Only the product naming, existing config formats and Chimera Client wiring remai - **Stage 3:** setup lifetime owners and cancellation, explicit ownership transfer on partial failure. - **Stage 4:** core/service startup reconciliation and runtime state, preserving Chimera Client and agent confirmation boundaries. - **Stage 5:** main/legacy window readiness, deep links, tray and shutdown ordering. + +## DIFF-070:启动顺序第 1b 阶段——纯路径发现与显式目录准备(部分对齐) + +- **目标与范围**:沿固定 `ref` 的 `PathResolver::discover` → `create_base_dirs` 边界,把 Tauri 入口从「解析路径时立即创建目录」调整为「一次发现 config/data 两个根目录,再显式准备目录」。这只是第一阶段的路径处理部分,**不是单实例启动顺序完全对齐**。风险等级 L2(启动路径、文件系统副作用)。 +- **基线**:Chimera 父提交 `9d31a8d0f523326fd62d518dfccebda62db7854e`(第 1 阶段);只读 `ref` gitlink `518bf49843e1f735380e11da1ebe2a85eb8ce8c6`,工作区干净、未更新。 +- **映射**:`ref/backend/nyanpasu-paths/src/lib.rs::PathResolver::{discover,create_base_dirs}` → `backend/tauri/src/utils/path.rs::PathResolver::{from_env,create_base_dirs}`;`ref/backend/tauri/src/lib.rs::run` 的目录准备步骤 → Chimera `backend/tauri/src/lib.rs::run`。Chimera `backend/tauri/src/utils/dirs.rs` 将 `discover_config_dir` / `discover_data_dir` 分离为不写磁盘的读取路径,既有 `app_config_dir` / `app_data_dir` 仍保留旧的 create-on-access 兼容契约,不更改 IPC 和持久化路径。 +- **启动行为**:在已经注入同一份 `PathResolver` 的前提下, + 1. `from_env` 只发现两个根目录,不创建 config/data/logs/cache/profiles;E2E 环境变量路径同样只发现,不立即创建。 + 2. `run` 显式调用 `create_base_dirs()`,配置和数据根目录均必须准备成功;profiles、logs 和 cache 为 best-effort。日志初始化与配置默认值创建仍使用同一份 resolver,属于之前阶段的改动。 + 3. 可选目录如果已被普通文件挡住,只记录错误、保留用户文件,不主动删除;这是对 ref 在该异常路径会先删除文件行为的一处保护性偏离。必要根目录也改用不会删除既有普通文件的 `std::fs::create_dir_all`:如果用户文件占用了根目录路径,启动明确报错并保留文件,而不是沿用旧 helper 的替换行为。这是针对数据损失风险的明确安全偏离;旧 `dirs::app_config_dir/app_data_dir` 兼容访问器本批未改。 +- **明确保留的差异与后续**:`ref` 在创建根目录前由独立 `single_instance::SingleInstance` 检查,Chimera 目前使用 Tauri `tauri_plugin_single_instance` 在 Builder 中检测并**转发二次启动到已有主/Legacy 窗口**。直接复制 ref 的前置锁会让二次启动无法正常唤起窗口;本阶段**不替换该插件、不改第二实例退出或深链接机制**,因此第二实例可能仍会在插件阻止它之前准备目录。若后续要对齐单实例时序,须先设计可验证的转发协议,覆盖 Windows/macOS/Linux、portable 和多用户边界,不能仅为了减少启动目录写入而退化既有体验。 +- **验证**:定向 `cargo test --locked --manifest-path backend/Cargo.toml -p chimera --lib utils::path::tests -- --test-threads=1` 包含显式且幂等的目录准备、必要目录失败时不创建另一根目录、必要根目录被普通文件占用时保护原始字节、可选叶目录文件保护以及原 config/data 路径布局。安全保护补强后,完整 `cargo test --locked --manifest-path backend/Cargo.toml -p chimera --lib -- --test-threads=2` **782/782 通过、1 项既有忽略**,其中定向路径测试 **6/6 通过**;`cargo clippy --locked --manifest-path backend/Cargo.toml -p chimera --all-targets --all-features` 通过(保留既有 warnings);Rustfmt、Prettier 与 `git diff --check` 均通过。新 SHA 的远程 CI 与 Windows E2E 仍待运行;纯 Rust 单测不能证明原生单实例和真实桌面启动。 +- **回滚**:回退本批 `lib.rs`、`utils/dirs.rs`、`utils/path.rs` 与本条记录即可,保留第 1 阶段已验证的 `PathResolver` 注入;无 Schema、Service/TUN、用户网络状态或子模块变更。 From cedc7dc89a408eb7bef7b76f0dffd9e4bf70e68f Mon Sep 17 00:00:00 2001 From: MFSGA <193414067+MFSGA@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:53:20 +0000 Subject: [PATCH 3/7] chore: bump version to v0.28.0 --- CHANGELOG.md | 210 ++++++++++++++++++++++++++++++++ backend/tauri/tauri.conf.json | 2 +- frontend/chimera/package.json | 2 +- frontend/interface/package.json | 2 +- frontend/ui/package.json | 2 +- frontend/utils/package.json | 2 +- package.json | 2 +- scripts/package.json | 2 +- 8 files changed, 217 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5004209fa..6b5236134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,213 @@ +## [0.28.0] - 2026-11-04 09:53:18 + +### ✅ Testing + +- **e2e:** Require one visible theme option instead of unique menu item by @MFSGA in [#580](https://github.com/MFSGA/Chimera/pull/580) + +- **e2e:** Capture theme popup before cleanup on failure by @MFSGA in [#579](https://github.com/MFSGA/Chimera/pull/579) + +- **e2e:** Avoid slow WebDriver scroll command before mouse actions by @MFSGA + +- **e2e:** Poll language route stability outside WebView timers by @MFSGA in [#578](https://github.com/MFSGA/Chimera/pull/578) + +- **e2e:** Use native WebDriver mouse actions for visible controls by @MFSGA + +- **e2e:** Expose stage before long-running Windows UI actions by @MFSGA in [#577](https://github.com/MFSGA/Chimera/pull/577) + +- **e2e:** Retry theme menu via real keyboard and record stages by @MFSGA + +- **e2e:** Choose theme menu items through real pointer input by @MFSGA in [#576](https://github.com/MFSGA/Chimera/pull/576) + +- **e2e:** Click only the current visible Profile details link by @MFSGA + +- **e2e:** Record pointer hit evidence on detail navigation failure by @MFSGA in [#575](https://github.com/MFSGA/Chimera/pull/575) + +- **e2e:** Diagnose Profile detail navigation after real click by @MFSGA in [#574](https://github.com/MFSGA/Chimera/pull/574) + +- **e2e:** Click visible Profile folder details through WebDriver by @MFSGA in [#573](https://github.com/MFSGA/Chimera/pull/573) + +- **e2e:** Wait for the actual visible Profile folder heading by @MFSGA in [#572](https://github.com/MFSGA/Chimera/pull/572) + +- **e2e:** Target visible folder details and preserve failure cause by @MFSGA in [#571](https://github.com/MFSGA/Chimera/pull/571) + +- **e2e:** Exercise Profile folder navigation through visible UI by @MFSGA in [#570](https://github.com/MFSGA/Chimera/pull/570) + +- **e2e:** Stabilize Windows pointer and settings flows by @MFSGA + +- **e2e:** Exercise Profile folders through visible UI by @MFSGA + +- **e2e:** Await settled Profile card motion before layout check by @MFSGA + +- **e2e:** Measure the actual responsive Profile card grid by @MFSGA + +- **e2e:** Capture route and pointer hit evidence on layout failures by @MFSGA + +- **i18n:** Handle structured plural messages in locale checks by @MFSGA + +- **lua:** Isolate memory guard from instruction timeout by @MFSGA + +### ✨ Features + +- **backup:** Create verifiable migration snapshots and safe restore by @MFSGA + +- **dashboard:** Settle layout selection after resize by @MFSGA + +- **jobs:** Preserve typed journal errors for safe recovery by @MFSGA + +- **migration:** Guard revision state and snapshot before applying steps by @MFSGA + +- **profiles:** Align folder routes and atomic scoped imports with ref by @MFSGA + +- **profiles:** Align folder sections and management UI with ref by @MFSGA + +- **profiles:** Align folder layout model actor IPC and query with ref by @MFSGA + +- **profiles:** Align card features subscription metrics and content summary with ref by @MFSGA + +- **profiles:** Align transform add menu with ref by @MFSGA + +- **profiles:** Align detail loading and missing states with ref by @MFSGA + +- **startup:** Report typed migration and client setup failures by @MFSGA + +- **storage:** Align window-scoped KV state with ref (#548) by @MFSGA + +- **system-proxy:** Align shared effects ownership with ref (#539) by @MFSGA + +- **traffic:** Align Flow topology with ref Sankey (#533) by @MFSGA + +- **tray:** Expose script mode for premium core by @MFSGA + +### 🐛 Bug Fixes + +- **ci:** Align nightly Linux RPM versions and macOS updater paths by @MFSGA + +- **config:** Prevent silent reinitialization of unreadable app settings by @MFSGA + +- **deps:** Sync backend lock with runtime by @MFSGA + +- **e2e:** Satisfy lint braces in route polling by @MFSGA + +- **jobs:** Preserve journal evidence on startup failures by @MFSGA + +- **logging:** Filter debug terminal output with the file layer by @MFSGA + +- **profiles:** Align remote imports and folder routes by @MFSGA + +- **recovery:** Classify IO and redb failures before preserving data by @MFSGA + +- **router:** Keep exiting routes out of pointer hit testing by @MFSGA + +- **router:** Freeze the outgoing route state like ref by @MFSGA + +- **router:** Retain exiting route presence across transitions by @MFSGA + +- **runtime:** Retain Lua guard and filtered debug logging by @MFSGA + +- **runtime:** Point service submodule to CI fix by @MFSGA + +- **runtime:** Align reused pid cleanup with upstream by @MFSGA + +- **session:** Share safe state recovery between migration and client by @MFSGA + +- **state:** Synchronize committed changes across windows by @MFSGA + +- **traffic:** Generate analysis links from visible query state by @MFSGA + +- **traffic:** Retain search state across analysis navigation by @MFSGA + +- **tray:** Serialize snapshot refreshes by @MFSGA + +- **ui:** Stabilize route transitions and traffic navigation by @MFSGA + +- **ui:** Isolate nested motion from routed exit barrier by @MFSGA + +- **ui:** Release exiting route presence after slide-out by @MFSGA + +- **updater:** Build stable release asset URLs by @MFSGA + +- **widgets:** Expose dashboard shortcut card headers by @MFSGA + +- **window:** Keep WebView2 visibility in sync (#540) by @MFSGA + +- **windows:** Gracefully stop managed cores by @MFSGA + +### 📚 Documentation + +- **governance:** Require ref-first user E2E and CI visual evidence by @MFSGA + +- **governance:** Honor explicit maintainer-directed manual updates by @MFSGA in [#568](https://github.com/MFSGA/Chimera/pull/568) + +- **lessons:** Record dependency release-age failure by @MFSGA + +- **lessons:** Verify v0.27.1 release fixes by @MFSGA + +- **ref:** Record migration safety acceptance and remaining gaps by @MFSGA + +### 🔨 Refactor + +- **core:** Reuse core manager from runtime submodule by @MFSGA + +- **frontend:** Align shared menus and profile module paths with ref by @MFSGA + +- **profiles:** Align active card gradient and actions with ref by @MFSGA + +- **profiles:** Align card actions and content editor with ref by @MFSGA + +- **profiles:** Align transform chain row primitives with ref by @MFSGA + +- **profiles:** Extract shared transform chain draft and editor by @MFSGA + +- **profiles:** Align transform and used-by detail sections with ref by @MFSGA + +- **profiles:** Align subscription option controls with ref by @MFSGA + +- **profiles:** Align sync history modules with ref by @MFSGA + +- **profiles:** Align deletion and subscription sections with ref by @MFSGA + +- **profiles:** Align file detail section with ref by @MFSGA + +- **profiles:** Align import and detail header with ref by @MFSGA + +- **runtime:** Align service helper wiring with ref by @MFSGA + +- **startup:** Separate path discovery from directory creation by @MFSGA in [#582](https://github.com/MFSGA/Chimera/pull/582) + +- **startup:** Reuse resolved paths across config logging and setup by @MFSGA in [#581](https://github.com/MFSGA/Chimera/pull/581) + +- **ui:** Align shared UI primitives with ref and prepare NixOS by @MFSGA + +- **utils:** Reuse utility crate from runtime submodule by @MFSGA + +### 🧹 Miscellaneous Tasks + +- **deps:** Update dependency knip to v6.44.1 by @renovate-bot in [#558](https://github.com/MFSGA/Chimera/pull/558) + +- **deps:** Update rust crate redb to v4.4.0 by @renovate-bot + +- **deps:** Merge Knip 6.43 update by @MFSGA + +- **deps:** Merge Tauri package alignment by @MFSGA + +- **deps:** Align collection UI libraries with ref by @MFSGA + +- **deps:** Refresh frontend dependency set (#549) by @MFSGA + +- **deps:** Update tanstack-router monorepo (#535) by @MFSGA + +- **ref:** Sync upstream reference baseline by @MFSGA + +- **ref:** Sync baseline to upstream main by @MFSGA + +- **ref:** Sync upstream main to 8e1084e4 by @MFSGA + +- **ref:** Update baseline to eb81fda5 by @MFSGA + +--- + +**Full Changelog**: https://github.com/MFSGA/Chimera/compare/v0.27.1...v0.28.0 + ## [0.27.1] - 2026-10-26 11:11:26 ### 🐛 Bug Fixes diff --git a/backend/tauri/tauri.conf.json b/backend/tauri/tauri.conf.json index afea6f4ff..7fdf5588a 100644 --- a/backend/tauri/tauri.conf.json +++ b/backend/tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Chimera", - "version": "0.27.1", + "version": "0.28.0", "identifier": "com.chimera.app", "build": { "beforeDevCommand": "pnpm run web:dev", diff --git a/frontend/chimera/package.json b/frontend/chimera/package.json index 7ed720e59..ed0c61f61 100644 --- a/frontend/chimera/package.json +++ b/frontend/chimera/package.json @@ -1,7 +1,7 @@ { "name": "chimera-ui", "private": true, - "version": "0.27.1", + "version": "0.28.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/interface/package.json b/frontend/interface/package.json index 0a661356e..55161d8e0 100644 --- a/frontend/interface/package.json +++ b/frontend/interface/package.json @@ -1,6 +1,6 @@ { "name": "@chimera/interface", - "version": "0.27.1", + "version": "0.28.0", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", "require": { diff --git a/frontend/ui/package.json b/frontend/ui/package.json index a15579893..d4442eb9c 100644 --- a/frontend/ui/package.json +++ b/frontend/ui/package.json @@ -1,6 +1,6 @@ { "name": "@chimera/ui", - "version": "0.27.1", + "version": "0.28.0", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/frontend/utils/package.json b/frontend/utils/package.json index 56116ea99..7013bb49c 100644 --- a/frontend/utils/package.json +++ b/frontend/utils/package.json @@ -1,6 +1,6 @@ { "name": "@chimera/utils", - "version": "0.27.1", + "version": "0.28.0", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/package.json b/package.json index 8a629b19e..5ac52a8ab 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chimera", "private": true, - "version": "0.27.1", + "version": "0.28.0", "type": "module", "scripts": { "dev": "run-p -r web:devtools tauri:dev", diff --git a/scripts/package.json b/scripts/package.json index 6a53f0566..442759588 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,7 +1,7 @@ { "name": "@chimera/scripts", "type": "module", - "version": "0.27.1", + "version": "0.28.0", "dependencies": { "yargs": "18.0.0", "@actions/github": "9.1.1" From 7932af9adab9cefe3970394b17427649d581ddf7 Mon Sep 17 00:00:00 2001 From: MFSGA <193414067+MFSGA@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:00:15 +0000 Subject: [PATCH 4/7] chore: bump version to v0.28.1 --- CHANGELOG.md | 210 ++++++++++++++++++++++++++++++++ backend/tauri/tauri.conf.json | 2 +- frontend/chimera/package.json | 2 +- frontend/interface/package.json | 2 +- frontend/ui/package.json | 2 +- frontend/utils/package.json | 2 +- package.json | 2 +- scripts/package.json | 2 +- 8 files changed, 217 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b5236134..5abf635fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,213 @@ +## [0.28.1] - 2026-11-04 10:00:13 + +### ✅ Testing + +- **e2e:** Require one visible theme option instead of unique menu item by @MFSGA in [#580](https://github.com/MFSGA/Chimera/pull/580) + +- **e2e:** Capture theme popup before cleanup on failure by @MFSGA in [#579](https://github.com/MFSGA/Chimera/pull/579) + +- **e2e:** Avoid slow WebDriver scroll command before mouse actions by @MFSGA + +- **e2e:** Poll language route stability outside WebView timers by @MFSGA in [#578](https://github.com/MFSGA/Chimera/pull/578) + +- **e2e:** Use native WebDriver mouse actions for visible controls by @MFSGA + +- **e2e:** Expose stage before long-running Windows UI actions by @MFSGA in [#577](https://github.com/MFSGA/Chimera/pull/577) + +- **e2e:** Retry theme menu via real keyboard and record stages by @MFSGA + +- **e2e:** Choose theme menu items through real pointer input by @MFSGA in [#576](https://github.com/MFSGA/Chimera/pull/576) + +- **e2e:** Click only the current visible Profile details link by @MFSGA + +- **e2e:** Record pointer hit evidence on detail navigation failure by @MFSGA in [#575](https://github.com/MFSGA/Chimera/pull/575) + +- **e2e:** Diagnose Profile detail navigation after real click by @MFSGA in [#574](https://github.com/MFSGA/Chimera/pull/574) + +- **e2e:** Click visible Profile folder details through WebDriver by @MFSGA in [#573](https://github.com/MFSGA/Chimera/pull/573) + +- **e2e:** Wait for the actual visible Profile folder heading by @MFSGA in [#572](https://github.com/MFSGA/Chimera/pull/572) + +- **e2e:** Target visible folder details and preserve failure cause by @MFSGA in [#571](https://github.com/MFSGA/Chimera/pull/571) + +- **e2e:** Exercise Profile folder navigation through visible UI by @MFSGA in [#570](https://github.com/MFSGA/Chimera/pull/570) + +- **e2e:** Stabilize Windows pointer and settings flows by @MFSGA + +- **e2e:** Exercise Profile folders through visible UI by @MFSGA + +- **e2e:** Await settled Profile card motion before layout check by @MFSGA + +- **e2e:** Measure the actual responsive Profile card grid by @MFSGA + +- **e2e:** Capture route and pointer hit evidence on layout failures by @MFSGA + +- **i18n:** Handle structured plural messages in locale checks by @MFSGA + +- **lua:** Isolate memory guard from instruction timeout by @MFSGA + +### ✨ Features + +- **backup:** Create verifiable migration snapshots and safe restore by @MFSGA + +- **dashboard:** Settle layout selection after resize by @MFSGA + +- **jobs:** Preserve typed journal errors for safe recovery by @MFSGA + +- **migration:** Guard revision state and snapshot before applying steps by @MFSGA + +- **profiles:** Align folder routes and atomic scoped imports with ref by @MFSGA + +- **profiles:** Align folder sections and management UI with ref by @MFSGA + +- **profiles:** Align folder layout model actor IPC and query with ref by @MFSGA + +- **profiles:** Align card features subscription metrics and content summary with ref by @MFSGA + +- **profiles:** Align transform add menu with ref by @MFSGA + +- **profiles:** Align detail loading and missing states with ref by @MFSGA + +- **startup:** Report typed migration and client setup failures by @MFSGA + +- **storage:** Align window-scoped KV state with ref (#548) by @MFSGA + +- **system-proxy:** Align shared effects ownership with ref (#539) by @MFSGA + +- **traffic:** Align Flow topology with ref Sankey (#533) by @MFSGA + +- **tray:** Expose script mode for premium core by @MFSGA + +### 🐛 Bug Fixes + +- **ci:** Align nightly Linux RPM versions and macOS updater paths by @MFSGA + +- **config:** Prevent silent reinitialization of unreadable app settings by @MFSGA + +- **deps:** Sync backend lock with runtime by @MFSGA + +- **e2e:** Satisfy lint braces in route polling by @MFSGA + +- **jobs:** Preserve journal evidence on startup failures by @MFSGA + +- **logging:** Filter debug terminal output with the file layer by @MFSGA + +- **profiles:** Align remote imports and folder routes by @MFSGA + +- **recovery:** Classify IO and redb failures before preserving data by @MFSGA + +- **router:** Keep exiting routes out of pointer hit testing by @MFSGA + +- **router:** Freeze the outgoing route state like ref by @MFSGA + +- **router:** Retain exiting route presence across transitions by @MFSGA + +- **runtime:** Retain Lua guard and filtered debug logging by @MFSGA + +- **runtime:** Point service submodule to CI fix by @MFSGA + +- **runtime:** Align reused pid cleanup with upstream by @MFSGA + +- **session:** Share safe state recovery between migration and client by @MFSGA + +- **state:** Synchronize committed changes across windows by @MFSGA + +- **traffic:** Generate analysis links from visible query state by @MFSGA + +- **traffic:** Retain search state across analysis navigation by @MFSGA + +- **tray:** Serialize snapshot refreshes by @MFSGA + +- **ui:** Stabilize route transitions and traffic navigation by @MFSGA + +- **ui:** Isolate nested motion from routed exit barrier by @MFSGA + +- **ui:** Release exiting route presence after slide-out by @MFSGA + +- **updater:** Build stable release asset URLs by @MFSGA + +- **widgets:** Expose dashboard shortcut card headers by @MFSGA + +- **window:** Keep WebView2 visibility in sync (#540) by @MFSGA + +- **windows:** Gracefully stop managed cores by @MFSGA + +### 📚 Documentation + +- **governance:** Require ref-first user E2E and CI visual evidence by @MFSGA + +- **governance:** Honor explicit maintainer-directed manual updates by @MFSGA in [#568](https://github.com/MFSGA/Chimera/pull/568) + +- **lessons:** Record dependency release-age failure by @MFSGA + +- **lessons:** Verify v0.27.1 release fixes by @MFSGA + +- **ref:** Record migration safety acceptance and remaining gaps by @MFSGA + +### 🔨 Refactor + +- **core:** Reuse core manager from runtime submodule by @MFSGA + +- **frontend:** Align shared menus and profile module paths with ref by @MFSGA + +- **profiles:** Align active card gradient and actions with ref by @MFSGA + +- **profiles:** Align card actions and content editor with ref by @MFSGA + +- **profiles:** Align transform chain row primitives with ref by @MFSGA + +- **profiles:** Extract shared transform chain draft and editor by @MFSGA + +- **profiles:** Align transform and used-by detail sections with ref by @MFSGA + +- **profiles:** Align subscription option controls with ref by @MFSGA + +- **profiles:** Align sync history modules with ref by @MFSGA + +- **profiles:** Align deletion and subscription sections with ref by @MFSGA + +- **profiles:** Align file detail section with ref by @MFSGA + +- **profiles:** Align import and detail header with ref by @MFSGA + +- **runtime:** Align service helper wiring with ref by @MFSGA + +- **startup:** Separate path discovery from directory creation by @MFSGA in [#582](https://github.com/MFSGA/Chimera/pull/582) + +- **startup:** Reuse resolved paths across config logging and setup by @MFSGA in [#581](https://github.com/MFSGA/Chimera/pull/581) + +- **ui:** Align shared UI primitives with ref and prepare NixOS by @MFSGA + +- **utils:** Reuse utility crate from runtime submodule by @MFSGA + +### 🧹 Miscellaneous Tasks + +- **deps:** Update dependency knip to v6.44.1 by @renovate-bot in [#558](https://github.com/MFSGA/Chimera/pull/558) + +- **deps:** Update rust crate redb to v4.4.0 by @renovate-bot + +- **deps:** Merge Knip 6.43 update by @MFSGA + +- **deps:** Merge Tauri package alignment by @MFSGA + +- **deps:** Align collection UI libraries with ref by @MFSGA + +- **deps:** Refresh frontend dependency set (#549) by @MFSGA + +- **deps:** Update tanstack-router monorepo (#535) by @MFSGA + +- **ref:** Sync upstream reference baseline by @MFSGA + +- **ref:** Sync baseline to upstream main by @MFSGA + +- **ref:** Sync upstream main to 8e1084e4 by @MFSGA + +- **ref:** Update baseline to eb81fda5 by @MFSGA + +--- + +**Full Changelog**: https://github.com/MFSGA/Chimera/compare/v0.27.1...v0.28.1 + ## [0.28.0] - 2026-11-04 09:53:18 ### ✅ Testing diff --git a/backend/tauri/tauri.conf.json b/backend/tauri/tauri.conf.json index 7fdf5588a..6d55fdc34 100644 --- a/backend/tauri/tauri.conf.json +++ b/backend/tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Chimera", - "version": "0.28.0", + "version": "0.28.1", "identifier": "com.chimera.app", "build": { "beforeDevCommand": "pnpm run web:dev", diff --git a/frontend/chimera/package.json b/frontend/chimera/package.json index ed0c61f61..bdd34f309 100644 --- a/frontend/chimera/package.json +++ b/frontend/chimera/package.json @@ -1,7 +1,7 @@ { "name": "chimera-ui", "private": true, - "version": "0.28.0", + "version": "0.28.1", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/interface/package.json b/frontend/interface/package.json index 55161d8e0..ad0eaec88 100644 --- a/frontend/interface/package.json +++ b/frontend/interface/package.json @@ -1,6 +1,6 @@ { "name": "@chimera/interface", - "version": "0.28.0", + "version": "0.28.1", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", "require": { diff --git a/frontend/ui/package.json b/frontend/ui/package.json index d4442eb9c..55596fbef 100644 --- a/frontend/ui/package.json +++ b/frontend/ui/package.json @@ -1,6 +1,6 @@ { "name": "@chimera/ui", - "version": "0.28.0", + "version": "0.28.1", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/frontend/utils/package.json b/frontend/utils/package.json index 7013bb49c..e6dc369c5 100644 --- a/frontend/utils/package.json +++ b/frontend/utils/package.json @@ -1,6 +1,6 @@ { "name": "@chimera/utils", - "version": "0.28.0", + "version": "0.28.1", "type": "module", "main": "src/index.ts", "types": "src/index.ts", diff --git a/package.json b/package.json index 5ac52a8ab..f154ce670 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chimera", "private": true, - "version": "0.28.0", + "version": "0.28.1", "type": "module", "scripts": { "dev": "run-p -r web:devtools tauri:dev", diff --git a/scripts/package.json b/scripts/package.json index 442759588..73d099a88 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,7 +1,7 @@ { "name": "@chimera/scripts", "type": "module", - "version": "0.28.0", + "version": "0.28.1", "dependencies": { "yargs": "18.0.0", "@actions/github": "9.1.1" From 772bceaa56c6f2b0d00f1f9d89996b8f6e817caf Mon Sep 17 00:00:00 2001 From: MFSGA Date: Wed, 4 Nov 2026 18:16:37 +0800 Subject: [PATCH 5/7] feat(core): align local host native store lifecycle --- LESSONS.md | 21 +++ backend/Cargo.lock | 15 +- backend/chimera-runtime | 2 +- backend/tauri/src/core/actor_v2/local_host.rs | 150 +++++++++++++++--- .../tauri/src/core/actor_v2/local_runtime.rs | 20 ++- backend/tauri/src/lib.rs | 9 ++ backend/tauri/src/setup.rs | 21 ++- backend/tauri/src/startup_failure.rs | 65 +++++++- docs/ref-alignment-runtime.md | 8 + ref | 2 +- 10 files changed, 272 insertions(+), 41 deletions(-) diff --git a/LESSONS.md b/LESSONS.md index c26b432a9..126835709 100644 --- a/LESSONS.md +++ b/LESSONS.md @@ -71,6 +71,14 @@ | LESSON-20261031-010 | 2026-10-31 | Profiles / URL / ref alignment | `new URL()` 接受 FTP/File/JavaScript 等非订阅协议;共享 ref 的 HTTP(S) Zod schema 并加 CI 回归 | Mitigated | | LESSON-20261031-011 | 2026-10-31 | Profiles / Actor / remote import | 未知文件夹的远程导入曾先 Fetch 后拒绝;按 ref 在下载前校验,测试严格断言 Fetch 0 次 | Mitigated | | LESSON-20261031-012 | 2026-10-31 | NixOS / Btrfs / Rust tests | Tauri 测试二进制链接在 Btrfs `reserve_bytes` 阻塞;Cargo check 成功,完整 Client 测试待环境复验 | Open | +| LESSON-20261103-001 | 2026-11-03 | Runtime / Windows / ref | 原样迁入原生存储路径测试在 Windows 将 `/data/ui` 错判为正确路径;改为 `Utf8Path::join` 生成预期值,重新运行 12/12 通过 | Mitigated | +| LESSON-20261104-001 | 2026-11-04 | Rust workspace / Cargo target / pnpm store | 默认 D 盘耗尽导致完整测试链接失败;恢复空间后在默认位置完成全量测试,pnpm 元数据策略检查仍告警 | Mitigated | + +### LESSON-20261103-001:原生路径重写测试不能假定 Unix 分隔符 + +- 证据:在 Windows Rust stable 1.99.0,初次运行 `cargo test -p chimera-core-manager --lib native_store::`,12 项中 2 项因预期 `/data/ui`、`/data/providers/local.yaml` 与实际 `/data\\ui` 等平台路径不同而失败;存储租约/迁移测试 10 项通过。 +- 修正:只调整 `crates/chimera-core-manager/src/native_store/paths.rs` 中的测试预期,使用 `Utf8Path::join` 生成同平台路径,未改变 `rewrite_paths` 生产逻辑。重新运行原生存储测试 12/12 通过,随后 Runtime 全库 121 通过、1 忽略。 +- 防复发:跨平台迁移参考用例要区分文件系统路径(平台分隔符)与 URL/代理传输路径(协议分隔符);保留测试对 `/ws`、证书 PEM 和凭证字段不重写的断言。本轮未执行 Linux/macOS 真实桌面 E2E。 ### LESSON-20261031-001:Profile Layout 未知 UID 不能静默插入文件夹 @@ -1061,3 +1069,16 @@ backend/Cargo.toml -p chimera --lib core::clash::proxies::tests:: -- --nocapture - 验证命令、实际用例数量、结果、未覆盖/跳过项:无锁模式的 `cargo test --manifest-path ./backend/Cargo.toml --workspace --all-features` 仅将 `simd-json` 锁定版本更新到 0.19.0,完整 backend workspace 测试通过;随后 CI 同一入口 `pnpm test:backend`(执行 `cargo test --locked --manifest-path ./backend/Cargo.toml --workspace --all-features`)在本机通过。修复后新 SHA 的 GitHub 全平台 CI 仍待验证。 - 风险/回滚和清理证据(如涉及宿主状态):只更新 Cargo 锁文件,不修改运行时用户配置或宿主状态;若需回滚同步,需同时恢复旧 submodule gitlink 与旧锁文件。 - 关联重复事件、后续防复发措施、L3 门禁调整审批链接(如有):每次更新 runtime submodule 时检查其被主仓引用的 Cargo manifests,并在提交前运行 `pnpm test:backend`(其中 `--locked` 用于发现未同步的父锁文件)。本次未修改工作流。 + +### LESSON-20261104-001:完整工作区测试需同时检查 Cargo 输出盘和 pnpm store + +- 日期与时区:2026-11-04 Asia/Shanghai +- 状态:Mitigated(恢复原盘空间后默认构建目录的 workspace 测试通过;pnpm 元数据策略检查仍有网络告警) +- 严重度:Low(本地验证受环境资源影响;没有确认产品断言失败) +- 模块:Rust workspace / Cargo target / pnpm dependency store +- 关联运行日志(脱敏):`cargo test --locked --quiet --manifest-path backend/Cargo.toml --workspace --all-features`。 +- 真实异常、复现步骤与证据:首次使用默认 `backend/target` 时,D 盘剩余空间约 1 MiB,Tauri 测试二进制链接报 `rustc-LLVM ERROR: IO failure on output stream: no space on device`,没有到达完整测试执行。使用临时 C 盘 target 后,工作区测试推进至 Tauri 777 项测试中的 696 进度;`specta_export::tests::typescript_bindings_are_fresh` 调用 pnpm 时,其提示 C 盘全局 store 无法硬链接到 D 盘工程并改用 `D:\.pnpm-store`,随后 `knip@6.44.1` tarball 请求多次超时。该次运行在反复重试前被中止,没有报告断言失败。 +- 确认的根因与排除项:链接 `ENOSPC` 是 D 盘空间不足;pnpm 的 store 实际落在 D 盘,跨盘构建 target 并未隔离依赖缓存的空间需求。tarball 超时出现在同一空间紧张的环境中,不能单凭它认定代码或依赖本身有缺陷。 +- 修复与验证:维护者已清出原构建盘空间;仅删除本次创建的临时 C 盘 Cargo target,按 `cargo metadata` 确认默认输出为 `backend/target` 后,`cargo test --locked --quiet --manifest-path backend/Cargo.toml --workspace --all-features` 在默认目录退出码 0。Tauri 测试二进制 **777 passed / 1 ignored**,其他 workspace 测试与 doctests 均通过;其中包含 local host 配置优先级/错误测试和启动诊断测试。`specta_export::tests::typescript_bindings_are_fresh` 的内部 pnpm 打印 supply-chain metadata fetch 超时及“可能需要安装”的警告,但随后 Prettier 成功处理生成文件,绑定相等断言通过(1/1)。发布版本快进后,pnpm 1,642 条锁文件策略检查通过;`pnpm test:manifest` 的 Deno 测试阶段因持续无输出被中止,相关 assertions 未验证。不要把 pnpm 内部元数据告警或未完成的 Deno 测试记成成功。 +- 防复发:运行全量 Cargo 测试前同时检查 `backend/target` 和 pnpm store 所在盘的剩余空间;若切换 `CARGO_TARGET_DIR`,仍需为项目盘上的 pnpm store 留出空间。代理/缓存失败时记录实际下载目标和有效 store,不把未完成运行称为测试通过。 +- 风险/回滚和清理证据:未触碰用户配置、宿主代理/TUN 或服务进程;清理目标仅为本次创建的临时 Cargo 构建目录。 diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 9de780fd9..f76ae339e 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "chimera-clash-api" -version = "1.0.1" +version = "1.1.0" dependencies = [ "backon", "chimera-utils", @@ -1263,20 +1263,22 @@ dependencies = [ [[package]] name = "chimera-core-manager" -version = "1.0.1" +version = "1.1.0" dependencies = [ "camino", - "chimera-clash-api 1.0.1", + "chimera-clash-api 1.1.0", "chimera-service-core-metadata", "chimera-utils", "chrono", "enumset", + "libc", "parking_lot", "schemars 1.2.2", "serde", "serde_json", "serde_yaml_ng", "specta", + "tempfile", "thiserror 2.0.21", "tokio", "tokio-util", @@ -1305,7 +1307,7 @@ dependencies = [ [[package]] name = "chimera-ipc" -version = "1.10.2" +version = "1.11.0" dependencies = [ "anyhow", "axum", @@ -1368,7 +1370,7 @@ dependencies = [ [[package]] name = "chimera-service-core-metadata" -version = "1.0.1" +version = "1.1.0" dependencies = [ "constcat", "enumset", @@ -1392,8 +1394,9 @@ dependencies = [ [[package]] name = "chimera-utils" -version = "0.1.2" +version = "0.2.0" dependencies = [ + "atomic_enum", "backon", "camino", "chimera_utils", diff --git a/backend/chimera-runtime b/backend/chimera-runtime index 2cb116099..81e93043c 160000 --- a/backend/chimera-runtime +++ b/backend/chimera-runtime @@ -1 +1 @@ -Subproject commit 2cb1160996f912b536418cb62bda470d097c1190 +Subproject commit 81e93043c29cb60425077f4419dcbd60d6f2b8c9 diff --git a/backend/tauri/src/core/actor_v2/local_host.rs b/backend/tauri/src/core/actor_v2/local_host.rs index 3c23577c1..03615195a 100644 --- a/backend/tauri/src/core/actor_v2/local_host.rs +++ b/backend/tauri/src/core/actor_v2/local_host.rs @@ -3,48 +3,79 @@ //! Chimera keeps its application paths and core selector, while the shared //! lifecycle transaction is owned by `chimera-core-manager`. -#[cfg(target_os = "macos")] use std::sync::Arc; -use anyhow::Result; +use anyhow::Result as AnyResult; use camino::Utf8PathBuf; use chimera_core_manager::{ - ControlOptions, CoreControl, CoreKind, CoreManager as RuntimeManager, CoreSpec, LocalIpcPolicy, - ManagerOptions, + ControlOptions, CoreControl, CoreKind, CoreManager, CoreSpec, LocalIpcPolicy, ManagerOptions, + native_store::{FsNativeStore, StoreOwner, legacy_kind}, }; +use chimera_utils::process::AtomicStopStrategy; +use snafu::{ResultExt, Snafu}; use crate::{config::chimera::ClashCore, utils::path::PathResolver}; -/// Construct the local host from the application's resolved directories. +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to inspect {}", path.display()))] + InspectConfig { + path: std::path::PathBuf, + source: std::io::Error, + }, + #[snafu(display("failed to read core kind from {}", path.display()))] + ReadCoreKind { + path: std::path::PathBuf, + source: chimera_core_manager::native_store::StoreError, + }, + #[snafu(display("failed to build core manager"))] + BuildManager { source: chimera_core_manager::Error }, + #[snafu(display("path is not valid UTF-8: {}", path.display()))] + InvalidPath { path: std::path::PathBuf }, +} + +/// Builds the local core host with the stop strategy shared by its process supervisors. /// -/// The private manager runtime directory holds epochs and effective configs; -/// staging is a sibling because caller-owned config bytes are materialized -/// only for one control operation. The core working directory remains the -/// user's data directory so existing GeoIP and GeoSite assets stay visible. -pub(crate) async fn build(paths: &PathResolver) -> Result { +/// # Errors +/// Returns an error if configuration inspection or core manager construction fails. +pub async fn build( + paths: &PathResolver, + shutdown_helper: &std::path::Path, + stop_strategy: Arc, +) -> Result { let runtime_root = paths.app_config_dir().join("runtime"); - let manager_options = ManagerOptions { - runtime_dir: Some(to_utf8(runtime_root.join("control"))?), + let options = ManagerOptions { + runtime_dir: Some( + to_utf8(runtime_root.join("control")).map_err(|path| Error::InvalidPath { path })?, + ), local_ipc_policy: LocalIpcPolicy::Disable, - #[cfg(windows)] - windows_shutdown_helper: Some(std::env::current_exe()?), - // A local macOS host may need the user to answer a DNS authorization - // dialog. Service hosts already run with administrator privileges. + windows_shutdown_helper: Some(shutdown_helper.to_owned()), + // Local macOS mode may require the user to authorize DNS changes. #[cfg(target_os = "macos")] dns_timeout: std::time::Duration::from_secs(60), ..ManagerOptions::default() }; + let working_dir = + to_utf8(paths.app_data_dir().to_owned()).map_err(|path| Error::InvalidPath { path })?; + let native_store = Arc::new(FsNativeStore::new( + working_dir.clone(), + StoreOwner::current(), + configured_core_kind(paths)?, + )); + let manager = CoreManager::builder(options) + .native_store(native_store) + .stop_strategy(stop_strategy); - let manager = RuntimeManager::builder(manager_options); #[cfg(target_os = "macos")] let manager = manager.dns_controller(Arc::new( chimera_core_manager::dns::macos::MacosDnsController::new( "State:/Network/Service/chimera-dns/DNS".into(), ), )); - let manager = manager.build().await?; - let source_dir = to_utf8(runtime_root.join("staging"))?; - let working_dir = to_utf8(paths.app_data_dir().to_owned())?; + + let manager = manager.build().await.context(BuildManagerSnafu)?; + let source_dir = + to_utf8(runtime_root.join("staging")).map_err(|path| Error::InvalidPath { path })?; Ok(CoreControl::spawn( manager, @@ -52,10 +83,24 @@ pub(crate) async fn build(paths: &PathResolver) -> Result { )) } +fn configured_core_kind(paths: &PathResolver) -> Result, Error> { + let typed_config = paths.application_config_path(); + let config_path = if typed_config.try_exists().context(InspectConfigSnafu { + path: typed_config.as_path(), + })? { + typed_config + } else { + paths.chimera_config_path() + }; + legacy_kind(config_path.as_path()).context(ReadCoreKindSnafu { + path: config_path.as_path(), + }) +} + /// Resolve an application core choice to the executable identity understood /// by the shared process manager. `ChimeraClient` deliberately keeps its own /// kind even though it accepts the clash-rs command line. -pub(crate) fn core_spec(core: &ClashCore) -> Result { +pub(crate) fn core_spec(core: &ClashCore) -> AnyResult { core_spec_with(core, |core, core_type| { if *core == ClashCore::ChimeraClientAlpha { let binary_name = format!("chimera-client-alpha{}", std::env::consts::EXE_SUFFIX); @@ -72,7 +117,7 @@ fn core_spec_with( &ClashCore, &chimera_utils::core::CoreType, ) -> std::io::Result, -) -> Result { +) -> AnyResult { let core_type: chimera_utils::core::CoreType = core.into(); let kind = match core { ClashCore::ClashPremium => CoreKind::ClashPremium, @@ -81,7 +126,8 @@ fn core_spec_with( ClashCore::ChimeraClient => CoreKind::ChimeraClient, ClashCore::ChimeraClientAlpha => CoreKind::ChimeraClient, }; - let binary_path = to_utf8(find_binary(core, &core_type)?)?; + let binary_path = to_utf8(find_binary(core, &core_type)?) + .map_err(|path| anyhow::anyhow!("path is not valid UTF-8: {}", path.to_string_lossy()))?; Ok(CoreSpec { kind, @@ -91,9 +137,8 @@ fn core_spec_with( }) } -fn to_utf8(path: std::path::PathBuf) -> Result { +fn to_utf8(path: std::path::PathBuf) -> Result { Utf8PathBuf::from_path_buf(path) - .map_err(|path| anyhow::anyhow!("path is not valid UTF-8: {}", path.to_string_lossy())) } #[cfg(test)] @@ -106,7 +151,15 @@ mod tests { let paths = PathResolver::with_base_dirs(root.path().join("config"), root.path().join("data")); - let control = build(&paths).await.unwrap(); + let control = build( + &paths, + &root.path().join("helper.exe"), + Arc::new(AtomicStopStrategy::new( + chimera_utils::process::StopStrategy::default(), + )), + ) + .await + .unwrap(); assert!(matches!( control.status().state, @@ -116,6 +169,51 @@ mod tests { control.shutdown().await.unwrap(); } + #[tokio::test] + async fn typed_config_takes_precedence_and_legacy_parse_errors_are_preserved() { + let root = tempfile::TempDir::new().unwrap(); + let paths = + PathResolver::with_base_dirs(root.path().join("config"), root.path().join("data")); + std::fs::create_dir_all(paths.app_config_dir()).unwrap(); + std::fs::create_dir_all(paths.app_data_dir()).unwrap(); + std::fs::write(paths.application_config_path(), "core: chimera-client\n").unwrap(); + std::fs::write(paths.chimera_config_path(), "core: mihomo\n").unwrap(); + assert_eq!( + configured_core_kind(&paths).unwrap(), + Some(CoreKind::ChimeraClient), + "typed config must take precedence over the legacy core choice" + ); + let stop_strategy = Arc::new(AtomicStopStrategy::new( + chimera_utils::process::StopStrategy::default(), + )); + + let control = build( + &paths, + &root.path().join("helper.exe"), + Arc::clone(&stop_strategy), + ) + .await + .expect("the typed config must be selected before the legacy core choice"); + control.shutdown().await.unwrap(); + + std::fs::remove_file(paths.application_config_path()).unwrap(); + assert_eq!( + configured_core_kind(&paths).unwrap(), + Some(CoreKind::Mihomo), + "legacy config must be used when the typed config is absent" + ); + std::fs::write(paths.chimera_config_path(), "invalid: [\n").unwrap(); + let result = build(&paths, &root.path().join("helper.exe"), stop_strategy).await; + let Err(error) = result else { + panic!("the malformed legacy config must be rejected"); + }; + let legacy_path = paths.chimera_config_path(); + assert!(matches!( + error, + Error::ReadCoreKind { ref path, .. } if path == &legacy_path + )); + } + #[test] fn core_spec_preserves_each_supported_brand_and_executable_mapping() { let root = tempfile::TempDir::new().unwrap(); diff --git a/backend/tauri/src/core/actor_v2/local_runtime.rs b/backend/tauri/src/core/actor_v2/local_runtime.rs index 11f55dff8..a3801fde2 100644 --- a/backend/tauri/src/core/actor_v2/local_runtime.rs +++ b/backend/tauri/src/core/actor_v2/local_runtime.rs @@ -1263,6 +1263,20 @@ mod tests { status::{ConfigRevisionInfo, RevisionIdInfo}, }; + async fn build_local_control( + paths: &crate::utils::path::PathResolver, + ) -> chimera_core_manager::CoreControl { + local_host::build( + paths, + &paths.app_config_dir().join("helper.exe"), + Arc::new(chimera_utils::process::AtomicStopStrategy::new( + chimera_utils::process::StopStrategy::default(), + )), + ) + .await + .unwrap() + } + // Contract: with an isolated app root and a newly built local host, status // must come from CoreControl as stopped/normal; a host that silently kept // reading the unused legacy manager would not know its manager's timestamp. @@ -1275,7 +1289,7 @@ mod tests { root.path().join("config"), root.path().join("data"), ); - let control = local_host::build(&paths).await.unwrap(); + let control = build_local_control(&paths).await; let runtime_paths = RuntimePaths::new( camino::Utf8PathBuf::from_path_buf(root.path().join("config/runtime/clash.yaml")) .unwrap(), @@ -1414,7 +1428,7 @@ mod tests { root.path().join("config"), root.path().join("data"), ); - let control = local_host::build(&paths).await.unwrap(); + let control = build_local_control(&paths).await; let runtime_paths = RuntimePaths::new( camino::Utf8PathBuf::from_path_buf(root.path().join("config/runtime/clash.yaml")) .unwrap(), @@ -1555,7 +1569,7 @@ mod tests { root.path().join("config"), root.path().join("data"), ); - let control = local_host::build(&paths).await.unwrap(); + let control = build_local_control(&paths).await; let runtime_paths = RuntimePaths::new( camino::Utf8PathBuf::from_path_buf(root.path().join("config/runtime/clash.yaml")) .unwrap(), diff --git a/backend/tauri/src/lib.rs b/backend/tauri/src/lib.rs index d5b6676b4..3f2fe5d9a 100644 --- a/backend/tauri/src/lib.rs +++ b/backend/tauri/src/lib.rs @@ -212,7 +212,16 @@ pub fn run() -> std::io::Result<()> { #[cfg(windows)] { let app_handle = app.handle().clone(); + let stop_strategy = std::sync::Arc::clone( + app.state::>() + .inner(), + ); shutdown_hook::setup_shutdown_hook(move || { + // Forced Windows session shutdown must not wait for a graceful core exit. + stop_strategy.store( + chimera_utils::process::StopStrategy::Kill, + std::sync::atomic::Ordering::Release, + ); crate::log_err!(utils::exit::shutdown_before_exit_blocking( &app_handle )); diff --git a/backend/tauri/src/setup.rs b/backend/tauri/src/setup.rs index f3929ce3a..7f61559dc 100644 --- a/backend/tauri/src/setup.rs +++ b/backend/tauri/src/setup.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use anyhow::Context as _; use chimera_traffic::{RedbTrafficStore, TrafficStore}; +use chimera_utils::process::{AtomicStopStrategy, StopStrategy}; use tauri::Manager; use crate::{ @@ -86,9 +87,23 @@ pub fn setup(app: &mut tauri::App, paths: PathResolver) -> anyhow::Result<()> { app.manage(tokio::sync::RwLock::new( crate::core::updater::UpdaterManager::new(), )); - let local_control = - tauri::async_runtime::block_on(crate::core::actor_v2::local_host::build(&paths)) - .context("failed to initialize the local core control plane")?; + let stop_strategy = Arc::new(AtomicStopStrategy::new(StopStrategy::default())); + app.manage(Arc::clone(&stop_strategy)); + // The desktop executable handles windows_shutdown_helper_entry in main.rs. + let local_control = crate::startup_failure::report_boundary( + (|| { + let shutdown_helper = + std::env::current_exe().context("failed to resolve the desktop shutdown helper")?; + tauri::async_runtime::block_on(crate::core::actor_v2::local_host::build( + &paths, + &shutdown_helper, + stop_strategy, + )) + .context("failed to initialize the local core control plane") + })(), + crate::startup_failure::StartupStage::CoreHost, + &paths, + )?; let core_facade = Arc::new(crate::core::actor_v2::CoreFacade::new_local_with_control( local_control, runtime_paths.clone(), diff --git a/backend/tauri/src/startup_failure.rs b/backend/tauri/src/startup_failure.rs index cdc36e93c..575928634 100644 --- a/backend/tauri/src/startup_failure.rs +++ b/backend/tauri/src/startup_failure.rs @@ -8,7 +8,6 @@ use std::{error::Error as StdError, path::PathBuf}; -use anyhow::Context as _; use serde::{Deserialize, Serialize}; use crate::{ @@ -39,6 +38,7 @@ pub(crate) enum StartupStage { Jobs, MigrationPrepare, MigrationRun, + CoreHost, TypedClient, } @@ -113,10 +113,12 @@ impl StartupFailure { StartupStage::MigrationPrepare | StartupStage::MigrationRun => { FailureKind::MigrationFailed } + StartupStage::CoreHost => FailureKind::Internal, StartupStage::TypedClient => FailureKind::UserConfig, }; let mut path = None; let mut revision = None; + let mut host_user_config = false; let mut chain = Vec::new(); let mut current = Some(error); while let Some(error) = current { @@ -196,6 +198,28 @@ impl StartupFailure { { path = Some(actual.clone()); } + if let Some(host_error) = + error.downcast_ref::() + { + match host_error { + crate::core::actor_v2::local_host::Error::InspectConfig { + path: actual, + .. + } => { + path = Some(actual.clone()); + } + crate::core::actor_v2::local_host::Error::ReadCoreKind { + path: actual, .. + } => { + path = Some(actual.clone()); + host_user_config = true; + } + crate::core::actor_v2::local_host::Error::BuildManager { .. } => {} + crate::core::actor_v2::local_host::Error::InvalidPath { path: actual } => { + path = Some(actual.clone()); + } + } + } current = error.source(); } @@ -208,6 +232,7 @@ impl StartupFailure { _ if revision.as_ref().is_some_and(RevisionFailure::is_newer) => { FailureKind::NewerVersion } + _ if host_user_config && stage == StartupStage::CoreHost => FailureKind::UserConfig, _ => default_kind, }; let path = path.or_else(|| { @@ -215,6 +240,7 @@ impl StartupFailure { StartupStage::Jobs => paths.jobs_path(), StartupStage::MigrationPrepare | StartupStage::MigrationRun + | StartupStage::CoreHost | StartupStage::TypedClient => paths.app_config_dir().to_owned(), }) }); @@ -431,6 +457,43 @@ mod tests { assert!(!user.failure.detail.is_empty()); } + #[tokio::test] + async fn malformed_typed_core_config_is_reported_at_the_host_boundary() { + let (temp, paths) = fixture(); + let typed_config = paths.application_config_path(); + std::fs::write(&typed_config, "invalid: [\n").unwrap(); + std::fs::write(paths.chimera_config_path(), "core: mihomo\n").unwrap(); + let result = crate::core::actor_v2::local_host::build( + &paths, + &temp.path().join("helper.exe"), + std::sync::Arc::new(chimera_utils::process::AtomicStopStrategy::new( + chimera_utils::process::StopStrategy::default(), + )), + ) + .await; + let Err(error) = result else { + panic!("the malformed typed config must be rejected"); + }; + + let report = Report::from_error(&error, StartupStage::CoreHost, &paths); + assert_eq!(report.failure.kind, FailureKind::UserConfig); + assert_eq!(report.failure.path, Some(typed_config)); + assert!(report.failure.detail.contains("failed to read core kind")); + } + + #[test] + fn invalid_host_path_reports_the_exact_path() { + let (_temp, paths) = fixture(); + let actual = paths.app_data_dir().join("unrepresentable-native-path"); + let error = crate::core::actor_v2::local_host::Error::InvalidPath { + path: actual.clone(), + }; + + let report = Report::from_error(&error, StartupStage::CoreHost, &paths); + assert_eq!(report.failure.kind, FailureKind::Internal); + assert_eq!(report.failure.path, Some(actual)); + } + #[test] fn backup_dir_is_advertised_only_for_a_committed_existing_snapshot() { use crate::core::backup::{self, BackupKind, BackupRequest, StorageSource}; diff --git a/docs/ref-alignment-runtime.md b/docs/ref-alignment-runtime.md index d58b705e0..797401794 100644 --- a/docs/ref-alignment-runtime.md +++ b/docs/ref-alignment-runtime.md @@ -1,5 +1,13 @@ # Runtime config path and rollback alignment +## Local core host / native store / stop-strategy migration (2026-11-03) + +- **Reference and intent:** Local `ref` HEAD `5ed3c019e13015fc5b06fc8d7069b330607d9036`; its pinned `backend/nyanpasu-runtime` commit is `a27c3fcc1ed57980034f25084613baa98efc01c4`. The runtime reference submodule was not checked out locally; the corresponding files were read directly at that exact commit. Correct a half-migrated `local_host::build` signature by migrating the **supporting runtime behavior**, not reverting to the old one-argument builder. Risk **L2** (core process lifecycle, native data migration, failure recovery). No user configuration, host networking, or Service installation was operated on. +- **Path/symbol map:** `ref/backend/nyanpasu-core/src/actor_v2/local_host.rs::build` → `backend/tauri/src/core/actor_v2/local_host.rs::build`; `ref/backend/tauri/src/setup.rs` → `backend/tauri/src/setup.rs` and Windows shutdown entry in `backend/tauri/src/lib.rs`; `nyanpasu-utils/src/process/{supervisor,pid_file}.rs` → `backend/chimera-runtime/crates/chimera-utils/src/process/`; `nyanpasu-core-manager/src/{native_store,manager,instance,runtime/process,config,kind}.rs` → corresponding `backend/chimera-runtime/crates/chimera-core-manager/src/`. Native-store migration copies the upstream complete `native_store/` module. Startup passes an `Arc` and explicit shutdown executable to the manager, uses a directory lease and per-core native home, rewrites file paths in prepared runtime configs, isolates configuration checks, and records/confirms process death before releasing a lease. +- **Product-preserving adaptations:** Chimera's `PathResolver` returns `PathBuf`, converted at the manager boundary to `Utf8PathBuf` with a structured error. The legacy fallback file is `chimera-config.yaml`; the Windows helper executable is the Chimera desktop process, whose entrypoint handles `windows_shutdown_helper_entry`. The existing macOS `chimera-dns` key and local DNS-authorization timeout are preserved. Chimera Client stable/Alpha continue to share the `ChimeraClient` kind, with their own native cache namespace and YAML cache semantics (not silently folded into ClashRust). Migrated path tests now use platform-specific path separators. +- **Tests actually executed:** Windows MSVC stable 1.99.0: `cargo check --offline --manifest-path backend/Cargo.toml -p chimera --lib` and `--tests` succeeded, as did `cargo check -p chimera-core-manager --tests` and `cargo check -p chimera-service --tests` in the runtime checkout. Runtime library unit tests: **121 passed, 1 ignored**; chimera-utils library unit tests: **3 passed**. Native-store suite's initial two Windows-only path assertions failed and were repaired by comparing platform-resolved expected paths; the subsequent full runtime suite passed. On 2026-11-04, before fast-forwarding from `ddad1aac` to release bump `7932af9a`, default-target `cargo test --locked --quiet --manifest-path backend/Cargo.toml --workspace --all-features` exited 0: the Tauri test binary reported **777 passed / 1 ignored**, with other workspace tests and doctests also passing. The new local-host config tests and CoreHost startup-error tests ran in that suite; after the fast-forward, the focused CoreHost startup tests were rerun and passed **8/8**. The nested `specta_export::tests::typescript_bindings_are_fresh` printed a pnpm supply-chain metadata fetch timeout/warning, then Prettier completed and the generated-vs-checked-in binding assertion passed; pnpm's independent metadata policy fetch is not claimed as verified. After the fast-forward, pnpm's 1,642-entry lockfile policy check passed, but `pnpm test:manifest` was stopped while Deno tests were running without output; its Deno assertions are **unverified**. `cargo fmt ... -- --check`, Markdown Prettier and `git diff --check` passed. **No desktop, host proxy/TUN, real core restart, cache-upgrade, or Service integration verification** is claimed. Full workspace rustfmt check is currently blocked by pre-existing CRLF/newline-style differences in untouched files; do not normalize unrelated files. +- **Remaining difference and acceptance:** Chimera Service's existing builder does not yet inject a `NativeStore` lease, whereas the upstream design expects cross-host coordination; review its privilege/owner source and IPC compatibility before enabling the same lease there. Native-store production acceptance also requires isolated Windows/macOS/Linux desktop and Core/Service process handoff tests, including fresh and legacy caches, failure/rollback, forced session shutdown and restart. The current migration is **partial cross-host alignment**, despite successfully compiling the desktop host. Rollback only this scoped implementation and its lockfile changes after reviewing newly written per-core native cache data; do not delete user data or reset pre-existing dirty `ref`/runtime submodule pointers. + Checked against `ref` commit `0c7b15b3c61db390db21cffb63d29805abd380a1` on 2026-10-13; the reference worktree was clean. The shared path and candidate flow maps to `ref/backend/tauri/src/client/runtime.rs::RuntimePaths`, `create_candidate`, and `promote_candidate`. Chimera keeps those paths and names: the runtime product is `/runtime/clash-config.yaml`, while candidate files live under `/runtime/.candidates`. diff --git a/ref b/ref index 518bf4984..5ed3c019e 160000 --- a/ref +++ b/ref @@ -1 +1 @@ -Subproject commit 518bf49843e1f735380e11da1ebe2a85eb8ce8c6 +Subproject commit 5ed3c019e13015fc5b06fc8d7069b330607d9036 From a795272b3b5c028fcf482513279c685bc23b9e59 Mon Sep 17 00:00:00 2001 From: MFSGA Date: Thu, 5 Nov 2026 07:14:42 +0800 Subject: [PATCH 6/7] chore(deps): update dependency meta-json-schema to v1.19.33 (#556) Co-authored-by: Renovate --- frontend/chimera/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/chimera/package.json b/frontend/chimera/package.json index bdd34f309..6826046df 100644 --- a/frontend/chimera/package.json +++ b/frontend/chimera/package.json @@ -90,7 +90,7 @@ "@types/react-dom": "19.3.0", "@vitejs/plugin-react": "6.1.2", "filesize": "11.0.28", - "meta-json-schema": "1.19.32", + "meta-json-schema": "1.19.33", "monaco-yaml": "5.5.1", "nanoid": "6.0.2", "postcss": "8.5.32", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8ac76c839..53b53ba08 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -483,8 +483,8 @@ importers: specifier: 11.0.28 version: 11.0.28 meta-json-schema: - specifier: 1.19.32 - version: 1.19.32 + specifier: 1.19.33 + version: 1.19.33 monaco-yaml: specifier: 5.5.1 version: 5.5.1(monaco-editor@0.57.0) @@ -6541,8 +6541,8 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - meta-json-schema@1.19.32: - resolution: {integrity: sha512-oDIYDGnGy8u2RPnz/SH83C00QnAHHWtny5oqALmKY5yf8wqK5bWIBu76rmxuY1CfDPMOpbAKd5ZpOWejbE/wng==} + meta-json-schema@1.19.33: + resolution: {integrity: sha512-xNjwPSo4MB8LRh0nraZ3bcbuXdzaGk4ME8piYPBW/JuwBWAd2vLF4puiaIadr2KgSYtwlHmog/en4RtGqzE+hQ==} engines: {node: '>=22.12.0', pnpm: '>=10'} micromark-core-commonmark@2.0.3: @@ -14389,7 +14389,7 @@ snapshots: merge2@1.4.1: {} - meta-json-schema@1.19.32: {} + meta-json-schema@1.19.33: {} micromark-core-commonmark@2.0.3: dependencies: From ce8fbddd91613b64d162882c83802ec02433d86d Mon Sep 17 00:00:00 2001 From: Renovate Date: Wed, 4 Nov 2026 23:16:22 +0000 Subject: [PATCH 7/7] chore(deps): update rust crate crc32fast to v1.5.4 --- backend/Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index f76ae339e..f22c1f116 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1710,9 +1710,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.3" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +checksum = "5f97d3416d5337d15c4b0d78c8c91df922ca143f57ec20e2d1c188333047ef04" dependencies = [ "cfg-if", ]