Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changes/use-plugin-cleanups-and-kill-tree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@tauri-apps/tauri': 'minor:enhance'
---

Introduce plugin-level cleanup hooks and centralize process-tree termination logic.

This change:
- Adds `kill_process_tree` helper to the runtime for cross-platform process-tree shutdown.
- Adds a new `cleanup_before_exit` lifecycle hook to plugins and wires it so plugin authors
can handle sidecar shutdown without core runtime logic.
- Removes hardcoded sidecar-draining from the runtime and delegates shutdown behavior to plugins.

This allows plugins (such as the shell plugin) to manage their own sidecar processes cleanly
and improves extensibility of the Tauri runtime. Fixes #14360.
35 changes: 35 additions & 0 deletions crates/tauri-cli/src/interface/rust/desktop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,41 @@ pub struct DevChild {

impl DevProcess for DevChild {
fn kill(&self) -> std::io::Result<()> {
if let Some(pid) = self.dev_child.id() {
#[cfg(windows)]
{
use std::process::Command;
let ps = format!(
"function Kill-Tree {{ Param([int]$ppid); Get-CimInstance Win32_Process | Where-Object {{ $_.ParentProcessId -eq $ppid }} | ForEach-Object {{ Kill-Tree $_.ProcessId }}; Stop-Process -Id $ppid -ErrorAction SilentlyContinue }}; Kill-Tree {}",
pid
);
let _ = Command::new("powershell")
.arg("-NoProfile")
.arg("-Command")
.arg(ps)
.status();
}

#[cfg(not(windows))]
{
use std::process::Command;
let sh = format!(r#"
getcpid() {{
for cpid in $(pgrep -P "$1" 2>/dev/null || true); do
getcpid "$cpid"
echo "$cpid"
done
}}
for p in $(getcpid {pid}); do
kill -9 "$p" 2>/dev/null || true
done
kill -9 {pid} 2>/dev/null || true
"#, pid = pid);

let _ = Command::new("sh").arg("-c").arg(sh).status();
}
}

self.dev_child.kill()?;
self.manually_killed_app.store(true, Ordering::Relaxed);
Ok(())
Expand Down
9 changes: 9 additions & 0 deletions crates/tauri/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,8 @@ impl<R: Runtime> AppHandle<R> {
Ok(())
}



/// Removes the plugin with the given name.
///
/// # Examples
Expand Down Expand Up @@ -1037,6 +1039,13 @@ macro_rules! shared_app_impl {
for (_, webview) in self.manager.webviews() {
webview.resources_table().clear();
}
// run plugin cleanup hooks so plugins can perform shutdown tasks (e.g. stop sidecars)
self
.manager
.plugins
.lock()
.unwrap()
.cleanup_before_exit(self.app_handle());
}

/// Gets the invoke key that must be referenced when using [`crate::webview::InvokeRequest`].
Expand Down
18 changes: 18 additions & 0 deletions crates/tauri/src/plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,15 @@ pub trait Plugin<R: Runtime>: Send {
#[allow(unused_variables)]
fn on_event(&mut self, app: &AppHandle<R>, event: &RunEvent) {}

/// Callback invoked when the application is performing cleanup before exit.
///
/// Plugins can use this hook to perform any process shutdown/cleanup they need
/// to do before the runtime exits (for example, killing sidecars or stopping
/// background tasks). This hook is executed inside `App::cleanup_before_exit` during application shutdown.

#[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.


/// Extend commands to [`crate::Builder::invoke_handler`].
#[allow(unused_variables)]
fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
Expand Down Expand Up @@ -979,6 +988,15 @@ impl<R: Runtime> PluginStore<R> {
.for_each(|plugin| plugin.on_event(app, event))
}

/// Runs the cleanup_before_exit hook for all plugins in the store.
pub(crate) fn cleanup_before_exit(&mut self, app: &AppHandle<R>) {
self.store.iter_mut().for_each(|plugin| {
#[cfg(feature = "tracing")]
let _span = tracing::trace_span!("plugin::hooks::cleanup_before_exit", name = plugin.name()).entered();
plugin.cleanup_before_exit(app)
})
}

/// Runs the plugin `extend_api` hook if it exists. Returns whether the invoke message was handled or not.
///
/// The message is not handled when the plugin exists **and** the command does not.
Expand Down
95 changes: 95 additions & 0 deletions crates/tauri/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,98 @@ fn restart_macos_app(current_binary: &std::path::Path, env: &Env) {
}
}
}

/// Kill a process and its descendant process tree (best-effort).
///
/// On Windows this function prefers the built-in `taskkill /T /PID <pid> /F`
/// utility which can terminate a process tree. If `taskkill` is unavailable
/// or returns a non-zero exit status (for example due to permissions), the
/// function falls back to a PowerShell-based recursive traversal which mirrors
/// the previous implementation.
///
/// On Unix-like systems a small shell function using `pgrep -P` is used to
/// collect child PIDs and send `SIGKILL` to descendants and the root PID.
///
/// Note: terminating processes is inherently best-effort and may fail for
/// protected or system processes, or when the caller lacks sufficient
/// privileges. Callers should handle and log any errors returned by this
/// function.

// TODO: Move this helper into the `process` plugin in the plugins-workspace repo.
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)

#[cfg(windows)]
{
use std::process::Command;

// Prefer the built-in `taskkill` utility on Windows which can terminate a process
// tree with `/T`. If that fails (permissions, not found, or non-zero exit), fall
// back to a PowerShell-based recursive stop that mirrors the previous behavior.
let pid_s = pid.to_string();

if let Ok(status) = Command::new("taskkill")
.args(&["/T", "/PID", &pid_s, "/F"]) // /F to force termination
.status()
{
if status.success() {
return Ok(());
}
// If taskkill returned non-zero, fall through to try PowerShell.
}

// Fallback: Use PowerShell to recursively find and stop child processes, then stop the root.
// This mirrors the approach used elsewhere in the project (tauri-cli) and preserves
// behavior on systems where taskkill isn't available or failed due to permissions.
let ps = format!(
"function Kill-Tree {{ Param([int]$ppid); Get-CimInstance Win32_Process | Where-Object {{ $_.ParentProcessId -eq $ppid }} | ForEach-Object {{ Kill-Tree $_.ProcessId }}; Stop-Process -Id $ppid -ErrorAction SilentlyContinue }}; Kill-Tree {}",
pid
);

let status = Command::new("powershell")
.arg("-NoProfile")
.arg("-Command")
.arg(ps)
.status()?;

if status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("kill-tree failed: powershell exited with status: {}", status),
))
}
}

#[cfg(not(windows))]
{
use std::process::Command;

// On Unix, recursively collect children via pgrep -P and kill them. We use a small
// shell function to traverse descendants and then kill them. Use SIGKILL to ensure
// termination (best effort).
let sh = format!(r#"
getcpid() {{
for cpid in $(pgrep -P "$1" 2>/dev/null || true); do
getcpid "$cpid"
echo "$cpid"
done
}}
for p in $(getcpid {pid}); do
kill -9 "$p" 2>/dev/null || true
done
kill -9 {pid} 2>/dev/null || true
"#, pid = pid);

let status = Command::new("sh").arg("-c").arg(sh).status()?;

if status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("sh kill-tree failed with status: {}", status),
))
}
}
}

Loading