This commit is contained in:
Sakina 2026-02-06 09:31:46 +08:00 committed by GitHub
commit 99e6d9c07b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 171 additions and 0 deletions

View File

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

View File

@ -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::SeqCst);
Ok(())

View File

@ -499,6 +499,8 @@ impl<R: Runtime> AppHandle<R> {
Ok(())
}
/// Removes the plugin with the given name.
///
/// # Examples
@ -1035,6 +1037,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`].

View File

@ -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>) {}
/// Extend commands to [`crate::Builder::invoke_handler`].
#[allow(unused_variables)]
fn extend_api(&mut self, invoke: Invoke<R>) -> bool {
@ -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.

View File

@ -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<()> {
#[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),
))
}
}
}