Skip to content

Conversation

@sakina1303
Copy link

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

@sakina1303 sakina1303 requested a review from a team as a code owner November 10, 2025 12:29
@github-project-automation github-project-automation bot moved this to 📬Proposal in Roadmap Nov 10, 2025
@github-actions
Copy link
Contributor

github-actions bot commented Nov 10, 2025

Package Changes Through ed8d624

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

Copy link
Contributor

@Legend-Master Legend-Master left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this!

/// background tasks). This is guaranteed to run from the thread performing
/// the app cleanup/exit sequence.
#[allow(unused_variables)]
fn cleanup_before_exit(&mut self, app: &AppHandle<R>) {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would also want to implement this for the TauriPlugin so that tauri::plugin::Builder could have access to this API

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for sure! I’ll add this to the TauriPlugin trait as well so it’s accessible via tauri::plugin::Builder.

@Legend-Master Legend-Master added this to the 2.10 milestone Nov 12, 2025
/// protected or system processes, or when the caller lacks sufficient
/// privileges. Callers should handle and log any errors returned by this
/// function.
pub fn kill_process_tree(pid: u32) -> std::io::Result<()> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is a generic function and not bound to this app instance imo this should be moved into https://github.com/tauri-apps/plugins-workspace/tree/v2/plugins/process (and in theory that plugin's exit and restart should probably be part of the app core plugin instead but that's a discussion for another day).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be okay if I leave a TODO for now and move it in a follow-up PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we at least have to decide how to proceed here because once this is added in tauri we cannot remove it without a major release since removing apis is a breaking change.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I move kill_process_tree to the process plugin now as you suggested? I’ll update this PR to avoid a future breaking change.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think that would make sense, yes. we can always move it back if needed (that way around works)

@Legend-Master
Copy link
Contributor

Legend-Master commented Nov 13, 2025

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 dev doesn't kill side cars (it kills the app forcefully by sending SIGKILL on mac and linux, TerminateProcess on Windows to cargo)
    • 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 send WM_QUERYENDSESSION to 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 sending WM_CLOSE)
  • Updater plugin exits the app through std::process::exit which doesn't trigger the Exit event so side cars are not killed

My brain is basically fried right now, no more processes 😭

@FabianLars
Copy link
Member

  1. and perhaps 2) should imo be fixed via [shell] Add option to spawn command in process group plugins-workspace#1332 and not by trying to kill a process tree. prob doesn't help with the rest though.

@Tunglies
Copy link
Contributor

Tunglies commented Dec 2, 2025

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 dev doesn't kill side cars (it kills the app forcefully by sending SIGKILL on mac and linux, TerminateProcess on Windows to cargo)

    • 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 send WM_QUERYENDSESSION to 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 sending WM_CLOSE)

  • Updater plugin exits the app through std::process::exit which doesn't trigger the Exit event so side cars are not killed

My 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.

@Tunglies
Copy link
Contributor

Tunglies commented Dec 2, 2025

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 dev doesn't kill side cars (it kills the app forcefully by sending SIGKILL on mac and linux, TerminateProcess on Windows to cargo)

    • 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 send WM_QUERYENDSESSION to 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 sending WM_CLOSE)

  • Updater plugin exits the app through std::process::exit which doesn't trigger the Exit event so side cars are not killed

My 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.

@Tunglies
Copy link
Contributor

Tunglies commented Dec 2, 2025

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.

@Legend-Master
Copy link
Contributor

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

@Tunglies
Copy link
Contributor

Tunglies commented Dec 4, 2025

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 dev from terminal.
  • 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.

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. clash-verge-rev/clash-verge-rev#5533 (comment) (Chinese)

@Legend-Master
Copy link
Contributor

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 (tauri-apps/tao#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

@Tunglies
Copy link
Contributor

Tunglies commented Dec 4, 2025

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)
}

@Tunglies
Copy link
Contributor

Tunglies commented Dec 4, 2025

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.

@FabianLars
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📬Proposal

Development

Successfully merging this pull request may close these issues.

[feat] Add kill tree for sidecar

5 participants