From 51440020f62108894bc1e245ea06d9c1bacac8f1 Mon Sep 17 00:00:00 2001 From: dupf Date: Mon, 8 Jun 2026 23:57:38 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(windows):=20=E4=BF=AE=E5=A4=8D=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E6=A8=A1=E5=9E=8B=E9=85=8D=E7=BD=AE=E6=97=B6=E6=A0=88?= =?UTF-8?q?=E6=BA=A2=E5=87=BA=E9=97=AA=E5=B4=A9=20(0xc00000fd)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows 上 Tauri 异步线程默认 ~1MiB 栈,序列化体积较大、嵌套较深的 Config 为 TOML 时会触发栈溢出导致进程闪退。改为在 Windows 下使用 8MiB 栈的专用线程执行 Config 保存;同时用 toml 序列化生成 active_workspace.toml,避免 Windows 路径反斜杠破坏 TOML,并让 expand_tilde_path 回退到 USERPROFILE。 Co-authored-by: Cursor --- apps/omninova-tauri/src-tauri/src/lib.rs | 16 ++++++++------- crates/omninova-core/src/config/loader.rs | 25 ++++++++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/apps/omninova-tauri/src-tauri/src/lib.rs b/apps/omninova-tauri/src-tauri/src/lib.rs index 8f9d5230..60e7014e 100644 --- a/apps/omninova-tauri/src-tauri/src/lib.rs +++ b/apps/omninova-tauri/src-tauri/src/lib.rs @@ -337,9 +337,7 @@ async fn save_setup_config( let mut next = setup_config_to_core(current, config)?; let next_gateway_url = format!("http://{}:{}", next.gateway.host, next.gateway.port); - if let Err(error) = save_config_with_fallback(&mut next) { - eprintln!("[config warning] {error}"); - } + save_config_with_fallback(&mut next)?; runtime.set_config(next).await.map_err(|e| e.to_string())?; if current_gateway_url != next_gateway_url { @@ -1095,15 +1093,19 @@ fn normalize_optional_string(value: Option) -> Option { }) } +fn user_home_dir() -> Option { + std::env::var_os("HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("USERPROFILE").map(PathBuf::from)) +} + fn expand_tilde_path(value: &str) -> PathBuf { if value == "~" { - return std::env::var_os("HOME") - .map(PathBuf::from) - .unwrap_or_else(|| PathBuf::from(value)); + return user_home_dir().unwrap_or_else(|| PathBuf::from(value)); } if let Some(rest) = value.strip_prefix("~/") { - if let Some(home) = std::env::var_os("HOME").map(PathBuf::from) { + if let Some(home) = user_home_dir() { return home.join(rest); } } diff --git a/crates/omninova-core/src/config/loader.rs b/crates/omninova-core/src/config/loader.rs index 64438279..047df005 100644 --- a/crates/omninova-core/src/config/loader.rs +++ b/crates/omninova-core/src/config/loader.rs @@ -6,6 +6,9 @@ use tracing::info; const APP_DIR_NAME: &str = ".omninova"; const CONFIG_FILE_NAME: &str = "config.toml"; const ACTIVE_WORKSPACE_FILE: &str = "active_workspace.toml"; +/// Windows async/Tauri worker threads often use ~1 MiB stacks; Config TOML +/// serialization is deeply nested and can overflow without a larger stack. +const CONFIG_SAVE_STACK_BYTES: usize = 8 * 1024 * 1024; /// Resolve the config directory with the following priority: /// 1. `OMNINOVA_CONFIG_DIR` env var @@ -107,6 +110,19 @@ impl Config { /// Save the current config to disk as TOML. pub fn save(&self) -> Result<()> { + if cfg!(target_os = "windows") { + let cfg = self.clone(); + return std::thread::Builder::new() + .stack_size(CONFIG_SAVE_STACK_BYTES) + .spawn(move || cfg.save_inner()) + .map_err(|e| anyhow::anyhow!("failed to spawn config save thread: {e}"))? + .join() + .map_err(|_| anyhow::anyhow!("config save thread panicked"))?; + } + self.save_inner() + } + + fn save_inner(&self) -> Result<()> { let content = toml::to_string_pretty(self) .context("Failed to serialize config to TOML")?; @@ -143,11 +159,14 @@ impl Config { .config_path .parent() .unwrap_or(&self.config_path) - .to_string_lossy(); + .to_string_lossy() + .to_string(); + let mut table = toml::Table::new(); + table.insert("config_dir".to_string(), toml::Value::String(dir_str)); + let body = toml::to_string(&table).context("Failed to serialize active workspace pointer")?; let content = format!( - "# Auto-generated – points to the active OmniNova workspace\nconfig_dir = \"{}\"\n", - dir_str + "# Auto-generated – points to the active OmniNova workspace\n{body}\n" ); std::fs::write(&active_path, content)?; Ok(()) From fcd3d217aee4fc82a70384f8d24f02becff3d6b7 Mon Sep 17 00:00:00 2001 From: dupf Date: Tue, 9 Jun 2026 00:36:11 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(windows):=20=E5=BC=82=E6=AD=A5=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E6=94=B9=E7=94=A8=208MiB=20worker=20?= =?UTF-8?q?=E6=A0=88=EF=BC=8C=E5=BD=BB=E5=BA=95=E4=BF=AE=E5=A4=8D=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E9=85=8D=E7=BD=AE=E6=A0=88=E6=BA=A2=E5=87=BA=E9=97=AA?= =?UTF-8?q?=E5=B4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.1.6.1 仅兜住了 Config::save() 一处。保存流程中反序列化命令入参、 setup_config_to_core 构建与 validate 配置等步骤仍跑在 Tauri 默认小栈 (~1MiB) 异步线程上,对超大且深度嵌套的 Config 做 serde (反)序列化时 仍会栈溢出 (0xC00000FD)。在 run() 最开始安装带 8MiB worker 栈的 tokio 运行时并 tauri::async_runtime::set,覆盖所有命令处理路径。 Co-authored-by: Cursor --- apps/omninova-tauri/src-tauri/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/omninova-tauri/src-tauri/src/lib.rs b/apps/omninova-tauri/src-tauri/src/lib.rs index 60e7014e..238561eb 100644 --- a/apps/omninova-tauri/src-tauri/src/lib.rs +++ b/apps/omninova-tauri/src-tauri/src/lib.rs @@ -1169,8 +1169,27 @@ fn display_provider_name(id: &str) -> String { } } +/// Worker-thread stack size for the async runtime that backs Tauri commands. +/// +/// The `Config` struct is large (~8.7 KiB) and very deeply nested, so serde +/// (de)serialization to/from TOML/JSON consumes a lot of stack. On Windows the +/// default worker-thread stack (~1 MiB) overflows during config save, crashing +/// the process with `0xC00000FD` (STATUS_STACK_OVERFLOW). An 8 MiB stack keeps +/// every command handler (which may (de)serialize `Config`) well within bounds. +const ASYNC_RUNTIME_STACK_BYTES: usize = 8 * 1024 * 1024; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + // Install a tokio runtime with larger worker stacks *before* anything uses + // the async runtime, so all Tauri command handlers run with enough stack. + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .thread_stack_size(ASYNC_RUNTIME_STACK_BYTES) + .build() + .expect("Failed to build async runtime"); + let runtime: &'static tokio::runtime::Runtime = Box::leak(Box::new(runtime)); + tauri::async_runtime::set(runtime.handle().clone()); + omninova_core::init().expect("Failed to initialize core"); let config = Config::load_or_init().expect("Failed to load config"); From 3fa9e81d35cdf2c39d92c44bc8578e0d809e7b59 Mon Sep 17 00:00:00 2001 From: dupf Date: Tue, 9 Jun 2026 01:17:56 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(windows):=20=E9=93=BE=E6=8E=A5=E5=99=A8?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=208MiB=20=E6=A0=88=E9=A2=84=E7=95=99?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E5=9B=9E=E4=BC=A0=E9=98=B6=E6=AE=B5?= =?UTF-8?q?=E6=A0=88=E6=BA=A2=E5=87=BA=E9=97=AA=E5=B4=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仅调大 tokio 异步运行时栈仍不够:Tauri 在反序列化命令入参、以及把命令 结果回传/序列化时使用 IPC/主线程,这些线程以 OS 默认栈大小创建 (PE SizeOfStackReserve,MSVC 默认约 1MiB),对超大且深度嵌套的 Config 做 serde 时仍会栈溢出 (0xC00000FD)。在 build.rs 中为 windows-msvc 目标 通过 /STACK:8388608 将可执行文件默认栈预留量提升到 8MiB;以 0 栈大小 创建的线程(主线程、Tauri/wry 内部线程、IPC 回传线程)均继承该值,覆盖 全部 Config serde 路径。 Co-authored-by: Cursor --- apps/omninova-tauri/src-tauri/build.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/omninova-tauri/src-tauri/build.rs b/apps/omninova-tauri/src-tauri/build.rs index eb063ab9..85ebe62a 100644 --- a/apps/omninova-tauri/src-tauri/build.rs +++ b/apps/omninova-tauri/src-tauri/build.rs @@ -1,6 +1,21 @@ fn main() { tauri_build::build(); + // Windows: raise the executable's default thread stack reserve to 8 MiB. + // + // The `Config` struct is large and deeply nested, so serde (de)serialization + // to/from JSON/TOML is stack-hungry. Tauri deserializes command arguments and + // serializes results on its IPC/main threads — created with the OS default + // stack size (PE `SizeOfStackReserve`, ~1 MiB by default on MSVC). That caused + // a `0xC00000FD` (STATUS_STACK_OVERFLOW) crash when saving the model config. + // Threads created with stack size 0 inherit this reserve, so this covers the + // main thread, Tauri/wry internal threads and the IPC response path. + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_env = std::env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default(); + if target_os == "windows" && target_env == "msvc" { + println!("cargo:rustc-link-arg-bins=/STACK:8388608"); + } + let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); let workspace_root = manifest_dir.join("../../.."); let profile = std::env::var("PROFILE").unwrap_or_else(|_| "debug".into());