Add kill-tree helper and runtime sidecar PID registry
Adds a best-effort process-tree killer and a lightweight runtime sidecar PID registry, ensuring that descendant processes spawned by sidecars are properly terminated when their parent sidecar is killed.
This resolves the core issue described in #14360 by introducing both API and runtime cleanup mechanisms for sidecar process management.
Details:
- tauri::process::kill_process_tree(pid: u32) — POSIX shell & PowerShell based, best-effort.
- Sidecar PID registry via AppHandle::register_sidecar(pid) / unregister_sidecar(pid) for cleanup on exit.
- App::cleanup_before_exit() drains registry and calls the kill helper.
- CLI dev-run now attempts process-tree cleanup when stopping dev children.
Files Changed:
- process.rs: Added kill_process_tree() (POSIX + PowerShell).
- mod.rs: Added sidecar PID tracking + helper methods.
- app.rs: Added register/unregister APIs; updated cleanup.
- desktop.rs: Added kill-tree logic in dev-run path.
- CHANGELOG.md / PR_DRAFT.md: Updated with notes.
Testing:
- [ ✅ ] cargo test -p tauri, all tests passed.
- [ ✅ ] Manual: verified registry and cleanup invoke kill helper.
- Runtime cleanup is opt-in: Call app_handle.register_sidecar(child.id() as u32) after spawning.
Fixes #14360
Package Changes Through ed8d624281fa8f9fce9c056dd5d9a1b8ad673b4a
There are 1 changes which include @tauri-apps/api with patch
Planned Package Versions
The following package releases are the planned based on the context of changes in this pull request.
| package | current | next |
|---|---|---|
| @tauri-apps/api | 2.9.0 | 2.9.1 |
Add another change file through the GitHub UI by following this link.
Read about change files or the docs at github.com/jbolda/covector
I think we have now gathered a few different problems around this now:
- Only the side car process is killed on app exit, not the entire process tree
- Just from testing, it seems like some of the processes are killed while some others are not???
tauri devdoesn't kill side cars (it kills the app forcefully by sendingSIGKILLon mac and linux,TerminateProcesson Windows tocargo)- It seems to me that it does kill the side car but not always the entire process tree???
- NSIS installers don't kill side cars, they kill the app forcefully through
TerminateProcess(~~the Wix .msi installers do, they sendWM_QUERYENDSESSIONto the app which is now handled after https://github.com/tauri-apps/tao/pull/1126~~ no, I thought they do but what actually happened is just it killed the tray icon by sendingWM_CLOSE) - Updater plugin exits the app through
std::process::exitwhich doesn't trigger theExitevent so side cars are not killed
My brain is basically fried right now, no more processes 😭
- and perhaps 2) should imo be fixed via https://github.com/tauri-apps/plugins-workspace/issues/1332 and not by trying to kill a process tree. prob doesn't help with the rest though.
I think we have now gathered a few different problems around this now:
Only the side car process is killed on app exit, not the entire process tree
- Just from testing, it seems like some of the processes are killed while some others are not???
tauri devdoesn't kill side cars (it kills the app forcefully by sendingSIGKILLon mac and linux,TerminateProcesson Windows tocargo)
- It seems to me that it does kill the side car but not always the entire process tree???
NSIS installers don't kill side cars, they kill the app forcefully through
TerminateProcess(~the Wix .msi installers do, they sendWM_QUERYENDSESSIONto the app which is now handled after fix(windows): emit LoopDestroyed on WM_ENDSESSION tao#1126~ no, I thought they do but what actually happened is just it killed the tray icon by sendingWM_CLOSE)Updater plugin exits the app through
std::process::exitwhich doesn't trigger theExitevent so side cars are not killedMy brain is basically fried right now, no more processes 😭
I fixed point 1, 2 in my(our) project. 3 and 4 not test on my machine but has no community report related. With sidecar, I hold a custom strcture to wrap ProcessChild and give impl Drop for it to handle kill when droping. The entire app(which is main process) listen on system signal and excute tauri exiting and custom behavior, it will kill itself and its sidecar. They works well both in tauri dev and release build.
I think we have now gathered a few different problems around this now:
Only the side car process is killed on app exit, not the entire process tree
- Just from testing, it seems like some of the processes are killed while some others are not???
tauri devdoesn't kill side cars (it kills the app forcefully by sendingSIGKILLon mac and linux,TerminateProcesson Windows tocargo)
- It seems to me that it does kill the side car but not always the entire process tree???
NSIS installers don't kill side cars, they kill the app forcefully through
TerminateProcess(~the Wix .msi installers do, they sendWM_QUERYENDSESSIONto the app which is now handled after fix(windows): emit LoopDestroyed on WM_ENDSESSION tao#1126~ no, I thought they do but what actually happened is just it killed the tray icon by sendingWM_CLOSE)Updater plugin exits the app through
std::process::exitwhich doesn't trigger theExitevent so side cars are not killedMy brain is basically fried right now, no more processes 😭
I fixed point 1, 2 in my(our) project. 3 and 4 not test on my machine but has no community report related. With sidecar, I hold a custom strcture to wrap ProcessChild and give impl Drop for it to handle kill when droping. The entire app(which is main process) listen on system signal and excute Apphandle::Exit, it will kill itself and its sidecar. They works well.
We found it's hard to fully trust tauri exit behavior, and external listen system signal mannuly in clash-verge-rev/crates/signal. But behavior of macOS system shutdown signal seems hooked by tauri, can only processing with tauri::RunEvent::Exit otherwise will skip signal hanlde, but ubuntu and other linux works fine.
This PR using system shell. For Windows example in some case, user can not invoke system powershell due to their machine permission setting or even disable powershell usage. We might not want to handle PID with shell diretcly, and there are potential security problems whether if cross-platform.
With sidecar, I hold a custom strcture to wrap ProcessChild and give impl Drop for it to handle kill when droping.
I think this is more or less something we should provide in shell plugin?
We found it's hard to fully trust tauri exit behavior
Could you explain about this a bit more?
This PR using system shell.
Not the biggest fan of this either
I think this is more or less something we should provide in shell plugin?
Yes, and the auto-cleanup or likely behavior can be toggled via a field setting whether if spawn or runtime. We did not provide this behavior before, might destroy downstream program behavior if enable by default.
Could you explain about this a bit more?
Months ago, did not remember too much details.
- The sidecar plugin's process will not be killed when exiting
cli tauri devfrom terminal. - We tried handle with
RunEvent::ExitRequestedandRunEvent::Exitto reset system network settings when system shudown. None of linux, macOS and windows tasks works as expected. Might just we did not learned the proper way.
More, hanlding with resue tauri::asyncruntime to excute exiting or system shutdown operations in Windows will produce more likehood to failed or skipped. With a new tokio spawn would be totally fixed on that and works fine across Windows, macOS and Linux. https://github.com/clash-verge-rev/clash-verge-rev/pull/5533#issuecomment-3562934643 (Chinese)
We tried handle with RunEvent::ExitRequested and RunEvent::Exit to reset system network settings when system shudown. None of linux, macOS and windows tasks works as expected. Might just we did not learned the proper way.
RunEvent::Exit event should now fire on system shutdown on Windows (https://github.com/tauri-apps/tao/pull/1126), not on macOS and Linux yet though
More, hanlding with resue tauri::asyncruntime to excute exiting or system shutdown operations in Windows will produce more likehood to failed or skipped.
Hmm, that sounds weird, the tauri async runtime by default should be very much the same as using a tokio one directly
should be very much the same as using a tokio one directly
Our program continuosly handle differents tauri commands, data-related processing, system setting processing. Might be a little more complex than genral program. The async_runtime::spawn The singleton async runtime used by Tauri and exposed to users. that we already used for spawn some inner tasks. A new tokio runtime would not be stuggle with tauri.
/// async_runtime::spawn
pub fn spawn<F>(task: F) -> JoinHandle<F::Output>
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
let runtime = RUNTIME.get_or_init(default_runtime);
runtime.spawn(task)
}
not on macOS and Linux yet though
Wierd on my test on macOS, signal_hook can not handle shutdown signal, but SIGTERM, SIGTERM interminal or standalone were fine. Macos shutdown handle only works with
#[cfg(target_os = "macos")]
tauri::RunEvent::Exit => async_runtime::block_on(async {
some_clean_up()
}),
Linux works with competely works with signal_hook. Seemed tauri hooked macOS shutdown signal and use tauri::RunEvent::Exit for case.
Macos shutdown handle only works with
Not gonna lie, this sounds like something macOS would. Not sending posix signals in favor of the NSApplication delegate bullshit would be such an Apple move 🙃 I'm somewhat sure it's not us doing that.