From a5ed767fecc2869e233762c55b6883e2f3057806 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 22:33:08 -0700 Subject: [PATCH 01/11] Use process-wrap to put the sidecar in a Job Object / process group. On Windows, force-killing or force-closing the Tauri host left the Node.js sidecar (and its node-pty grandchildren) running as orphans that locked target/debug/node.exe and caused the next cargo build to fail with PermissionDenied. The kill-on-exit path was gated on RunEvent::Exit, which doesn't fire reliably across every close path (tauri-apps/tauri#10555), and tauri-plugin-shell deliberately doesn't manage child lifetime for you (tauri-apps/plugins-workspace#3062). Replace the Rust-side spawn/kill with std::process::Command wrapped by process-wrap: JobObject on Windows (KILL_ON_JOB_CLOSE), process group on Unix. The OS now guarantees the sidecar tree dies with the host regardless of which lifecycle events fired. tauri-plugin-shell stays registered because the frontend still uses its open() in updater.ts. Verified by force-killing mouseterm.exe with Stop-Process -Force and confirming the sidecar PID disappears immediately. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/Cargo.lock | 117 +++++++++++++-- standalone/src-tauri/Cargo.toml | 4 +- standalone/src-tauri/src/lib.rs | 242 ++++++++++++++++++++------------ 3 files changed, 259 insertions(+), 104 deletions(-) diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index db79e7b..5202e0c 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -309,6 +309,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -1940,7 +1946,7 @@ dependencies = [ name = "mouseterm" version = "0.9.0" dependencies = [ - "libc", + "process-wrap", "serde", "serde_json", "tauri", @@ -2006,6 +2012,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -2644,6 +2662,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process-wrap" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" +dependencies = [ + "indexmap 2.13.0", + "nix", + "tracing", + "windows 0.62.2", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -3623,7 +3653,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3705,7 +3735,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -3864,7 +3894,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -3889,7 +3919,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -4251,9 +4281,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tracing-core" version = "0.1.36" @@ -4687,7 +4729,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -4711,7 +4753,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -4767,11 +4809,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -4783,6 +4837,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -4817,7 +4880,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -4864,6 +4938,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -5002,6 +5086,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -5315,7 +5408,7 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", diff --git a/standalone/src-tauri/Cargo.toml b/standalone/src-tauri/Cargo.toml index 75d8338..0b5190b 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -22,9 +22,7 @@ tauri-plugin-shell = "2" tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" - -[target.'cfg(unix)'.dependencies] -libc = "0.2" +process-wrap = { version = "9", features = ["std"] } [profile.release] strip = true diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 21c63da..f101fe1 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -4,8 +4,9 @@ use std::{ collections::HashMap, env, fs::{create_dir_all, OpenOptions}, - io::Write, + io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, + process::Stdio, sync::atomic::{AtomicU64, Ordering}, sync::mpsc, sync::{Arc, Mutex}, @@ -15,7 +16,7 @@ use tauri::{ menu::{AboutMetadata, Menu, PredefinedMenuItem, Submenu}, AppHandle, DragDropEvent, Emitter, Manager, RunEvent, WindowEvent, }; -use tauri_plugin_shell::{process::CommandEvent, ShellExt}; +use process_wrap::std::*; enum SidecarMsg { Json(String), @@ -24,12 +25,13 @@ enum SidecarMsg { type SidecarSender = mpsc::Sender; type PendingRequests = Arc>>>; +type SharedChild = Arc>>; struct SidecarState { tx: SidecarSender, pending_requests: PendingRequests, next_request_id: AtomicU64, - child_pid: u32, + child: SharedChild, } const LOG_FILE_ENV: &str = "MOUSETERM_LOG_FILE"; @@ -262,27 +264,16 @@ fn read_update_log() -> Result { #[tauri::command] fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) { let _ = state.tx.send(SidecarMsg::Shutdown); - kill_process_tree(state.child_pid); + kill_sidecar(&state.child); } -/// Kill the sidecar process. On Windows, `taskkill /T` kills the entire -/// process tree so that child shell processes don't outlive the sidecar. -/// On Unix, a single SIGTERM to the sidecar is sufficient because node-pty -/// manages its own child processes and cleans them up on exit. -fn kill_process_tree(pid: u32) { - append_log(format!("[sidecar] killing process tree (pid={pid})")); - #[cfg(target_os = "windows")] - { - use std::os::windows::process::CommandExt; - const CREATE_NO_WINDOW: u32 = 0x08000000; - let _ = std::process::Command::new("taskkill") - .args(["/F", "/T", "/PID", &pid.to_string()]) - .creation_flags(CREATE_NO_WINDOW) - .output(); - } - #[cfg(unix)] - { - unsafe { libc::kill(pid as i32, libc::SIGTERM); } +/// Kill the sidecar via process-wrap. On Windows the Job Object wrapper +/// guarantees the kill propagates to grandchildren even if the sidecar +/// spawned its own children. On Unix it sends to the whole process group. +fn kill_sidecar(child: &SharedChild) { + if let Ok(mut guard) = child.lock() { + append_log(format!("[sidecar] killing (pid={})", guard.id())); + let _ = guard.start_kill(); } } @@ -337,12 +328,35 @@ fn sidecar_script_arg_path(path: &Path) -> PathBuf { path.to_path_buf() } +/// Locate the bundled Node.js binary at runtime. tauri-build/tauri-bundler +/// place externalBin entries alongside the main exe, sometimes with the +/// target-triple suffix (`node-x86_64-pc-windows-msvc.exe`) and sometimes +/// stripped (`node.exe`). Try both, return the first that exists. +fn resolve_node_binary_path() -> Result { + let exe = env::current_exe().map_err(|e| format!("current_exe: {e}"))?; + let dir = exe + .parent() + .ok_or_else(|| "current_exe has no parent".to_string())?; + Ok(find_node_binary(dir, env!("TAURI_ENV_TARGET_TRIPLE")) + .ok_or_else(|| format!("node sidecar not found in {}", dir.display()))?) +} + +fn find_node_binary(dir: &Path, target_triple: &str) -> Option { + let suffix = if cfg!(windows) { ".exe" } else { "" }; + let candidates = [ + dir.join(format!("node-{target_triple}{suffix}")), + dir.join(format!("node{suffix}")), + ]; + candidates.into_iter().find(|p| p.is_file()) +} + fn start_sidecar(app: &AppHandle) -> Result { let sidecar_path = resolve_sidecar_path( app.path().resource_dir().ok(), Path::new(env!("CARGO_MANIFEST_DIR")), ); let sidecar_arg_path = sidecar_script_arg_path(&sidecar_path); + let node_path = resolve_node_binary_path()?; append_log(format!( "[sidecar] resolved script: {}", sidecar_path.display() @@ -351,87 +365,99 @@ fn start_sidecar(app: &AppHandle) -> Result { "[sidecar] script argument: {}", sidecar_arg_path.display() )); + append_log(format!("[sidecar] node binary: {}", node_path.display())); + + let mut wrap = CommandWrap::with_new(&node_path, |c| { + c.arg(&sidecar_arg_path) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + }); + #[cfg(windows)] + { + wrap.wrap(JobObject); + } + #[cfg(unix)] + { + wrap.wrap(ProcessGroup::leader()); + } - let (mut rx, mut child) = app - .shell() - .sidecar("node") - .map_err(|err| format!("failed to resolve bundled Node.js runtime: {err}"))? - .arg(&sidecar_arg_path) - .set_raw_out(false) + let mut child = wrap .spawn() .map_err(|err| format!("failed to start Node.js sidecar: {err}"))?; - let child_pid = child.pid(); + let child_pid = child.id(); append_log(format!("[sidecar] spawned Node.js runtime (pid={child_pid})")); + let stdin = child + .stdin() + .take() + .ok_or_else(|| "sidecar stdin missing".to_string())?; + let stdout = child + .stdout() + .take() + .ok_or_else(|| "sidecar stdout missing".to_string())?; + let stderr = child + .stderr() + .take() + .ok_or_else(|| "sidecar stderr missing".to_string())?; + let handle = app.clone(); let pending_requests: PendingRequests = Arc::new(Mutex::new(HashMap::new())); let pending_requests_for_task = Arc::clone(&pending_requests); - // ── stdout/stderr reader task ─────────────────────────────────────── - tauri::async_runtime::spawn(async move { - while let Some(event) = rx.recv().await { - match event { - CommandEvent::Stdout(line) => { - let Ok(line) = String::from_utf8(line) else { - append_log("[sidecar stdout] invalid UTF-8"); - continue; - }; - let Ok(mut msg) = serde_json::from_str::(&line) else { - append_log(format!("[sidecar stdout] {}", line.trim_end())); - continue; - }; - let Some(event) = msg.get("event").and_then(|e| e.as_str()).map(String::from) - else { - append_log("[sidecar stdout] JSON line missing event"); + // ── stdout reader thread ──────────────────────────────────────────── + std::thread::spawn(move || { + let reader = BufReader::new(stdout); + for line_result in reader.lines() { + let Ok(line) = line_result else { + break; + }; + let Ok(mut msg) = serde_json::from_str::(&line) else { + append_log(format!("[sidecar stdout] {}", line.trim_end())); + continue; + }; + let Some(event) = msg.get("event").and_then(|e| e.as_str()).map(String::from) + else { + append_log("[sidecar stdout] JSON line missing event"); + continue; + }; + let data = msg + .as_object_mut() + .and_then(|m| m.remove("data")) + .unwrap_or(JsonValue::Null); + + if let Some(request_id) = data + .get("requestId") + .and_then(|request_id| request_id.as_str()) + { + if let Ok(mut pending) = pending_requests_for_task.lock() { + if let Some(response_tx) = pending.remove(request_id) { + let _ = response_tx.send(data.clone()); continue; - }; - let data = msg - .as_object_mut() - .and_then(|m| m.remove("data")) - .unwrap_or(serde_json::Value::Null); - - if let Some(request_id) = data - .get("requestId") - .and_then(|request_id| request_id.as_str()) - { - if let Ok(mut pending) = pending_requests_for_task.lock() { - if let Some(response_tx) = pending.remove(request_id) { - let _ = response_tx.send(data.clone()); - continue; - } - } } - - let _ = handle.emit(&event, data); - } - CommandEvent::Stderr(line) => { - if let Ok(line) = String::from_utf8(line) { - let message = format!("[sidecar] {}", line.trim_end()); - eprintln!("{message}"); - append_log(message); - } - } - CommandEvent::Error(err) => { - let message = format!("[sidecar] {err}"); - eprintln!("{message}"); - append_log(message); - } - CommandEvent::Terminated(payload) => { - let message = format!( - "[sidecar] exited (code: {:?}, signal: {:?})", - payload.code, payload.signal - ); - eprintln!("{message}"); - append_log(message); - break; } - _ => {} } + + let _ = handle.emit(&event, data); + } + }); + + // ── stderr reader thread ──────────────────────────────────────────── + std::thread::spawn(move || { + let reader = BufReader::new(stderr); + for line_result in reader.lines() { + let Ok(line) = line_result else { + break; + }; + let message = format!("[sidecar] {}", line.trim_end()); + eprintln!("{message}"); + append_log(message); } }); // ── stdin writer thread ───────────────────────────────────────────── let (tx, writer_rx) = mpsc::channel::(); + let mut stdin = stdin; std::thread::spawn(move || { while let Ok(msg) = writer_rx.recv() { @@ -442,7 +468,7 @@ fn start_sidecar(app: &AppHandle) -> Result { } SidecarMsg::Json(line) => { let payload = format!("{}\n", line); - if child.write(payload.as_bytes()).is_err() { + if stdin.write_all(payload.as_bytes()).is_err() { append_log("[sidecar] stdin write failed"); break; } @@ -451,11 +477,13 @@ fn start_sidecar(app: &AppHandle) -> Result { } }); + let child: SharedChild = Arc::new(Mutex::new(child)); + Ok(SidecarState { tx, pending_requests, next_request_id: AtomicU64::new(0), - child_pid, + child, }) } @@ -561,7 +589,7 @@ pub fn run() { if let Some(state) = app.try_state::() { append_log("[app] exit — killing sidecar"); let _ = state.tx.send(SidecarMsg::Shutdown); - kill_process_tree(state.child_pid); + kill_sidecar(&state.child); } } }); @@ -569,7 +597,7 @@ pub fn run() { #[cfg(test)] mod tests { - use super::{resolve_sidecar_path, strip_windows_verbatim_prefix}; + use super::{find_node_binary, resolve_sidecar_path, strip_windows_verbatim_prefix}; use std::fs; use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -653,4 +681,40 @@ mod tests { PathBuf::from(r"\\server\share\MouseTerm\sidecar\main.js") ); } + + #[test] + fn finds_node_binary_with_triple_suffix() { + let dir = unique_temp_dir("node-triple"); + fs::create_dir_all(&dir).expect("failed to create dir"); + let suffix = if cfg!(windows) { ".exe" } else { "" }; + let triple = "x86_64-pc-windows-msvc"; + let expected = dir.join(format!("node-{triple}{suffix}")); + fs::write(&expected, b"fake").expect("failed to write fake binary"); + + let resolved = find_node_binary(&dir, triple).expect("should resolve"); + assert_eq!(resolved, expected); + fs::remove_dir_all(&dir).expect("failed to clean temp dir"); + } + + #[test] + fn finds_node_binary_falls_back_to_stripped_name() { + let dir = unique_temp_dir("node-stripped"); + fs::create_dir_all(&dir).expect("failed to create dir"); + let suffix = if cfg!(windows) { ".exe" } else { "" }; + let expected = dir.join(format!("node{suffix}")); + fs::write(&expected, b"fake").expect("failed to write fake binary"); + + let resolved = find_node_binary(&dir, "x86_64-pc-windows-msvc").expect("should resolve"); + assert_eq!(resolved, expected); + fs::remove_dir_all(&dir).expect("failed to clean temp dir"); + } + + #[test] + fn returns_none_when_no_node_binary_present() { + let dir = unique_temp_dir("node-missing"); + fs::create_dir_all(&dir).expect("failed to create dir"); + + assert!(find_node_binary(&dir, "x86_64-pc-windows-msvc").is_none()); + fs::remove_dir_all(&dir).expect("failed to clean temp dir"); + } } From 5fba847754ce02f97131efdb7b45104ba156ce7d Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 22:41:42 -0700 Subject: [PATCH 02/11] Cache the log file handle to avoid re-opening it per sidecar line. `append_log` was called from the per-line stdout/stderr reader loops, and each call resolved the log path from env vars, ran create_dir_all on the parent, and reopened the file with OpenOptions. On a chatty stderr stream that's three syscalls plus several allocations per line. Cache the resolved PathBuf in a OnceLock and the open append-mode File in OnceLock>>; init_log keeps its own truncate-mode open since it runs once at startup and the cache opens lazily after. Drive-by cleanup from the /simplify pass: drop a few narrating comments, an over-wrapped Result, and a redundant `let mut` rebind. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 66 ++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index f101fe1..1f36f8f 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -3,13 +3,13 @@ use serde_json::{Map as JsonMap, Value as JsonValue}; use std::{ collections::HashMap, env, - fs::{create_dir_all, OpenOptions}, + fs::{create_dir_all, File, OpenOptions}, io::{BufRead, BufReader, Write}, path::{Path, PathBuf}, process::Stdio, sync::atomic::{AtomicU64, Ordering}, sync::mpsc, - sync::{Arc, Mutex}, + sync::{Arc, Mutex, OnceLock}, time::{Duration, SystemTime, UNIX_EPOCH}, }; use tauri::{ @@ -58,8 +58,33 @@ fn default_log_path() -> PathBuf { env::temp_dir().join("mouseterm.log") } +fn log_path() -> &'static Path { + static PATH: OnceLock = OnceLock::new(); + PATH.get_or_init(default_log_path) +} + +// `append_log` runs per stdout/stderr line from the sidecar; reopening +// the file each call costs a syscall + dir-walk per chatty subprocess +// log line. Cache an append handle for the life of the process. +fn log_file() -> Option<&'static Mutex> { + static FILE: OnceLock>> = OnceLock::new(); + FILE.get_or_init(|| { + let path = log_path(); + if let Some(parent) = path.parent() { + let _ = create_dir_all(parent); + } + OpenOptions::new() + .create(true) + .append(true) + .open(path) + .ok() + .map(Mutex::new) + }) + .as_ref() +} + fn init_log() { - let path = default_log_path(); + let path = log_path(); if let Some(parent) = path.parent() { let _ = create_dir_all(parent); } @@ -68,7 +93,7 @@ fn init_log() { .create(true) .write(true) .truncate(true) - .open(&path) + .open(path) { let _ = writeln!( file, @@ -80,19 +105,15 @@ fn init_log() { } fn append_log(message: impl AsRef) { - let path = default_log_path(); - if let Some(parent) = path.parent() { - let _ = create_dir_all(parent); - } - - if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let Some(file) = log_file() else { return }; + if let Ok(mut file) = file.lock() { let _ = writeln!(file, "[{}] {}", log_timestamp(), message.as_ref()); } } fn read_log_tail(max_bytes: usize) -> Result { - let path = default_log_path(); - let contents = std::fs::read_to_string(&path) + let path = log_path(); + let contents = std::fs::read_to_string(path) .map_err(|e| format!("read {}: {e}", path.display()))?; if contents.len() <= max_bytes { return Ok(contents); @@ -267,9 +288,8 @@ fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) { kill_sidecar(&state.child); } -/// Kill the sidecar via process-wrap. On Windows the Job Object wrapper -/// guarantees the kill propagates to grandchildren even if the sidecar -/// spawned its own children. On Unix it sends to the whole process group. +// Job Object on Windows / process group on Unix — kill propagates to the +// sidecar's grandchildren (the spawned shells). fn kill_sidecar(child: &SharedChild) { if let Ok(mut guard) = child.lock() { append_log(format!("[sidecar] killing (pid={})", guard.id())); @@ -328,19 +348,17 @@ fn sidecar_script_arg_path(path: &Path) -> PathBuf { path.to_path_buf() } -/// Locate the bundled Node.js binary at runtime. tauri-build/tauri-bundler -/// place externalBin entries alongside the main exe, sometimes with the -/// target-triple suffix (`node-x86_64-pc-windows-msvc.exe`) and sometimes -/// stripped (`node.exe`). Try both, return the first that exists. fn resolve_node_binary_path() -> Result { let exe = env::current_exe().map_err(|e| format!("current_exe: {e}"))?; let dir = exe .parent() .ok_or_else(|| "current_exe has no parent".to_string())?; - Ok(find_node_binary(dir, env!("TAURI_ENV_TARGET_TRIPLE")) - .ok_or_else(|| format!("node sidecar not found in {}", dir.display()))?) + find_node_binary(dir, env!("TAURI_ENV_TARGET_TRIPLE")) + .ok_or_else(|| format!("node sidecar not found in {}", dir.display())) } +// tauri-bundler sometimes strips the target-triple suffix (e.g. install dir +// has `node.exe`, dev/bundle has `node-x86_64-pc-windows-msvc.exe`). fn find_node_binary(dir: &Path, target_triple: &str) -> Option { let suffix = if cfg!(windows) { ".exe" } else { "" }; let candidates = [ @@ -388,7 +406,7 @@ fn start_sidecar(app: &AppHandle) -> Result { let child_pid = child.id(); append_log(format!("[sidecar] spawned Node.js runtime (pid={child_pid})")); - let stdin = child + let mut stdin = child .stdin() .take() .ok_or_else(|| "sidecar stdin missing".to_string())?; @@ -405,7 +423,6 @@ fn start_sidecar(app: &AppHandle) -> Result { let pending_requests: PendingRequests = Arc::new(Mutex::new(HashMap::new())); let pending_requests_for_task = Arc::clone(&pending_requests); - // ── stdout reader thread ──────────────────────────────────────────── std::thread::spawn(move || { let reader = BufReader::new(stdout); for line_result in reader.lines() { @@ -442,7 +459,6 @@ fn start_sidecar(app: &AppHandle) -> Result { } }); - // ── stderr reader thread ──────────────────────────────────────────── std::thread::spawn(move || { let reader = BufReader::new(stderr); for line_result in reader.lines() { @@ -455,9 +471,7 @@ fn start_sidecar(app: &AppHandle) -> Result { } }); - // ── stdin writer thread ───────────────────────────────────────────── let (tx, writer_rx) = mpsc::channel::(); - let mut stdin = stdin; std::thread::spawn(move || { while let Ok(msg) = writer_rx.recv() { From 77853408475f2dd16728377665a5a3b189f79038 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 23:06:41 -0700 Subject: [PATCH 03/11] Hide Windows sidecar console --- standalone/src-tauri/Cargo.lock | 1 + standalone/src-tauri/Cargo.toml | 3 +++ standalone/src-tauri/src/lib.rs | 3 +++ 3 files changed, 7 insertions(+) diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index 5202e0c..a30c5bf 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -1953,6 +1953,7 @@ dependencies = [ "tauri-build", "tauri-plugin-shell", "tauri-plugin-updater", + "windows 0.62.2", ] [[package]] diff --git a/standalone/src-tauri/Cargo.toml b/standalone/src-tauri/Cargo.toml index 0b5190b..67bfeda 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -24,6 +24,9 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" process-wrap = { version = "9", features = ["std"] } +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = ["Win32_System_Threading"] } + [profile.release] strip = true lto = true diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 1f36f8f..c79eef5 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -17,6 +17,8 @@ use tauri::{ AppHandle, DragDropEvent, Emitter, Manager, RunEvent, WindowEvent, }; use process_wrap::std::*; +#[cfg(windows)] +use windows::Win32::System::Threading::CREATE_NO_WINDOW; enum SidecarMsg { Json(String), @@ -393,6 +395,7 @@ fn start_sidecar(app: &AppHandle) -> Result { }); #[cfg(windows)] { + wrap.wrap(CreationFlags(CREATE_NO_WINDOW)); wrap.wrap(JobObject); } #[cfg(unix)] From a4ece707ca4a9d6a90c6d00dbccb4ea872a43bb1 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 23:38:05 -0700 Subject: [PATCH 04/11] Replace wildcard process_wrap import with explicit names. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index c79eef5..4572d38 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -16,7 +16,11 @@ use tauri::{ menu::{AboutMetadata, Menu, PredefinedMenuItem, Submenu}, AppHandle, DragDropEvent, Emitter, Manager, RunEvent, WindowEvent, }; -use process_wrap::std::*; +use process_wrap::std::{ChildWrapper, CommandWrap}; +#[cfg(windows)] +use process_wrap::std::{CreationFlags, JobObject}; +#[cfg(unix)] +use process_wrap::std::ProcessGroup; #[cfg(windows)] use windows::Win32::System::Threading::CREATE_NO_WINDOW; From eb43796869ccc9852aa699e9bb07c422411c7dde Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 23:38:59 -0700 Subject: [PATCH 05/11] Use a Drop guard for test temp dirs so failed asserts don't leak. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 63 ++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 4572d38..b3cd351 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -623,48 +623,60 @@ mod tests { use std::path::{Path, PathBuf}; use std::time::{SystemTime, UNIX_EPOCH}; - fn unique_temp_dir(name: &str) -> PathBuf { - let suffix = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time before unix epoch") - .as_nanos(); - std::env::temp_dir().join(format!("mouseterm-{name}-{suffix}")) + // RAII guard so a failing assert doesn't leak the temp dir. + struct TempDir(PathBuf); + impl TempDir { + fn new(name: &str) -> Self { + let suffix = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system time before unix epoch") + .as_nanos(); + let path = std::env::temp_dir().join(format!("mouseterm-{name}-{suffix}")); + fs::create_dir_all(&path).expect("failed to create temp dir"); + TempDir(path) + } + fn path(&self) -> &Path { + &self.0 + } + } + impl Drop for TempDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } } #[test] fn prefers_packaged_sidecar_when_resource_exists() { - let resource_dir = unique_temp_dir("resource"); - let sidecar_dir = resource_dir.join("sidecar"); + let resource_dir = TempDir::new("resource"); + let sidecar_dir = resource_dir.path().join("sidecar"); let sidecar_path = sidecar_dir.join("main.js"); fs::create_dir_all(&sidecar_dir).expect("failed to create sidecar dir"); fs::write(&sidecar_path, "console.log('packaged');").expect("failed to create sidecar"); let resolved = resolve_sidecar_path( - Some(resource_dir.clone()), + Some(resource_dir.path().to_path_buf()), Path::new("/repo/standalone/src-tauri"), ); assert_eq!(resolved, sidecar_path); - fs::remove_dir_all(&resource_dir).expect("failed to clean temp dir"); } #[test] fn finds_sidecar_under_up_prefix() { - let resource_dir = unique_temp_dir("resource-up"); - let sidecar_dir = resource_dir.join("_up_").join("sidecar"); + let resource_dir = TempDir::new("resource-up"); + let sidecar_dir = resource_dir.path().join("_up_").join("sidecar"); let sidecar_path = sidecar_dir.join("main.js"); fs::create_dir_all(&sidecar_dir).expect("failed to create sidecar dir"); fs::write(&sidecar_path, "console.log('packaged');").expect("failed to create sidecar"); let resolved = resolve_sidecar_path( - Some(resource_dir.clone()), + Some(resource_dir.path().to_path_buf()), Path::new("/repo/standalone/src-tauri"), ); assert_eq!(resolved, sidecar_path); - fs::remove_dir_all(&resource_dir).expect("failed to clean temp dir"); } #[test] @@ -705,37 +717,32 @@ mod tests { #[test] fn finds_node_binary_with_triple_suffix() { - let dir = unique_temp_dir("node-triple"); - fs::create_dir_all(&dir).expect("failed to create dir"); + let dir = TempDir::new("node-triple"); let suffix = if cfg!(windows) { ".exe" } else { "" }; let triple = "x86_64-pc-windows-msvc"; - let expected = dir.join(format!("node-{triple}{suffix}")); + let expected = dir.path().join(format!("node-{triple}{suffix}")); fs::write(&expected, b"fake").expect("failed to write fake binary"); - let resolved = find_node_binary(&dir, triple).expect("should resolve"); + let resolved = find_node_binary(dir.path(), triple).expect("should resolve"); assert_eq!(resolved, expected); - fs::remove_dir_all(&dir).expect("failed to clean temp dir"); } #[test] fn finds_node_binary_falls_back_to_stripped_name() { - let dir = unique_temp_dir("node-stripped"); - fs::create_dir_all(&dir).expect("failed to create dir"); + let dir = TempDir::new("node-stripped"); let suffix = if cfg!(windows) { ".exe" } else { "" }; - let expected = dir.join(format!("node{suffix}")); + let expected = dir.path().join(format!("node{suffix}")); fs::write(&expected, b"fake").expect("failed to write fake binary"); - let resolved = find_node_binary(&dir, "x86_64-pc-windows-msvc").expect("should resolve"); + let resolved = + find_node_binary(dir.path(), "x86_64-pc-windows-msvc").expect("should resolve"); assert_eq!(resolved, expected); - fs::remove_dir_all(&dir).expect("failed to clean temp dir"); } #[test] fn returns_none_when_no_node_binary_present() { - let dir = unique_temp_dir("node-missing"); - fs::create_dir_all(&dir).expect("failed to create dir"); + let dir = TempDir::new("node-missing"); - assert!(find_node_binary(&dir, "x86_64-pc-windows-msvc").is_none()); - fs::remove_dir_all(&dir).expect("failed to clean temp dir"); + assert!(find_node_binary(dir.path(), "x86_64-pc-windows-msvc").is_none()); } } From 8f3c16ee0541d489b624a846341e0ed5793d5b75 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 23:39:27 -0700 Subject: [PATCH 06/11] Reap the spawned sidecar if taking its pipes fails. Previously a missing stdin/stdout/stderr would early-return Err while the child kept running. On Windows the Job Object reaps it when the handle drops, but on Unix the process group leader was leaked. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index b3cd351..137dd65 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -413,18 +413,19 @@ fn start_sidecar(app: &AppHandle) -> Result { let child_pid = child.id(); append_log(format!("[sidecar] spawned Node.js runtime (pid={child_pid})")); - let mut stdin = child - .stdin() - .take() - .ok_or_else(|| "sidecar stdin missing".to_string())?; - let stdout = child - .stdout() - .take() - .ok_or_else(|| "sidecar stdout missing".to_string())?; - let stderr = child - .stderr() - .take() - .ok_or_else(|| "sidecar stderr missing".to_string())?; + // We piped all three streams ourselves, so `take` should always succeed — + // but if it doesn't, the child is already running and would otherwise + // outlive this function. Reap it before bailing. + let stdin = child.stdin().take(); + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + let (mut stdin, stdout, stderr) = match (stdin, stdout, stderr) { + (Some(i), Some(o), Some(e)) => (i, o, e), + _ => { + let _ = child.start_kill(); + return Err("sidecar pipes missing after spawn".to_string()); + } + }; let handle = app.clone(); let pending_requests: PendingRequests = Arc::new(Mutex::new(HashMap::new())); From e9215ce760bccce9faf8e734a09fa16bd20c6451 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 23:40:22 -0700 Subject: [PATCH 07/11] =?UTF-8?q?Drop=20the=20SidecarMsg::Shutdown=20senti?= =?UTF-8?q?nel=20=E2=80=94=20kill=20is=20the=20mechanism.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Shutdown variant only broke the writer thread out of its loop; it never sent anything to the sidecar over stdin, so the previous code that did `tx.send(Shutdown); kill_sidecar(...)` gave the JS side no graceful-cleanup window despite appearing to. Simplify: writer thread now sends raw JSON lines and exits when the channel closes or the post-kill stdin write fails. Document the kill semantics in kill_sidecar. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 35 +++++++++++---------------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 137dd65..abbe08c 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -24,12 +24,7 @@ use process_wrap::std::ProcessGroup; #[cfg(windows)] use windows::Win32::System::Threading::CREATE_NO_WINDOW; -enum SidecarMsg { - Json(String), - Shutdown, -} - -type SidecarSender = mpsc::Sender; +type SidecarSender = mpsc::Sender; type PendingRequests = Arc>>>; type SharedChild = Arc>>; @@ -142,7 +137,7 @@ struct PtySpawnOptions { } fn send_to_sidecar(state: &SidecarState, line: String) { - let _ = state.tx.send(SidecarMsg::Json(line)); + let _ = state.tx.send(line); } fn request_from_sidecar( @@ -290,12 +285,13 @@ fn read_update_log() -> Result { #[tauri::command] fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) { - let _ = state.tx.send(SidecarMsg::Shutdown); kill_sidecar(&state.child); } // Job Object on Windows / process group on Unix — kill propagates to the -// sidecar's grandchildren (the spawned shells). +// sidecar's grandchildren (the spawned shells). On Unix this is SIGKILL to +// the whole process group, which is more thorough than the previous +// SIGTERM-to-just-node path that left node-pty grandchildren orphaned. fn kill_sidecar(child: &SharedChild) { if let Ok(mut guard) = child.lock() { append_log(format!("[sidecar] killing (pid={})", guard.id())); @@ -479,22 +475,14 @@ fn start_sidecar(app: &AppHandle) -> Result { } }); - let (tx, writer_rx) = mpsc::channel::(); + let (tx, writer_rx) = mpsc::channel::(); std::thread::spawn(move || { - while let Ok(msg) = writer_rx.recv() { - match msg { - SidecarMsg::Shutdown => { - append_log("[sidecar] shutdown requested"); - break; - } - SidecarMsg::Json(line) => { - let payload = format!("{}\n", line); - if stdin.write_all(payload.as_bytes()).is_err() { - append_log("[sidecar] stdin write failed"); - break; - } - } + while let Ok(line) = writer_rx.recv() { + let payload = format!("{}\n", line); + if stdin.write_all(payload.as_bytes()).is_err() { + append_log("[sidecar] stdin write failed"); + break; } } }); @@ -610,7 +598,6 @@ pub fn run() { if let RunEvent::Exit = event { if let Some(state) = app.try_state::() { append_log("[app] exit — killing sidecar"); - let _ = state.tx.send(SidecarMsg::Shutdown); kill_sidecar(&state.child); } } From 8cc00a4a966640c7531f71861060d24306e31694 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Thu, 30 Apr 2026 23:41:59 -0700 Subject: [PATCH 08/11] Restore sidecar exit observability via a reaper thread. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Old `tauri-plugin-shell` reader logged a `[sidecar] exited (code, signal)` line from CommandEvent::Terminated. After the move to plain pipe readers that signal was lost — the reader threads just broke silently on EOF and any in-flight `request_from_sidecar_timeout` callers waited the full timeout instead of failing fast. Add a thread that polls try_wait on the shared child every 250ms; on exit, log the status and clear the pending-requests map so blocked callers wake immediately (their channels see Disconnected and surface the existing timeout error). Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index abbe08c..57f88b0 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -489,6 +489,34 @@ fn start_sidecar(app: &AppHandle) -> Result { let child: SharedChild = Arc::new(Mutex::new(child)); + // Reaper: poll for exit so we log a real exit status and unblock any + // pending `request_from_sidecar_timeout` callers immediately instead of + // making them wait the full timeout when the sidecar has already died. + let child_for_reaper = Arc::clone(&child); + let pending_for_reaper = Arc::clone(&pending_requests); + std::thread::spawn(move || { + loop { + let status = match child_for_reaper.lock() { + Ok(mut guard) => guard.try_wait(), + Err(_) => return, + }; + match status { + Ok(Some(status)) => { + append_log(format!("[sidecar] exited (status: {status})")); + if let Ok(mut pending) = pending_for_reaper.lock() { + pending.clear(); + } + return; + } + Ok(None) => std::thread::sleep(Duration::from_millis(250)), + Err(err) => { + append_log(format!("[sidecar] wait error: {err}")); + return; + } + } + } + }); + Ok(SidecarState { tx, pending_requests, From 6569b3805e117151f97f9e414fa65104ac2047d4 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Fri, 1 May 2026 01:44:06 -0700 Subject: [PATCH 09/11] Distinguish timeout from sidecar-exit in request_from_sidecar_timeout. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the reaper drops pending senders on sidecar exit, callers got "timed out waiting for X" — misleading. Surface disconnect explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 57f88b0..b603cea 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -179,11 +179,20 @@ fn request_from_sidecar_timeout( match rx.recv_timeout(timeout) { Ok(response) => Ok(response), - Err(_) => { + Err(err) => { if let Ok(mut pending) = state.pending_requests.lock() { pending.remove(&request_id); } - Err(format!("timed out waiting for {event}")) + // Disconnected means the reaper cleared pending_requests because + // the sidecar exited — surface that distinctly from a real timeout. + match err { + mpsc::RecvTimeoutError::Timeout => { + Err(format!("timed out waiting for {event}")) + } + mpsc::RecvTimeoutError::Disconnected => { + Err(format!("sidecar exited before responding to {event}")) + } + } } } } From 56956314e4ffc410db617325893a135278f0a2bc Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Fri, 1 May 2026 01:44:12 -0700 Subject: [PATCH 10/11] Rename shutdown_sidecar Tauri command to kill_sidecar_now. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After dropping the SidecarMsg::Shutdown sentinel, the command no longer performs a graceful shutdown — it just kills. Name it accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 4 ++-- standalone/src/tauri-adapter.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index b603cea..b64bd60 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -293,7 +293,7 @@ fn read_update_log() -> Result { } #[tauri::command] -fn shutdown_sidecar(state: tauri::State<'_, SidecarState>) { +fn kill_sidecar_now(state: tauri::State<'_, SidecarState>) { kill_sidecar(&state.child); } @@ -623,7 +623,7 @@ pub fn run() { pty_get_cwd, pty_get_scrollback, pty_request_init, - shutdown_sidecar, + kill_sidecar_now, get_available_shells, read_clipboard_file_paths, read_clipboard_image_as_file_path, diff --git a/standalone/src/tauri-adapter.ts b/standalone/src/tauri-adapter.ts index 1416b77..50648c1 100644 --- a/standalone/src/tauri-adapter.ts +++ b/standalone/src/tauri-adapter.ts @@ -93,7 +93,7 @@ export class TauriAdapter implements PlatformAdapter { unlisten(); } this.unlistenFns = []; - invoke("shutdown_sidecar"); + invoke("kill_sidecar_now"); } async getAvailableShells(): Promise<{ name: string; path: string; args?: string[] }[]> { From 31c7e2e1d9577dd2b532565b11d3c12216a0caa7 Mon Sep 17 00:00:00 2001 From: Edgar Twigg Date: Fri, 1 May 2026 01:46:33 -0700 Subject: [PATCH 11/11] Gate pkg/about/AboutMetadata to macOS to silence unused warnings. The about-menu construction is only consumed under cfg(target_os = "macos"). Move pkg, about, and the AboutMetadata import behind the same cfg so non-macOS builds compile clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- standalone/src-tauri/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index b64bd60..2624d14 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -13,9 +13,11 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; use tauri::{ - menu::{AboutMetadata, Menu, PredefinedMenuItem, Submenu}, + menu::{Menu, PredefinedMenuItem, Submenu}, AppHandle, DragDropEvent, Emitter, Manager, RunEvent, WindowEvent, }; +#[cfg(target_os = "macos")] +use tauri::menu::AboutMetadata; use process_wrap::std::{ChildWrapper, CommandWrap}; #[cfg(windows)] use process_wrap::std::{CreationFlags, JobObject}; @@ -545,7 +547,9 @@ pub fn run() { // action that fights with the webview's DOM keydown handler. The // terminal owns Cmd+C / Cmd+V / Cmd+X in JS (see `Wall.tsx`). .menu(|handle| { + #[cfg(target_os = "macos")] let pkg = handle.package_info(); + #[cfg(target_os = "macos")] let about = AboutMetadata { name: Some(pkg.name.clone()), version: Some(pkg.version.to_string()),