From b753b82f619f5f1f0bb8720b0ce864711cb1adab Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Thu, 4 Jun 2026 11:06:33 +0800 Subject: [PATCH 1/3] Implements a Windows-specific kill shim using TerminateProcess, registers it in the multi-call binary, and documents the limited TERM/KILL compatibility semantics. --- Cargo.toml | 2 + README.md | 4 +- build.rs | 4 + src/kill.rs | 266 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 9 ++ 5 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 src/kill.rs diff --git a/Cargo.toml b/Cargo.toml index 5e97da5..4935e20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,8 +130,10 @@ ntfind = { package = "find", path = "deps/ntfind" } [dependencies.windows-sys] version = "*" features = [ + "Win32_Foundation", "Win32_System_Console", "Win32_System_Registry", + "Win32_System_Threading", ] [build-dependencies] diff --git a/README.md b/README.md index b639e12..e8e3f39 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Legend: ✅ ships and works · ⚠️ ships but conflicts with a built-in · | `expand` | 🛑 | 🛑 | Conflicts with the built-in DOS command | | `find` | ✅ | ✅ | Integrated port of the original DOS command | | `hostname` | ✅ | ✅ | Superset of the Windows built-in | -| `kill` | 🛑 | 🛑 | Unavailable due to lack of signals on Windows; Implementing a form of SIGTERM/SIGKILL may be possible in the future however | +| `kill` | ✅ | ⚠️ | Terminates processes by PID. `TERM` and `KILL` are accepted for compatibility and both map to Windows process termination. | | `ls` | ✅ | ⚠️ | | | `mkdir` | ⚠️ | ⚠️ | | | `more` | 🛑 | 🛑 | Conflicts with the built-in DOS command (consider `edit` as an alternative) | @@ -72,7 +72,7 @@ Legend: ✅ ships and works · ⚠️ ships but conflicts with a built-in · | `sleep` | ✅ | ⚠️ | | | `sort` | ✅ | ✅ | Integrated port of the original DOS command | | `tee` | ✅ | ⚠️ | | -| `timeout` | 🛑 | 🛑 | Relies on `kill`'s functionality | +| `timeout` | 🛑 | 🛑 | Requires broader timeout and process-management support | | `uptime` | ✅ | ⚠️ | | | `whoami` | 🛑 | 🛑 | Conflicts with the built-in Windows command | diff --git a/build.rs b/build.rs index 4752313..94a26f0 100644 --- a/build.rs +++ b/build.rs @@ -61,6 +61,10 @@ fn generate_uutils_map() { entries.push(("sort".into(), "(sort_uumain, sort_uu_app)".into())); } + if !coreutils.iter().any(|(util, _)| util == "kill") { + entries.push(("kill".into(), "(kill_uumain, kill_uu_app)".into())); + } + entries.sort(); let mut phf_map = phf_codegen::OrderedMap::new(); diff --git a/src/kill.rs b/src/kill.rs new file mode 100644 index 0000000..f94ba81 --- /dev/null +++ b/src/kill.rs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::ffi::OsString; +use std::io; + +use clap::Command; +use uucore::Args; +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_TERMINATE, TerminateProcess}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum KillSignal { + Term, + Kill, +} + +impl KillSignal { + fn exit_code(self) -> u32 { + match self { + Self::Term => 15, + Self::Kill => 9, + } + } +} + +#[derive(Debug, Eq, PartialEq)] +enum KillAction { + Help, + Version, + List, + Terminate { signal: KillSignal, pids: Vec }, +} + +pub fn uumain(args: T) -> i32 { + let mut args = args.into_iter(); + let program = args.next().unwrap_or_else(|| OsString::from("kill")); + let program = program.to_string_lossy(); + let args: Vec = args.collect(); + + match parse_args(&args) { + Ok(KillAction::Help) => { + print_help(&program); + 0 + } + Ok(KillAction::Version) => { + println!("kill {VERSION}"); + 0 + } + Ok(KillAction::List) => { + println!("TERM KILL"); + 0 + } + Ok(KillAction::Terminate { signal, pids }) => { + let mut status = 0; + for pid in pids { + if let Err(err) = terminate_pid(pid, signal) { + eprintln!("kill: ({pid}) - {err}"); + status = 1; + } + } + status + } + Err(err) => { + eprintln!("kill: {err}"); + 1 + } + } +} + +pub fn uu_app() -> Command { + Command::new("kill") + .version(VERSION) + .about("Terminate Windows processes by process ID") +} + +fn parse_args(args: &[OsString]) -> Result { + let mut signal = KillSignal::Term; + let mut pids = Vec::new(); + let mut options_done = false; + let mut idx = 0; + + while idx < args.len() { + let arg = args[idx].to_string_lossy(); + + if !options_done { + match arg.as_ref() { + "--" => { + options_done = true; + idx += 1; + continue; + } + "--help" | "-h" => return Ok(KillAction::Help), + "--version" | "-V" => return Ok(KillAction::Version), + "--list" | "-l" => return Ok(KillAction::List), + "--signal" | "-s" => { + idx += 1; + let Some(value) = args.get(idx) else { + return Err(format!("option '{arg}' requires an argument")); + }; + signal = parse_signal(&value.to_string_lossy())?; + idx += 1; + continue; + } + _ if arg.starts_with("--signal=") => { + signal = parse_signal(&arg["--signal=".len()..])?; + idx += 1; + continue; + } + _ if arg.starts_with('-') && arg.len() > 1 => { + signal = parse_signal(&arg[1..]) + .map_err(|_| format!("unsupported option '{arg}'"))?; + idx += 1; + continue; + } + _ => {} + } + } + + pids.push(parse_pid(&arg)?); + idx += 1; + } + + if pids.is_empty() { + return Err("missing process id".to_string()); + } + + Ok(KillAction::Terminate { signal, pids }) +} + +fn parse_signal(value: &str) -> Result { + match value.to_ascii_uppercase().trim_start_matches("SIG") { + "TERM" | "15" => Ok(KillSignal::Term), + "KILL" | "9" => Ok(KillSignal::Kill), + _ => Err(format!("unsupported signal '{value}'")), + } +} + +fn parse_pid(value: &str) -> Result { + let pid = value + .parse() + .map_err(|err| format!("invalid process id '{value}': {err}"))?; + + if pid == 0 { + Err("invalid process id '0'".to_string()) + } else { + Ok(pid) + } +} + +fn terminate_pid(pid: u32, signal: KillSignal) -> io::Result<()> { + let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) }; + if handle.is_null() { + return Err(io::Error::last_os_error()); + } + + let ret = unsafe { TerminateProcess(handle, signal.exit_code()) }; + let result = if ret == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + }; + + unsafe { CloseHandle(handle) }; + result +} + +fn print_help(program: &str) { + println!( + "\ +Usage: {program} [OPTION] PID... + +Terminate Windows processes by process ID. + +Options: + -s, --signal SIGNAL accept TERM, KILL, 15, or 9 + -l, --list list supported signal names + --help display this help and exit + --version output version information and exit" + ); +} + +#[cfg(test)] +mod tests { + use super::{KillAction, KillSignal, parse_args, parse_pid, parse_signal}; + use std::ffi::OsString; + + fn args(values: &[&str]) -> Vec { + values.iter().map(OsString::from).collect() + } + + #[test] + fn parses_default_term_signal() { + assert_eq!( + parse_args(&args(&["123", "456"])), + Ok(KillAction::Terminate { + signal: KillSignal::Term, + pids: vec![123, 456], + }) + ); + } + + #[test] + fn parses_signal_options() { + assert_eq!(parse_signal("TERM"), Ok(KillSignal::Term)); + assert_eq!(parse_signal("sigterm"), Ok(KillSignal::Term)); + assert_eq!(parse_signal("15"), Ok(KillSignal::Term)); + assert_eq!(parse_signal("KILL"), Ok(KillSignal::Kill)); + assert_eq!(parse_signal("9"), Ok(KillSignal::Kill)); + } + + #[test] + fn parses_obsolete_signal_form() { + assert_eq!( + parse_args(&args(&["-9", "123"])), + Ok(KillAction::Terminate { + signal: KillSignal::Kill, + pids: vec![123], + }) + ); + } + + #[test] + fn parses_long_signal_form() { + assert_eq!( + parse_args(&args(&["--signal=KILL", "123"])), + Ok(KillAction::Terminate { + signal: KillSignal::Kill, + pids: vec![123], + }) + ); + + assert_eq!( + parse_args(&args(&["-s", "TERM", "123"])), + Ok(KillAction::Terminate { + signal: KillSignal::Term, + pids: vec![123], + }) + ); + } + + #[test] + fn parses_end_of_options() { + assert_eq!( + parse_args(&args(&["--", "123"])), + Ok(KillAction::Terminate { + signal: KillSignal::Term, + pids: vec![123], + }) + ); + } + + #[test] + fn rejects_invalid_pid() { + assert!(parse_pid("0").is_err()); + assert!(parse_pid("abc").is_err()); + } + + #[test] + fn rejects_unsupported_signal() { + assert!(parse_args(&args(&["-HUP", "123"])).is_err()); + assert!(parse_signal("HUP").is_err()); + } +} diff --git a/src/main.rs b/src/main.rs index b0838c7..0c06f38 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ // Microsoft-authored changes, which Microsoft makes available to uutils // under the uutils MIT License for upstream incorporation. See NOTICE.md. +mod kill; mod nthelpers; use std::borrow::Cow; @@ -320,3 +321,11 @@ fn sort_uumain(args: T) -> i32 { fn sort_uu_app() -> Command { sort::uu_app() } + +fn kill_uumain(args: T) -> i32 { + kill::uumain(args) +} + +fn kill_uu_app() -> Command { + kill::uu_app() +} From bcf4f66f30eef21f9cea1b864b15307eedffd282 Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Thu, 4 Jun 2026 13:36:52 +0800 Subject: [PATCH 2/3] Improve Windows kill compatibility --- README.md | 2 +- src/kill.rs | 150 +++++++++++++++++++++++++++++++--- src/pwsh-install-template.ps1 | 2 +- 3 files changed, 139 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e8e3f39..b0b44df 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Legend: ✅ ships and works · ⚠️ ships but conflicts with a built-in · | `expand` | 🛑 | 🛑 | Conflicts with the built-in DOS command | | `find` | ✅ | ✅ | Integrated port of the original DOS command | | `hostname` | ✅ | ✅ | Superset of the Windows built-in | -| `kill` | ✅ | ⚠️ | Terminates processes by PID. `TERM` and `KILL` are accepted for compatibility and both map to Windows process termination. | +| `kill` | ✅ | ⚠️ | Terminates processes by PID. `0` probes access, while `TERM` and `KILL` both map to Windows process termination. Unix process groups aren't supported. | | `ls` | ✅ | ⚠️ | | | `mkdir` | ⚠️ | ⚠️ | | | `more` | 🛑 | 🛑 | Conflicts with the built-in DOS command (consider `edit` as an alternative) | diff --git a/src/kill.rs b/src/kill.rs index f94ba81..37d69d0 100644 --- a/src/kill.rs +++ b/src/kill.rs @@ -13,6 +13,7 @@ const VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum KillSignal { + Zero, Term, Kill, } @@ -20,6 +21,7 @@ enum KillSignal { impl KillSignal { fn exit_code(self) -> u32 { match self { + Self::Zero => 0, Self::Term => 15, Self::Kill => 9, } @@ -30,10 +32,31 @@ impl KillSignal { enum KillAction { Help, Version, - List, + List(Vec), + Table, Terminate { signal: KillSignal, pids: Vec }, } +struct SignalInfo { + signal: KillSignal, + name: &'static str, +} + +const SIGNALS: &[SignalInfo] = &[ + SignalInfo { + signal: KillSignal::Zero, + name: "0", + }, + SignalInfo { + signal: KillSignal::Term, + name: "TERM", + }, + SignalInfo { + signal: KillSignal::Kill, + name: "KILL", + }, +]; + pub fn uumain(args: T) -> i32 { let mut args = args.into_iter(); let program = args.next().unwrap_or_else(|| OsString::from("kill")); @@ -49,14 +72,12 @@ pub fn uumain(args: T) -> i32 { println!("kill {VERSION}"); 0 } - Ok(KillAction::List) => { - println!("TERM KILL"); - 0 - } + Ok(KillAction::List(values)) => list_signals(&values), + Ok(KillAction::Table) => table_signals(), Ok(KillAction::Terminate { signal, pids }) => { let mut status = 0; for pid in pids { - if let Err(err) = terminate_pid(pid, signal) { + if let Err(err) = signal_pid(pid, signal) { eprintln!("kill: ({pid}) - {err}"); status = 1; } @@ -94,7 +115,8 @@ fn parse_args(args: &[OsString]) -> Result { } "--help" | "-h" => return Ok(KillAction::Help), "--version" | "-V" => return Ok(KillAction::Version), - "--list" | "-l" => return Ok(KillAction::List), + "--list" | "-l" => return Ok(KillAction::List(args[idx + 1..].to_vec())), + "--table" | "-t" => return Ok(KillAction::Table), "--signal" | "-s" => { idx += 1; let Some(value) = args.get(idx) else { @@ -132,6 +154,7 @@ fn parse_args(args: &[OsString]) -> Result { fn parse_signal(value: &str) -> Result { match value.to_ascii_uppercase().trim_start_matches("SIG") { + "0" => Ok(KillSignal::Zero), "TERM" | "15" => Ok(KillSignal::Term), "KILL" | "9" => Ok(KillSignal::Kill), _ => Err(format!("unsupported signal '{value}'")), @@ -139,17 +162,79 @@ fn parse_signal(value: &str) -> Result { } fn parse_pid(value: &str) -> Result { + if value == "0" || value.starts_with('-') { + return Err(format!( + "process groups are not supported on Windows: '{value}'" + )); + } + let pid = value .parse() .map_err(|err| format!("invalid process id '{value}': {err}"))?; - if pid == 0 { - Err("invalid process id '0'".to_string()) + Ok(pid) +} + +fn list_signals(values: &[OsString]) -> i32 { + if values.is_empty() { + println!("0 TERM KILL"); + return 0; + } + + let mut status = 0; + for value in values { + let value = value.to_string_lossy(); + match parse_signal(&value) { + Ok(signal) => println!("{}", signal_list_value(signal, &value)), + Err(err) => { + eprintln!("kill: {err}"); + status = 1; + } + } + } + status +} + +fn table_signals() -> i32 { + for info in SIGNALS { + println!("{:>2} {}", info.signal.exit_code(), info.name); + } + 0 +} + +fn signal_list_value(signal: KillSignal, input: &str) -> String { + if input.parse::().is_ok() { + signal_name(signal).to_string() } else { - Ok(pid) + signal.exit_code().to_string() } } +fn signal_name(signal: KillSignal) -> &'static str { + SIGNALS + .iter() + .find(|info| info.signal == signal) + .map_or("UNKNOWN", |info| info.name) +} + +fn signal_pid(pid: u32, signal: KillSignal) -> io::Result<()> { + if signal == KillSignal::Zero { + probe_pid(pid) + } else { + terminate_pid(pid, signal) + } +} + +fn probe_pid(pid: u32) -> io::Result<()> { + let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) }; + if handle.is_null() { + return Err(io::Error::last_os_error()); + } + + unsafe { CloseHandle(handle) }; + Ok(()) +} + fn terminate_pid(pid: u32, signal: KillSignal) -> io::Result<()> { let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) }; if handle.is_null() { @@ -175,8 +260,9 @@ Usage: {program} [OPTION] PID... Terminate Windows processes by process ID. Options: - -s, --signal SIGNAL accept TERM, KILL, 15, or 9 - -l, --list list supported signal names + -s, --signal SIGNAL accept 0, TERM, KILL, 15, or 9 + -l, --list [SIGNAL] list supported signal names or convert SIGNAL + -t, --table list supported signals in a table --help display this help and exit --version output version information and exit" ); @@ -184,7 +270,10 @@ Options: #[cfg(test)] mod tests { - use super::{KillAction, KillSignal, parse_args, parse_pid, parse_signal}; + use super::{ + KillAction, KillSignal, list_signals, parse_args, parse_pid, parse_signal, + signal_list_value, + }; use std::ffi::OsString; fn args(values: &[&str]) -> Vec { @@ -209,6 +298,7 @@ mod tests { assert_eq!(parse_signal("15"), Ok(KillSignal::Term)); assert_eq!(parse_signal("KILL"), Ok(KillSignal::Kill)); assert_eq!(parse_signal("9"), Ok(KillSignal::Kill)); + assert_eq!(parse_signal("0"), Ok(KillSignal::Zero)); } #[test] @@ -255,6 +345,7 @@ mod tests { #[test] fn rejects_invalid_pid() { assert!(parse_pid("0").is_err()); + assert!(parse_pid("-123").is_err()); assert!(parse_pid("abc").is_err()); } @@ -263,4 +354,37 @@ mod tests { assert!(parse_args(&args(&["-HUP", "123"])).is_err()); assert!(parse_signal("HUP").is_err()); } + + #[test] + fn parses_zero_signal_probe() { + assert_eq!( + parse_args(&args(&["-0", "123"])), + Ok(KillAction::Terminate { + signal: KillSignal::Zero, + pids: vec![123], + }) + ); + } + + #[test] + fn parses_list_and_table_modes() { + assert_eq!( + parse_args(&args(&["-l", "9", "TERM"])), + Ok(KillAction::List(args(&["9", "TERM"]))) + ); + assert_eq!(parse_args(&args(&["--table"])), Ok(KillAction::Table)); + } + + #[test] + fn converts_list_values() { + assert_eq!(signal_list_value(KillSignal::Kill, "9"), "KILL"); + assert_eq!(signal_list_value(KillSignal::Term, "TERM"), "15"); + assert_eq!(list_signals(&args(&["9", "TERM"])), 0); + } + + #[test] + fn rejects_process_groups() { + let err = parse_args(&args(&["--", "-123"])).unwrap_err(); + assert!(err.contains("process groups are not supported")); + } } diff --git a/src/pwsh-install-template.ps1 b/src/pwsh-install-template.ps1 index 335124f..2398efc 100644 --- a/src/pwsh-install-template.ps1 +++ b/src/pwsh-install-template.ps1 @@ -7,7 +7,7 @@ $script:__COREUTILS__ = [System.Collections.Generic.HashSet[string]]::new( 'csplit', 'cut', 'date', 'df', 'dirname', 'du', 'echo', 'env', 'expr', 'factor', 'false', 'find', 'fmt', 'fold', 'grep', - 'head', 'hostname', 'join', 'la', 'link', + 'head', 'hostname', 'join', 'kill', 'la', 'link', 'ln', 'ls', 'md5sum', 'mkdir', 'mktemp', 'mv', 'nl', 'nproc', 'numfmt', 'od', 'pathchk', 'pr', 'printenv', 'printf', 'ptx', From cacb173d9ed4df913b36015a66bf8de79c02784a Mon Sep 17 00:00:00 2001 From: arookieofc <2128194521@qq.com> Date: Thu, 4 Jun 2026 13:53:31 +0800 Subject: [PATCH 3/3] Move Windows kill shim into deps crate --- Cargo.lock | 10 ++++++++++ Cargo.toml | 3 +-- deps/ntkill/Cargo.toml | 19 +++++++++++++++++++ src/kill.rs => deps/ntkill/src/lib.rs | 6 ++---- src/main.rs | 5 ++--- 5 files changed, 34 insertions(+), 9 deletions(-) create mode 100644 deps/ntkill/Cargo.toml rename src/kill.rs => deps/ntkill/src/lib.rs (99%) diff --git a/Cargo.lock b/Cargo.lock index 79e71f5..efd3589 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,6 +418,7 @@ dependencies = [ "find", "findutils", "itertools", + "ntkill", "phf", "phf_codegen", "regex", @@ -1654,6 +1655,15 @@ dependencies = [ "bitflags 2.11.1", ] +[[package]] +name = "ntkill" +version = "0.0.0" +dependencies = [ + "clap", + "uucore", + "windows-sys 0.59.0", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" diff --git a/Cargo.toml b/Cargo.toml index 4935e20..ff813cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,15 +125,14 @@ uptime = { package = "uu_uptime", path = "deps/coreutils/src/uu/uptime" } findutils = { package = "findutils", path = "deps/findutils" } grep = { package = "uu_grep", path = "deps/grep" } ntfind = { package = "find", path = "deps/ntfind" } +ntkill = { path = "deps/ntkill" } # For registry access in main.rs [dependencies.windows-sys] version = "*" features = [ - "Win32_Foundation", "Win32_System_Console", "Win32_System_Registry", - "Win32_System_Threading", ] [build-dependencies] diff --git a/deps/ntkill/Cargo.toml b/deps/ntkill/Cargo.toml new file mode 100644 index 0000000..c00f33c --- /dev/null +++ b/deps/ntkill/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ntkill" +edition = "2024" +license = "MIT" +repository = "https://github.com/microsoft/coreutils" +rust-version = "1.88.0" +version = "0.0.0" +publish = false + +[dependencies] +clap = { version = "4.5", features = ["wrap_help", "cargo", "color"] } +uucore = { path = "../coreutils/src/uucore" } + +[dependencies.windows-sys] +version = "*" +features = [ + "Win32_Foundation", + "Win32_System_Threading", +] diff --git a/src/kill.rs b/deps/ntkill/src/lib.rs similarity index 99% rename from src/kill.rs rename to deps/ntkill/src/lib.rs index 37d69d0..942a1f2 100644 --- a/src/kill.rs +++ b/deps/ntkill/src/lib.rs @@ -168,11 +168,9 @@ fn parse_pid(value: &str) -> Result { )); } - let pid = value + value .parse() - .map_err(|err| format!("invalid process id '{value}': {err}"))?; - - Ok(pid) + .map_err(|err| format!("invalid process id '{value}': {err}")) } fn list_signals(values: &[OsString]) -> i32 { diff --git a/src/main.rs b/src/main.rs index 0c06f38..fcbb17b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,6 @@ // Microsoft-authored changes, which Microsoft makes available to uutils // under the uutils MIT License for upstream incorporation. See NOTICE.md. -mod kill; mod nthelpers; use std::borrow::Cow; @@ -323,9 +322,9 @@ fn sort_uu_app() -> Command { } fn kill_uumain(args: T) -> i32 { - kill::uumain(args) + ntkill::uumain(args) } fn kill_uu_app() -> Command { - kill::uu_app() + ntkill::uu_app() }