diff --git a/Cargo.lock b/Cargo.lock index e7bde2ed30..ff33eafee3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2032,6 +2032,18 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537bc3c4a347b87fd52ac6c03a02ab1302962cfd93373c5d7a112cdc337854cc" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -2170,6 +2182,7 @@ dependencies = [ "napi", "napi-build", "napi-derive", + "nix 0.30.0", "nom", "once_cell", "parking_lot", diff --git a/packages/nx/Cargo.toml b/packages/nx/Cargo.toml index 804e406968..f161d628bf 100644 --- a/packages/nx/Cargo.toml +++ b/packages/nx/Cargo.toml @@ -59,10 +59,11 @@ xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] } vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "1de895505fe9f697aadac585e4075b8fb45c880d" } [target.'cfg(windows)'.dependencies] -winapi = { version = "0.3", features = ["fileapi"] } +winapi = { version = "0.3", features = ["fileapi", "psapi", "shellapi"] } [target.'cfg(all(not(windows), not(target_family = "wasm")))'.dependencies] mio = "0.8" +nix = { version = "0.30.0", features = ["process", "signal"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] arboard = "3.4.1" diff --git a/packages/nx/src/executors/run-commands/running-tasks.ts b/packages/nx/src/executors/run-commands/running-tasks.ts index 22c3111428..58288487e6 100644 --- a/packages/nx/src/executors/run-commands/running-tasks.ts +++ b/packages/nx/src/executors/run-commands/running-tasks.ts @@ -71,7 +71,7 @@ export class ParallelRunningTasks implements RunningTask { } } - async kill(signal?: NodeJS.Signals | number) { + async kill(signal?: NodeJS.Signals) { await Promise.all( this.childProcesses.map(async (p) => { try { @@ -215,7 +215,7 @@ export class SeriallyRunningTasks implements RunningTask { throw new Error('Not implemented'); } - kill(signal?: NodeJS.Signals | number) { + kill(signal?: NodeJS.Signals) { return this.currentProcess.kill(signal); } @@ -357,7 +357,7 @@ class RunningNodeProcess implements RunningTask { this.childProcess.send(message); } - kill(signal?: NodeJS.Signals | number): Promise { + kill(signal?: NodeJS.Signals): Promise { return new Promise((res, rej) => { if (process.platform === 'win32' || process.platform === 'darwin') { if (this.childProcess.kill(signal)) { diff --git a/packages/nx/src/native/index.d.ts b/packages/nx/src/native/index.d.ts index b08cd3989a..f7f148ae54 100644 --- a/packages/nx/src/native/index.d.ts +++ b/packages/nx/src/native/index.d.ts @@ -26,7 +26,7 @@ export declare class AppLifeCycle { export declare class ChildProcess { getParserAndWriter(): ExternalObject<[ParserArc, WriterArc]> - kill(): void + kill(signal?: NodeJS.Signals): void onExit(callback: (message: string) => void): void onOutput(callback: (message: string) => void): void cleanup(): void diff --git a/packages/nx/src/native/pseudo_terminal/child_process.rs b/packages/nx/src/native/pseudo_terminal/child_process.rs index 06a2c186c5..39d875d92e 100644 --- a/packages/nx/src/native/pseudo_terminal/child_process.rs +++ b/packages/nx/src/native/pseudo_terminal/child_process.rs @@ -1,3 +1,4 @@ +use super::process_killer::ProcessKiller; use crate::native::pseudo_terminal::pseudo_terminal::{ParserArc, WriterArc}; use crossbeam_channel::Sender; use crossbeam_channel::{bounded, Receiver}; @@ -8,7 +9,6 @@ use napi::{ }, Env, JsFunction, }; -use portable_pty::ChildKiller; use std::io::Write; use std::sync::{Arc, Mutex, RwLock}; use tracing::warn; @@ -21,7 +21,7 @@ pub enum ChildProcessMessage { #[napi] pub struct ChildProcess { parser: Arc>, - process_killer: Box, + process_killer: ProcessKiller, message_receiver: Receiver, pub(crate) wait_receiver: Receiver, thread_handles: Vec>, @@ -32,7 +32,7 @@ impl ChildProcess { pub fn new( parser: Arc>, writer_arc: Arc>>, - process_killer: Box, + process_killer: ProcessKiller, message_receiver: Receiver, exit_receiver: Receiver, ) -> Self { @@ -51,9 +51,9 @@ impl ChildProcess { External::new((self.parser.clone(), self.writer_arc.clone())) } - #[napi] - pub fn kill(&mut self) -> anyhow::Result<()> { - self.process_killer.kill().map_err(anyhow::Error::from) + #[napi(ts_args_type = "signal?: NodeJS.Signals")] + pub fn kill(&mut self, signal: Option<&str>) -> anyhow::Result<()> { + self.process_killer.kill(signal) } #[napi] diff --git a/packages/nx/src/native/pseudo_terminal/mod.rs b/packages/nx/src/native/pseudo_terminal/mod.rs index 8c6dbd7f86..170cbb8611 100644 --- a/packages/nx/src/native/pseudo_terminal/mod.rs +++ b/packages/nx/src/native/pseudo_terminal/mod.rs @@ -10,3 +10,7 @@ pub mod child_process; #[cfg_attr(target_os = "macos", path = "mac.rs")] #[cfg_attr(not(target_os = "macos"), path = "non_mac.rs")] pub mod rust_pseudo_terminal; + +#[cfg_attr(windows, path = "process_killer/windows.rs")] +#[cfg_attr(not(windows), path = "process_killer/unix.rs")] +mod process_killer; diff --git a/packages/nx/src/native/pseudo_terminal/process_killer/unix.rs b/packages/nx/src/native/pseudo_terminal/process_killer/unix.rs new file mode 100644 index 0000000000..f6a31ff71c --- /dev/null +++ b/packages/nx/src/native/pseudo_terminal/process_killer/unix.rs @@ -0,0 +1,59 @@ +use nix::{ + sys::signal::{kill, Signal as NixSignal}, + unistd::Pid, +}; + +pub struct ProcessKiller { + pid: i32, +} + +impl ProcessKiller { + pub fn new(pid: i32) -> Self { + Self { pid } + } + + pub fn kill(&self, signal: Option<&str>) -> anyhow::Result<()> { + let pid = Pid::from_raw(self.pid); + match kill( + pid, + NixSignal::from( + Signal::try_from(signal.unwrap_or("SIGINT")).map_err(|e| anyhow::anyhow!(e))?, + ), + ) { + Ok(_) => Ok(()), + Err(e) => Err(anyhow::anyhow!("Failed to kill process: {}", e)), + } + } +} + +enum Signal { + SIGTERM, + SIGINT, + SIGKILL, + SIGHUP, +} + +impl TryFrom<&str> for Signal { + type Error = String; + + fn try_from(value: &str) -> Result { + match value { + "SIGHUP" => Ok(Signal::SIGHUP), + "SIGINT" => Ok(Signal::SIGINT), + "SIGKILL" => Ok(Signal::SIGKILL), + "SIGTERM" => Ok(Signal::SIGTERM), + _ => Err(format!("Invalid signal: {}", value)), + } + } +} + +impl From for NixSignal { + fn from(signal: Signal) -> Self { + match signal { + Signal::SIGTERM => NixSignal::SIGTERM, + Signal::SIGINT => NixSignal::SIGINT, + Signal::SIGKILL => NixSignal::SIGKILL, + Signal::SIGHUP => NixSignal::SIGHUP, + } + } +} diff --git a/packages/nx/src/native/pseudo_terminal/process_killer/windows.rs b/packages/nx/src/native/pseudo_terminal/process_killer/windows.rs new file mode 100644 index 0000000000..580f54f2b6 --- /dev/null +++ b/packages/nx/src/native/pseudo_terminal/process_killer/windows.rs @@ -0,0 +1,47 @@ +use std::ptr::null_mut; + +use winapi::shared::minwindef::DWORD; +use winapi::shared::ntdef::HANDLE; +use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess}; +use winapi::um::winnt::{PROCESS_QUERY_INFORMATION, PROCESS_TERMINATE}; + +pub struct ProcessKiller { + pid: i32, +} + +impl ProcessKiller { + pub fn new(pid: i32) -> Self { + Self { pid } + } + + // windows doesn't have different signals to kill with + pub fn kill(&self, _: Option) -> anyhow::Result<()> { + let pc = WinProcess::open(self.pid as DWORD).expect("!open"); + pc.kill() + .map_err(|e| anyhow::anyhow!("Failed to kill process {}: {}", self.pid, e))?; + Ok(()) + } +} + +struct WinProcess(HANDLE); +impl WinProcess { + fn open(pid: DWORD) -> anyhow::Result { + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms684320%28v=vs.85%29.aspx + let pc = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_TERMINATE, 0, pid) }; + if pc == null_mut() { + anyhow::bail!("Failed to open process with pid {}", pid) + } else { + Ok(WinProcess(pc)) + } + } + + fn kill(self) -> Result<(), String> { + unsafe { TerminateProcess(self.0, 1) }; + Ok(()) + } +} +impl Drop for WinProcess { + fn drop(&mut self) { + unsafe { winapi::um::handleapi::CloseHandle(self.0) }; + } +} diff --git a/packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs b/packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs index efbd38af43..7e6b7f774d 100644 --- a/packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs +++ b/packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs @@ -23,7 +23,7 @@ use tracing::log::trace; use vt100_ctt::Parser; use super::os; -use crate::native::pseudo_terminal::child_process::ChildProcess; +use crate::native::pseudo_terminal::{child_process::ChildProcess, process_killer::ProcessKiller}; pub struct PseudoTerminal { pub pty_pair: PtyPair, @@ -226,7 +226,11 @@ impl PseudoTerminal { trace!("Enabling raw mode"); enable_raw_mode().expect("Failed to enter raw terminal mode"); } - let process_killer = child.clone_killer(); + let process_killer = ProcessKiller::new( + child + .process_id() + .expect("unable to determine child process id") as i32, + ); trace!("Getting running clone"); let running_clone = self.running.clone(); @@ -318,7 +322,7 @@ mod tests { while i < 10 { println!("Running {}", i); let cp1 = pseudo_terminal - .run_command(String::from("whoami"), None, None, None) + .run_command(String::from("whoami"), None, None, None, None, None) .unwrap(); cp1.wait_receiver.recv().unwrap(); i += 1; diff --git a/packages/nx/src/tasks-runner/life-cycle.ts b/packages/nx/src/tasks-runner/life-cycle.ts index 7fc1f16452..25309da528 100644 --- a/packages/nx/src/tasks-runner/life-cycle.ts +++ b/packages/nx/src/tasks-runner/life-cycle.ts @@ -27,7 +27,7 @@ interface RustRunningTask extends RunningTask { onExit(cb: (code: number, terminalOutput: string) => void): void; - kill(signal?: NodeJS.Signals | number): Promise | void; + kill(signal?: NodeJS.Signals): Promise | void; } export interface LifeCycle { diff --git a/packages/nx/src/tasks-runner/pseudo-terminal.ts b/packages/nx/src/tasks-runner/pseudo-terminal.ts index a82870e28e..68d32caeb6 100644 --- a/packages/nx/src/tasks-runner/pseudo-terminal.ts +++ b/packages/nx/src/tasks-runner/pseudo-terminal.ts @@ -6,15 +6,15 @@ import { PseudoIPCServer } from './pseudo-ipc'; import { RunningTask } from './running-tasks/running-task'; // Register single event listeners for all pseudo-terminal instances -const pseudoTerminalShutdownCallbacks: Array<() => void> = []; +const pseudoTerminalShutdownCallbacks: Array<(s?: NodeJS.Signals) => void> = []; process.on('SIGINT', () => { - pseudoTerminalShutdownCallbacks.forEach((cb) => cb()); + pseudoTerminalShutdownCallbacks.forEach((cb) => cb('SIGINT')); }); process.on('SIGTERM', () => { - pseudoTerminalShutdownCallbacks.forEach((cb) => cb()); + pseudoTerminalShutdownCallbacks.forEach((cb) => cb('SIGTERM')); }); process.on('SIGHUP', () => { - pseudoTerminalShutdownCallbacks.forEach((cb) => cb()); + pseudoTerminalShutdownCallbacks.forEach((cb) => cb('SIGHUP')); }); process.on('exit', () => { pseudoTerminalShutdownCallbacks.forEach((cb) => cb()); @@ -56,10 +56,10 @@ export class PseudoTerminal { this.initialized = true; } - shutdown() { + shutdown(s?: NodeJS.Signals) { for (const cp of this.childProcesses) { try { - cp.kill(); + cp.kill(s); } catch {} } if (this.initialized) { @@ -192,10 +192,10 @@ export class PseudoTtyProcess implements RunningTask { this.outputCallbacks.push(callback); } - kill(): void { + kill(s?: NodeJS.Signals): void { if (this.isAlive) { try { - this.childProcess.kill(); + this.childProcess.kill(s); } catch { // when the child process completes before we explicitly call kill, this will throw // do nothing diff --git a/packages/nx/src/tasks-runner/running-tasks/batch-process.ts b/packages/nx/src/tasks-runner/running-tasks/batch-process.ts index 22899eb266..2c878133bf 100644 --- a/packages/nx/src/tasks-runner/running-tasks/batch-process.ts +++ b/packages/nx/src/tasks-runner/running-tasks/batch-process.ts @@ -76,7 +76,7 @@ export class BatchProcess { } } - kill(signal?: NodeJS.Signals | number): void { + kill(signal?: NodeJS.Signals): void { if (this.childProcess.connected) { this.childProcess.kill(signal); } diff --git a/packages/nx/src/tasks-runner/running-tasks/node-child-process.ts b/packages/nx/src/tasks-runner/running-tasks/node-child-process.ts index 843da31465..8411207979 100644 --- a/packages/nx/src/tasks-runner/running-tasks/node-child-process.ts +++ b/packages/nx/src/tasks-runner/running-tasks/node-child-process.ts @@ -86,7 +86,7 @@ export class NodeChildProcessWithNonDirectOutput implements RunningTask { } } - public kill(signal?: NodeJS.Signals | number) { + public kill(signal?: NodeJS.Signals) { if (this.childProcess.connected) { this.childProcess.kill(signal); } @@ -209,7 +209,7 @@ export class NodeChildProcessWithDirectOutput implements RunningTask { return this.terminalOutput; } - kill(signal?: NodeJS.Signals | number): void { + kill(signal?: NodeJS.Signals): void { if (this.childProcess.connected) { this.childProcess.kill(signal); } diff --git a/packages/nx/src/tasks-runner/running-tasks/running-task.ts b/packages/nx/src/tasks-runner/running-tasks/running-task.ts index 062e6c0af9..8e0e4dfe2b 100644 --- a/packages/nx/src/tasks-runner/running-tasks/running-task.ts +++ b/packages/nx/src/tasks-runner/running-tasks/running-task.ts @@ -5,7 +5,7 @@ export abstract class RunningTask { abstract onExit(cb: (code: number) => void): void; - abstract kill(signal?: NodeJS.Signals | number): Promise | void; + abstract kill(signal?: NodeJS.Signals): Promise | void; abstract onOutput?(cb: (output: string) => void): void;