mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-10-18 23:07:27 +02:00
feat(command): implement Drop for CommandChild with kill_on_drop option (#3149)
* feat(command): implement Drop for CommandChild with kill_on_drop option * fmt * log * merge fixes, docs * clippy * Apply suggestion from @lucasfernog [skip ci] --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
1 parent
0bd82c829b
commit
738c323cfc
2 files changed
+104
-2
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"shell": minor:feat
|
||||
"shell-js": minor:feat
|
||||
---
|
||||
|
||||
Add `Command::kill_on_drop` to kill the spawned process (or its process group / job object when `Command::set_process_group` is enabled) when its `CommandChild` is dropped.
|
||||
@@ -63,6 +63,7 @@ pub struct Command {
|
||||
cmd: StdCommand,
|
||||
raw_out: bool,
|
||||
process_group: bool,
|
||||
kill_on_drop: bool,
|
||||
}
|
||||
|
||||
/// Spawned child process.
|
||||
@@ -74,6 +75,7 @@ pub struct CommandChild {
|
||||
process_group: bool,
|
||||
#[cfg(windows)]
|
||||
job: Option<job_object::JobObject>,
|
||||
kill_on_drop: bool,
|
||||
}
|
||||
|
||||
impl CommandChild {
|
||||
@@ -85,7 +87,16 @@ impl CommandChild {
|
||||
|
||||
/// Sends a kill signal to the child and waits for it to exit.
|
||||
/// With `process_group` enabled this kills the whole process group (POSIX) or job object (Windows).
|
||||
pub fn kill(self) -> crate::Result<()> {
|
||||
pub fn kill(mut self) -> crate::Result<()> {
|
||||
self.send_kill()?;
|
||||
// Already killed, so `Drop` must not do it again.
|
||||
self.kill_on_drop = false;
|
||||
self.inner.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends the kill signal without waiting for the child to exit.
|
||||
fn send_kill(&self) -> crate::Result<()> {
|
||||
// No early return when the child already exited: the rest of its
|
||||
// process group or job object may still be running.
|
||||
#[cfg(unix)]
|
||||
@@ -110,7 +121,6 @@ impl CommandChild {
|
||||
None => self.inner.kill()?,
|
||||
}
|
||||
|
||||
self.inner.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -120,6 +130,17 @@ impl CommandChild {
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CommandChild {
|
||||
/// Kills the child process (or its process group / job object) if `kill_on_drop` is enabled.
|
||||
fn drop(&mut self) {
|
||||
if self.kill_on_drop
|
||||
&& let Err(e) = self.send_kill()
|
||||
{
|
||||
log::error!("Failed to kill child process {}: {}", self.inner.id(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes the result of a process after it has terminated.
|
||||
#[derive(Debug)]
|
||||
pub struct ExitStatus {
|
||||
@@ -210,6 +231,7 @@ impl Command {
|
||||
cmd: command,
|
||||
raw_out: false,
|
||||
process_group: false,
|
||||
kill_on_drop: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +310,21 @@ impl Command {
|
||||
self
|
||||
}
|
||||
|
||||
/// Configures whether the child process is killed when its [`CommandChild`] is dropped.
|
||||
/// Defaults to `false`, so a dropped [`CommandChild`] leaves the process running.
|
||||
///
|
||||
/// The kill works like [`CommandChild::kill`] but does not wait for the process to exit.
|
||||
/// Without [`Command::set_process_group`] only the child itself is killed, and any
|
||||
/// processes it spawned keep running; enable it to kill the whole process group (POSIX)
|
||||
/// or job object (Windows).
|
||||
///
|
||||
/// This has no effect on processes spawned from the JavaScript API; the plugin kills those when the app exits.
|
||||
#[must_use]
|
||||
pub fn kill_on_drop(mut self, kill_on_drop: bool) -> Self {
|
||||
self.kill_on_drop = kill_on_drop;
|
||||
self
|
||||
}
|
||||
|
||||
/// Spawns the command.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -350,6 +387,7 @@ impl Command {
|
||||
pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
|
||||
let raw = self.raw_out;
|
||||
let process_group = self.process_group;
|
||||
let kill_on_drop = self.kill_on_drop;
|
||||
let mut command: StdCommand = self.into();
|
||||
let (stdout_reader, stdout_writer) = pipe()?;
|
||||
let (stderr_reader, stderr_writer) = pipe()?;
|
||||
@@ -425,6 +463,7 @@ impl Command {
|
||||
process_group,
|
||||
#[cfg(windows)]
|
||||
job,
|
||||
kill_on_drop,
|
||||
},
|
||||
))
|
||||
}
|
||||
@@ -774,6 +813,63 @@ mod tests {
|
||||
assert_ne!(ret, 0, "process group should no longer exist");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
fn wait_terminated(mut rx: Receiver<CommandEvent>) -> TerminatedPayload {
|
||||
tauri::async_runtime::block_on(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
if let CommandEvent::Terminated(payload) = event {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
panic!("channel closed without a Terminated event");
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_kill_on_drop() {
|
||||
let (rx, child) = Command::new("sleep")
|
||||
.args(["60"])
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
drop(child);
|
||||
|
||||
let payload = wait_terminated(rx);
|
||||
assert_eq!(payload.signal, Some(libc::SIGKILL));
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_no_kill_on_drop_by_default() {
|
||||
let (_rx, child) = Command::new("sleep").args(["60"]).spawn().unwrap();
|
||||
let pid = child.pid();
|
||||
drop(child);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
let ret = unsafe { libc::kill(pid as i32, 0) };
|
||||
assert_eq!(ret, 0, "process should still be running");
|
||||
unsafe { libc::kill(pid as i32, libc::SIGKILL) };
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn test_cmd_kill_on_drop_process_group() {
|
||||
// The shell's own child (sleep) must die too, not just the shell.
|
||||
let (rx, child) = Command::new("sh")
|
||||
.args(["-c", "sleep 60; true"])
|
||||
.set_process_group(true)
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let pid = child.pid();
|
||||
drop(child);
|
||||
|
||||
wait_terminated(rx);
|
||||
let ret = unsafe { libc::killpg(pid as i32, 0) };
|
||||
assert_ne!(ret, 0, "process group should no longer exist");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cmd_process_group_output() {
|
||||
// Hangs on Windows if the suspended child is never resumed.
|
||||
|
||||
Reference in new issue
Block a user