diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index db79e7b..a30c5bf 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,13 +1946,14 @@ dependencies = [ name = "mouseterm" version = "0.9.0" dependencies = [ - "libc", + "process-wrap", "serde", "serde_json", "tauri", "tauri-build", "tauri-plugin-shell", "tauri-plugin-updater", + "windows 0.62.2", ] [[package]] @@ -2006,6 +2013,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 +2663,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 +3654,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -3705,7 +3736,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -3864,7 +3895,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -3889,7 +3920,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -4251,9 +4282,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 +4730,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 +4754,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 +4810,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 +4838,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 +4881,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 +4939,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 +5087,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 +5409,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..67bfeda 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -22,9 +22,10 @@ tauri-plugin-shell = "2" tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +process-wrap = { version = "9", features = ["std"] } -[target.'cfg(unix)'.dependencies] -libc = "0.2" +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62", features = ["Win32_System_Threading"] } [profile.release] strip = true diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index 21c63da..2624d14 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -3,33 +3,38 @@ use serde_json::{Map as JsonMap, Value as JsonValue}; use std::{ collections::HashMap, env, - fs::{create_dir_all, OpenOptions}, - io::Write, + 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::{ - menu::{AboutMetadata, Menu, PredefinedMenuItem, Submenu}, + menu::{Menu, PredefinedMenuItem, Submenu}, AppHandle, DragDropEvent, Emitter, Manager, RunEvent, WindowEvent, }; -use tauri_plugin_shell::{process::CommandEvent, ShellExt}; - -enum SidecarMsg { - Json(String), - Shutdown, -} - -type SidecarSender = mpsc::Sender; +#[cfg(target_os = "macos")] +use tauri::menu::AboutMetadata; +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; + +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"; @@ -56,8 +61,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); } @@ -66,7 +96,7 @@ fn init_log() { .create(true) .write(true) .truncate(true) - .open(&path) + .open(path) { let _ = writeln!( file, @@ -78,19 +108,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); @@ -113,7 +139,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( @@ -155,11 +181,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}")) + } + } } } } @@ -260,29 +295,18 @@ 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); +fn kill_sidecar_now(state: tauri::State<'_, SidecarState>) { + 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); } +// Job Object on Windows / process group on Unix — kill propagates to the +// 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())); + let _ = guard.start_kill(); } } @@ -337,12 +361,33 @@ fn sidecar_script_arg_path(path: &Path) -> PathBuf { path.to_path_buf() } +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())?; + 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 = [ + 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,101 +396,133 @@ fn start_sidecar(app: &AppHandle) -> Result { "[sidecar] script argument: {}", sidecar_arg_path.display() )); + append_log(format!("[sidecar] node binary: {}", node_path.display())); - 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 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(CreationFlags(CREATE_NO_WINDOW)); + wrap.wrap(JobObject); + } + #[cfg(unix)] + { + wrap.wrap(ProcessGroup::leader()); + } + + 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})")); + // 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())); 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"); + 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); + } + }); + + 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 (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 child.write(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; + } + } + }); + + 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; } } } @@ -455,7 +532,7 @@ fn start_sidecar(app: &AppHandle) -> Result { tx, pending_requests, next_request_id: AtomicU64::new(0), - child_pid, + child, }) } @@ -470,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()), @@ -548,7 +627,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, @@ -560,8 +639,7 @@ 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_process_tree(state.child_pid); + kill_sidecar(&state.child); } } }); @@ -569,53 +647,65 @@ 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}; - 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] @@ -653,4 +743,35 @@ mod tests { PathBuf::from(r"\\server\share\MouseTerm\sidecar\main.js") ); } + + #[test] + fn finds_node_binary_with_triple_suffix() { + let dir = TempDir::new("node-triple"); + let suffix = if cfg!(windows) { ".exe" } else { "" }; + let triple = "x86_64-pc-windows-msvc"; + 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.path(), triple).expect("should resolve"); + assert_eq!(resolved, expected); + } + + #[test] + fn finds_node_binary_falls_back_to_stripped_name() { + let dir = TempDir::new("node-stripped"); + let suffix = if cfg!(windows) { ".exe" } else { "" }; + 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.path(), "x86_64-pc-windows-msvc").expect("should resolve"); + assert_eq!(resolved, expected); + } + + #[test] + fn returns_none_when_no_node_binary_present() { + let dir = TempDir::new("node-missing"); + + assert!(find_node_binary(dir.path(), "x86_64-pc-windows-msvc").is_none()); + } } 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[] }[]> {