diff --git a/Cargo.lock b/Cargo.lock index 21be915..4f94573 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -96,6 +96,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "windows-sys 0.59.0", ] [[package]] @@ -121,7 +122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -288,7 +289,7 @@ checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -340,7 +341,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ae859aa07428ca9a929b936690f8b12dc5f11dd8c6992a18ca93919f28bc177" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -363,7 +364,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -440,7 +441,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -531,7 +532,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -568,7 +569,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -600,7 +601,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -609,13 +619,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -624,42 +650,90 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 064efc7..9737a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ description = "Cross platform scripting for deno task" [features] default = ["shell"] -shell = ["futures", "glob", "nix", "os_pipe", "path-dedot", "tokio"] +shell = ["futures", "glob", "nix", "os_pipe", "path-dedot", "tokio", "windows-sys"] serialization = ["serde"] [dependencies] @@ -28,6 +28,9 @@ tokio = { version = "1", features = ["fs", "io-std", "io-util", "macros", "proce [target.'cfg(unix)'.dependencies] nix = { version = "0.27.1", features = ["signal"], optional = true } +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.59.0", features = ["Win32_Security", "Win32_System_JobObjects", "Win32_System_Threading"], optional = true } + [dev-dependencies] deno_unsync = "0.4.1" parking_lot = "0.12.1" diff --git a/src/shell/child_process_tracker.rs b/src/shell/child_process_tracker.rs new file mode 100644 index 0000000..a157857 --- /dev/null +++ b/src/shell/child_process_tracker.rs @@ -0,0 +1,168 @@ +use std::rc::Rc; + +use anyhow::Result; + +use tokio::process::Child; + +/// Windows does not have a concept of parent processes and so +/// killing the deno task process will not also kill any spawned +/// processes by default. To make this work, we can use winapi's +/// jobs api, which allows for associating a main process so that +/// when the main process terminates, it will also terminate the +/// associated processes. +/// +/// Read more: https://stackoverflow.com/questions/3342941/kill-child-process-when-parent-process-is-killed +#[derive(Clone)] +pub struct ChildProcessTracker(Rc); + +impl ChildProcessTracker { + #[cfg(windows)] + pub fn new() -> Self { + match windows::JobObject::new() { + Ok(tracker) => Self(Rc::new(tracker)), + Err(err) => { + if cfg!(debug_assertions) { + panic!("Could not start tracking processes. {:#}", err); + } else { + // fallback to not tracking processes if this fails + Self(Rc::new(NullChildProcessTracker)) + } + } + } + } + + #[cfg(not(windows))] + pub fn new() -> Self { + // no-op on non-windows platforms as they don't + // require tracking the child processes + Self(Rc::new(NullChildProcessTracker)) + } + + pub fn track(&self, child: &Child) { + if let Err(err) = self.0.track(child) { + if cfg!(debug_assertions) && child.id().is_some() { + panic!("Could not track process: {:#}", err); + } + } + } +} + +trait Tracker: Send + Sync { + fn track(&self, child: &Child) -> Result<()>; +} + +struct NullChildProcessTracker; + +impl Tracker for NullChildProcessTracker { + fn track(&self, _: &Child) -> Result<()> { + Ok(()) + } +} + +#[cfg(target_os = "windows")] +mod windows { + use std::ptr; + + use anyhow::bail; + use anyhow::Result; + use tokio::process::Child; + + use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows_sys::Win32::Foundation::TRUE; + use windows_sys::Win32::System::JobObjects::AssignProcessToJobObject; + use windows_sys::Win32::System::JobObjects::CreateJobObjectW; + use windows_sys::Win32::System::JobObjects::JobObjectExtendedLimitInformation; + use windows_sys::Win32::System::JobObjects::SetInformationJobObject; + use windows_sys::Win32::System::JobObjects::JOBOBJECT_EXTENDED_LIMIT_INFORMATION; + use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_BREAKAWAY_OK; + use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION; + use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + use windows_sys::Win32::System::JobObjects::JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK; + + use super::Tracker; + + pub struct JobObject(WinHandle); + + impl JobObject { + pub fn new() -> Result { + // SAFETY: WinAPI calls + unsafe { + let handle = + WinHandle::new(CreateJobObjectW(ptr::null_mut(), ptr::null())); + let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); + info.BasicLimitInformation.LimitFlags = + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + | JOB_OBJECT_LIMIT_BREAKAWAY_OK + | JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK + | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION; + let result = SetInformationJobObject( + handle.as_raw(), + JobObjectExtendedLimitInformation, + &mut info as *mut _ as *mut core::ffi::c_void, + std::mem::size_of_val(&info) as u32, + ); + if result != TRUE { + bail!( + "Could not set job information object. {:#}", + std::io::Error::last_os_error() + ); + } + + Ok(Self(handle)) + } + } + + fn add_process_handle(&self, process_handle: HANDLE) -> Result<()> { + // SAFETY: WinAPI call + unsafe { + let result = AssignProcessToJobObject(self.0.as_raw(), process_handle); + if result != TRUE { + bail!( + "Could not assign process to job object. {:#}", + std::io::Error::last_os_error() + ); + } else { + Ok(()) + } + } + } + } + + impl Tracker for JobObject { + fn track(&self, child: &Child) -> Result<()> { + if let Some(handle) = child.raw_handle() { + self.add_process_handle(handle) + } else { + // process exited... ignore + Ok(()) + } + } + } + + struct WinHandle(HANDLE); + + impl WinHandle { + pub fn new(handle: HANDLE) -> Self { + WinHandle(handle) + } + + pub fn as_raw(&self) -> HANDLE { + self.0 + } + } + + unsafe impl Send for WinHandle {} + unsafe impl Sync for WinHandle {} + + impl Drop for WinHandle { + fn drop(&mut self) { + // SAFETY: WinAPI calls + unsafe { + if !self.0.is_null() && self.0 != INVALID_HANDLE_VALUE { + windows_sys::Win32::Foundation::CloseHandle(self.0); + } + } + } + } +} diff --git a/src/shell/commands/executable.rs b/src/shell/commands/executable.rs index c0259b0..96de5e2 100644 --- a/src/shell/commands/executable.rs +++ b/src/shell/commands/executable.rs @@ -57,6 +57,8 @@ impl ShellCommand for ExecutableCommand { } }; + context.state.track_child_process(&child); + // avoid deadlock since this is holding onto the pipes drop(sub_command); diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 7af0f0c..67cfa4a 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -17,6 +17,7 @@ pub use types::ShellPipeWriter; pub use types::ShellState; pub use types::SignalKind; +mod child_process_tracker; mod command; mod commands; mod execute; diff --git a/src/shell/types.rs b/src/shell/types.rs index ab35c46..929d2a8 100644 --- a/src/shell/types.rs +++ b/src/shell/types.rs @@ -16,6 +16,7 @@ use futures::future::LocalBoxFuture; use tokio::sync::broadcast; use tokio::task::JoinHandle; +use crate::shell::child_process_tracker::ChildProcessTracker; use crate::shell::fs_util; use super::commands::builtin_commands; @@ -53,6 +54,7 @@ pub struct ShellState { cwd: PathBuf, commands: Rc>>, kill_signal: KillSignal, + process_tracker: ChildProcessTracker, tree_exit_code_cell: TreeExitCodeCell, } @@ -72,6 +74,7 @@ impl ShellState { cwd: PathBuf::new(), commands: Rc::new(commands), kill_signal, + process_tracker: ChildProcessTracker::new(), tree_exit_code_cell: Default::default(), }; // ensure the data is normalized @@ -161,6 +164,10 @@ impl ShellState { &self.kill_signal } + pub fn track_child_process(&self, child: &tokio::process::Child) { + self.process_tracker.track(child); + } + pub(crate) fn tree_exit_code_cell(&self) -> &TreeExitCodeCell { &self.tree_exit_code_cell }