Wrap sidecar in a Job Object/process group via process-wrap#41
Merged
Wrap sidecar in a Job Object/process group via process-wrap#41
Conversation
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) <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
31c7e2e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://871169fd.mouseterm.pages.dev |
| Branch Preview URL: | https://robust-sidecar-cleanup.mouseterm.pages.dev |
`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<Option<Mutex<File>>>; 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) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
tauri-plugin-shellsidecar API withstd::process::Commandwrapped byprocess-wrap, so the Node.js sidecar runs inside a Windows Job Object (withKILL_ON_JOB_CLOSE) or a Unix process group.node.exethat was holdingtarget\debug\node.exeopen and breaking subsequentcargo builds withPermissionDeniedfromtauri-build.tauri-plugin-shellregistered as a plugin because the frontend usesopen()from@tauri-apps/plugin-shellinupdater.ts. Only the Rust-side sidecar spawn moves off it.What changed
standalone/src-tauri/Cargo.toml— addprocess-wrap = { version = "9", features = ["std"] }, drop unusedlibcdep (was only used by the now-deletedkill_process_tree).standalone/src-tauri/src/lib.rs:start_sidecarnow resolves the bundlednodebinary via a newresolve_node_binary_path()(looks alongsidecurrent_exe()fornode-{TAURI_ENV_TARGET_TRIPLE}{ext}thennode{ext}), spawns viaprocess_wrap::std::CommandWrap+JobObject/ProcessGroup::leader, and reads stdout/stderr on plainstd::threads usingBufReader::lines()(replacing the oldtauri::async_runtime::spawn+CommandEventmatcher).SidecarState.child_pid: u32→child: Arc<Mutex<Box<dyn ChildWrapper + Send + Sync>>>.kill_process_tree(taskkill on Windows /libc::killon Unix) replaced bykill_sidecarwhich callsstart_kill()on the wrapped child. Same exit-time wiring as before —RunEvent::Exitand theshutdown_sidecarcommand both call it.Why not just upgrade Tauri?
We're already on the latest stable
tauri(2.10.3) andtauri-plugin-shell(2.3.5). The relevant upstream feature request — a sidecar-lifecycle plugin that handles graceful shutdown and crash detection — is open and unimplemented (tauri-apps/plugins-workspace#3062). Job Objects are the industry-standard answer to "kill child when parent dies regardless of how parent dies" (Chromium, VS Code, etc. all use them);process-wrapis the canonical Rust crate that abstracts Job Objects + Unix process groups under one API.Why not also remove
tauri-plugin-shellentirely?The frontend still uses
open()for the changelog/issue links. Swapping that totauri-plugin-openeris a separate small follow-up — keeping it out of this PR keeps the diff focused.Test plan
cargo test -p mouseterm_libpasses (3 new tests coverfind_node_binarypaths, 5 existing tests unchanged).pnpm dev:standalonelaunches and a terminal pane works (sidecar IPC functions over the newstd::process::Commandpipes).node.exeorphan remains.Stop-Process -Forceonmouseterm.exe): sidecarnode.exedisappears immediately. This was the failure mode the refactor exists to fix.🤖 Generated with Claude Code