From 35a4ba3e79ca8933f7136b1f891c33b863ff7581 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 14 Nov 2024 11:31:03 +0800 Subject: [PATCH 01/21] pkill: init source --- .idea/.gitignore | 5 + .idea/modules.xml | 8 + .idea/procps.iml | 23 ++ .idea/vcs.xml | 6 + Cargo.lock | 11 + Cargo.toml | 2 + build.rs | 2 +- src/uu/pkill/Cargo.toml | 26 ++ src/uu/pkill/pkill.md | 7 + src/uu/pkill/src/main.rs | 1 + src/uu/pkill/src/pkill.rs | 324 +++++++++++++++++++++++++ src/uu/pkill/src/process.rs | 468 ++++++++++++++++++++++++++++++++++++ 12 files changed, 882 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/modules.xml create mode 100644 .idea/procps.iml create mode 100644 .idea/vcs.xml create mode 100644 src/uu/pkill/Cargo.toml create mode 100644 src/uu/pkill/pkill.md create mode 100644 src/uu/pkill/src/main.rs create mode 100644 src/uu/pkill/src/pkill.rs create mode 100644 src/uu/pkill/src/process.rs diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..10b731c5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..8b5ca44a --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/procps.iml b/.idea/procps.iml new file mode 100644 index 00000000..792021de --- /dev/null +++ b/.idea/procps.iml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index aca53cd7..9e5f1840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -633,6 +633,7 @@ dependencies = [ "uu_pgrep", "uu_pidof", "uu_pidwait", + "uu_pkill", "uu_pmap", "uu_ps", "uu_pwdx", @@ -1053,6 +1054,16 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_pkill" +version = "0.0.1" +dependencies = [ + "clap", + "regex", + "uucore", + "walkdir", +] + [[package]] name = "uu_pmap" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index c7cdf747..ee17a11c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ feat_common_core = [ "pidwait", "top", "snice", + "pkill", ] [workspace.dependencies] @@ -84,6 +85,7 @@ ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" pidwait = { optional = true, version = "0.0.1", package = "uu_pidwait", path = "src/uu/pidwait" } top = { optional = true, version = "0.0.1", package = "uu_top", path = "src/uu/top" } snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" } +pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/uu/pkill" } [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/build.rs b/build.rs index a4390333..616b5bd5 100644 --- a/build.rs +++ b/build.rs @@ -11,7 +11,7 @@ use std::io::Write; use std::path::Path; pub fn main() { - if let Ok(profile) = env::var("PROFILE") { + if let Ok(profile) = env::var("PROFILEjh") { println!("cargo:rustc-cfg=build={profile:?}"); } diff --git a/src/uu/pkill/Cargo.toml b/src/uu/pkill/Cargo.toml new file mode 100644 index 00000000..09f9aa06 --- /dev/null +++ b/src/uu/pkill/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "uu_pkill" +version = "0.0.1" +edition = "2021" +authors = ["uutils developers"] +license = "MIT" +description = "pgrep ~ (uutils) Kills processes based on name and other attributes." + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps/tree/main/src/uu/pkill" +keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +walkdir = { workspace = true } +regex = { workspace = true } + +[lib] +path = "src/pkill.rs" + +[[bin]] +name = "pgrep" +path = "src/main.rs" diff --git a/src/uu/pkill/pkill.md b/src/uu/pkill/pkill.md new file mode 100644 index 00000000..c8353288 --- /dev/null +++ b/src/uu/pkill/pkill.md @@ -0,0 +1,7 @@ +# pgrep + +``` +pkill [options] +``` + +Kills processes based on name and other attributes. diff --git a/src/uu/pkill/src/main.rs b/src/uu/pkill/src/main.rs new file mode 100644 index 00000000..7bba1735 --- /dev/null +++ b/src/uu/pkill/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_pkill); diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs new file mode 100644 index 00000000..cd6d1e59 --- /dev/null +++ b/src/uu/pkill/src/pkill.rs @@ -0,0 +1,324 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// Pid utils +pub mod process; + +use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; +use process::{walk_process, ProcessInformation, Teletype}; +use regex::Regex; +use std::{collections::HashSet, sync::OnceLock}; +use uucore::{ + error::{UResult, USimpleError}, + format_usage, help_about, help_usage, +}; + +const ABOUT: &str = help_about!("pkill.md"); +const USAGE: &str = help_usage!("pkill.md"); + +static REGEX: OnceLock = OnceLock::new(); + +struct Settings { + exact: bool, + full: bool, + ignore_case: bool, + inverse: bool, + newest: bool, + oldest: bool, + older: Option, + parent: Option>, + runstates: Option, + terminal: Option>, +} + +/// # Conceptual model of `pgrep` +/// +/// At first, `pgrep` command will check the patterns is legal. +/// In this stage, `pgrep` will construct regex if `--exact` argument was passed. +/// +/// Then, `pgrep` will collect all *matched* pids, and filtering them. +/// In this stage `pgrep` command will collect all the pids and its information from __/proc/__ +/// file system. At the same time, `pgrep` will construct filters from command +/// line arguments to filter the collected pids. Note that the "-o" and "-n" flag filters works +/// if them enabled and based on general collecting result. +/// +/// Last, `pgrep` will construct output format from arguments, and print the processed result. +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + + let pattern = try_get_pattern_from(&matches)?; + REGEX + .set(Regex::new(&pattern).map_err(|e| USimpleError::new(2, e.to_string()))?) + .unwrap(); + + let settings = Settings { + exact: matches.get_flag("exact"), + full: matches.get_flag("full"), + ignore_case: matches.get_flag("ignore-case"), + inverse: matches.get_flag("inverse"), + newest: matches.get_flag("newest"), + oldest: matches.get_flag("oldest"), + parent: matches + .get_many::("parent") + .map(|parents| parents.copied().collect()), + runstates: matches.get_one::("runstates").cloned(), + older: matches.get_one::("older").copied(), + terminal: matches.get_many::("terminal").map(|ttys| { + ttys.cloned() + .flat_map(Teletype::try_from) + .collect::>() + }), + }; + + if (!settings.newest + && !settings.oldest + && settings.runstates.is_none() + && settings.older.is_none() + && settings.parent.is_none() + && settings.terminal.is_none()) + && pattern.is_empty() + { + return Err(USimpleError::new( + 2, + "no matching criteria specified\nTry `pgrep --help' for more information.", + )); + } + + // Collect pids + let pids = { + let mut pids = collect_matched_pids(&settings); + if pids.is_empty() { + uucore::error::set_exit_code(1); + pids + } else { + process_flag_o_n(&settings, &mut pids) + } + }; + + // Processing output + let output = if matches.get_flag("count") { + format!("{}", pids.len()) + } else { + let delimiter = matches.get_one::("delimiter").unwrap(); + + let formatted: Vec<_> = if matches.get_flag("list-full") { + pids.into_iter() + .map(|it| { + // pgrep from procps-ng outputs the process name inside square brackets + // if /proc//cmdline is empty + if it.cmdline.is_empty() { + format!("{} [{}]", it.pid, it.clone().status().get("Name").unwrap()) + } else { + format!("{} {}", it.pid, it.cmdline) + } + }) + .collect() + } else if matches.get_flag("list-name") { + pids.into_iter() + .map(|it| format!("{} {}", it.pid, it.clone().status().get("Name").unwrap())) + .collect() + } else { + pids.into_iter().map(|it| format!("{}", it.pid)).collect() + }; + + formatted.join(delimiter) + }; + + if !output.is_empty() { + println!("{}", output); + }; + + Ok(()) +} + +/// Try to get the pattern from the command line arguments. Returns an empty string if no pattern +/// is specified. +fn try_get_pattern_from(matches: &ArgMatches) -> UResult { + let pattern = match matches.get_many::("pattern") { + Some(patterns) if patterns.len() > 1 => { + return Err(USimpleError::new( + 2, + "only one pattern can be provided\nTry `pgrep --help' for more information.", + )) + } + Some(mut patterns) => patterns.next().unwrap(), + None => return Ok(String::new()), + }; + + let pattern = if matches.get_flag("ignore-case") { + &pattern.to_lowercase() + } else { + pattern + }; + + let pattern = if matches.get_flag("exact") { + &format!("^{}$", pattern) + } else { + pattern + }; + + Ok(pattern.to_string()) +} + +/// Collect pids with filter construct from command line arguments +fn collect_matched_pids(settings: &Settings) -> Vec { + // Filtration general parameters + let filtered: Vec = { + let mut tmp_vec = Vec::new(); + + for mut pid in walk_process().collect::>() { + let run_state_matched = match (&settings.runstates, (pid).run_state()) { + (Some(arg_run_states), Ok(pid_state)) => { + arg_run_states.contains(&pid_state.to_string()) + } + (_, Err(_)) => false, + _ => true, + }; + + let binding = pid.status(); + let name = binding.get("Name").unwrap(); + let name = if settings.ignore_case { + name.to_lowercase() + } else { + name.into() + }; + let pattern_matched = { + let want = if settings.exact { + // Equals `Name` in /proc//status + // The `unwrap` operation must succeed + // because the REGEX has been verified as correct in `uumain`. + &name + } else if settings.full { + // Equals `cmdline` in /proc//cmdline + &pid.cmdline + } else { + // From manpage: + // The process name used for matching is limited to the 15 characters present in the output of /proc/pid/stat. + &pid.proc_stat()[..15] + }; + + REGEX.get().unwrap().is_match(want) + }; + + let tty_matched = match &settings.terminal { + Some(ttys) => ttys.contains(&pid.tty()), + None => true, + }; + + let arg_older = settings.older.unwrap_or(0); + let older_matched = pid.start_time().unwrap() >= arg_older; + + // the PPID is the fourth field in /proc//stat + // (https://www.kernel.org/doc/html/latest/filesystems/proc.html#id10) + let stat = pid.stat(); + let ppid = stat.get(3); + let parent_matched = match (&settings.parent, ppid) { + (Some(parents), Some(ppid)) => parents.contains(&ppid.parse::().unwrap()), + _ => true, + }; + + if (run_state_matched + && pattern_matched + && tty_matched + && older_matched + && parent_matched) + ^ settings.inverse + { + tmp_vec.push(pid); + } + } + tmp_vec + }; + + filtered +} + +/// Sorting pids for flag `-o` and `-n`. +/// +/// This function can also be used as a filter to filter out process information. +fn process_flag_o_n( + settings: &Settings, + pids: &mut [ProcessInformation], +) -> Vec { + if settings.oldest || settings.newest { + pids.sort_by(|a, b| { + b.clone() + .start_time() + .unwrap() + .cmp(&a.clone().start_time().unwrap()) + }); + + let start_time = if settings.newest { + pids.first().cloned().unwrap().start_time().unwrap() + } else { + pids.last().cloned().unwrap().start_time().unwrap() + }; + + // There might be some process start at same time, so need to be filtered. + let mut filtered = pids + .iter() + .filter(|it| (*it).clone().start_time().unwrap() == start_time) + .collect::>(); + + if settings.newest { + filtered.sort_by(|a, b| b.pid.cmp(&a.pid)); + } else { + filtered.sort_by(|a, b| a.pid.cmp(&b.pid)); + } + + vec![filtered.first().cloned().unwrap().clone()] + } else { + pids.to_vec() + } +} + +#[allow(clippy::cognitive_complexity)] +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .args_override_self(true) + .group(ArgGroup::new("oldest_newest").args(["oldest", "newest", "inverse"])) + .args([ + arg!(-d --delimiter "specify output delimiter") + .default_value("\n") + .hide_default_value(true), + arg!(-l --"list-name" "list PID and process name"), + arg!(-a --"list-full" "list PID and full command line"), + arg!(-v --inverse "negates the matching"), + // arg!(-w --lightweight "list all TID"), + arg!(-c --count "count of matching processes"), + arg!(-f --full "use full process name to match"), + // arg!(-g --pgroup ... "match listed process group IDs"), + // arg!(-G --group ... "match real group IDs"), + arg!(-i --"ignore-case" "match case insensitively"), + arg!(-n --newest "select most recently started"), + arg!(-o --oldest "select least recently started"), + arg!(-O --older "select where older than seconds") + .value_parser(clap::value_parser!(u64)), + arg!(-P --parent "match only child processes of the given parent") + .value_delimiter(',') + .value_parser(clap::value_parser!(u64)), + // arg!(-s --session "match session IDs"), + arg!(-t --terminal "match by controlling terminal") + .value_delimiter(','), + // arg!(-u --euid ... "match by effective IDs"), + // arg!(-U --uid ... "match by real IDs"), + arg!(-x --exact "match exactly with the command name"), + // arg!(-F --pidfile "read PIDs from file"), + // arg!(-L --logpidfile "fail if PID file is not locked"), + arg!(-r --runstates "match runstates [D,S,Z,...]"), + // arg!( --ns "match the processes that belong to the same namespace as "), + // arg!( --nslist ... "list which namespaces will be considered for the --ns option."), + ]) + .arg( + Arg::new("pattern") + .help("Name of the program to find the PID of") + .action(ArgAction::Append) + .index(1), + ) +} diff --git a/src/uu/pkill/src/process.rs b/src/uu/pkill/src/process.rs new file mode 100644 index 00000000..d0c63c70 --- /dev/null +++ b/src/uu/pkill/src/process.rs @@ -0,0 +1,468 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::hash::Hash; +use std::{ + collections::HashMap, + fmt::{self, Display, Formatter}, + fs, io, + path::PathBuf, + rc::Rc, +}; +use walkdir::{DirEntry, WalkDir}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Teletype { + Tty(u64), + TtyS(u64), + Pts(u64), + Unknown, +} + +impl Display for Teletype { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Tty(id) => write!(f, "/dev/pts/{}", id), + Self::TtyS(id) => write!(f, "/dev/tty{}", id), + Self::Pts(id) => write!(f, "/dev/ttyS{}", id), + Self::Unknown => write!(f, "?"), + } + } +} + +impl TryFrom for Teletype { + type Error = (); + + fn try_from(value: String) -> Result { + if value == "?" { + return Ok(Self::Unknown); + } + + Self::try_from(value.as_str()) + } +} + +impl TryFrom<&str> for Teletype { + type Error = (); + + fn try_from(value: &str) -> Result { + Self::try_from(PathBuf::from(value)) + } +} + +impl TryFrom for Teletype { + type Error = (); + + fn try_from(value: PathBuf) -> Result { + // Three case: /dev/pts/* , /dev/ttyS**, /dev/tty** + + let mut iter = value.iter(); + // Case 1 + + // Considering this format: **/**/pts/ + if let (Some(_), Some(num)) = (iter.find(|it| *it == "pts"), iter.next()) { + return num + .to_str() + .ok_or(())? + .parse::() + .map_err(|_| ()) + .map(Teletype::Pts); + }; + + // Considering this format: **/**/ttyS** then **/**/tty** + let path = value.to_str().ok_or(())?; + + let f = |prefix: &str| { + value + .iter() + .last()? + .to_str()? + .strip_prefix(prefix)? + .parse::() + .ok() + }; + + if path.contains("ttyS") { + // Case 2 + f("ttyS").ok_or(()).map(Teletype::TtyS) + } else if path.contains("tty") { + // Case 3 + f("tty").ok_or(()).map(Teletype::Tty) + } else { + Err(()) + } + } +} + +/// State or process +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum RunState { + ///`R`, running + Running, + ///`S`, sleeping + Sleeping, + ///`D`, sleeping in an uninterruptible wait + UninterruptibleWait, + ///`Z`, zombie + Zombie, + ///`T`, traced or stopped + Stopped, +} + +impl Display for RunState { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Running => write!(f, "R"), + Self::Sleeping => write!(f, "S"), + Self::UninterruptibleWait => write!(f, "D"), + Self::Zombie => write!(f, "Z"), + Self::Stopped => write!(f, "T"), + } + } +} + +impl TryFrom for RunState { + type Error = io::Error; + + fn try_from(value: char) -> Result { + match value { + 'R' => Ok(Self::Running), + 'S' => Ok(Self::Sleeping), + 'D' => Ok(Self::UninterruptibleWait), + 'Z' => Ok(Self::Zombie), + 'T' => Ok(Self::Stopped), + _ => Err(io::ErrorKind::InvalidInput.into()), + } + } +} + +impl TryFrom<&str> for RunState { + type Error = io::Error; + + fn try_from(value: &str) -> Result { + if value.len() != 1 { + return Err(io::ErrorKind::InvalidInput.into()); + } + + Self::try_from( + value + .chars() + .nth(0) + .ok_or::(io::ErrorKind::InvalidInput.into())?, + ) + } +} + +impl TryFrom for RunState { + type Error = io::Error; + + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + +impl TryFrom<&String> for RunState { + type Error = io::Error; + + fn try_from(value: &String) -> Result { + Self::try_from(value.as_str()) + } +} + +/// Process ID and its information +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ProcessInformation { + pub pid: usize, + pub cmdline: String, + + inner_status: String, + inner_stat: String, + + /// Processed `/proc/self/status` file + cached_status: Option>>, + /// Processed `/proc/self/stat` file + cached_stat: Option>>, + + cached_start_time: Option, +} + +impl ProcessInformation { + /// Try new with pid path such as `/proc/self` + /// + /// # Error + /// + /// If the files in path cannot be parsed into [ProcessInformation], + /// it almost caused by wrong filesystem structure. + /// + /// - [The /proc Filesystem](https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories) + pub fn try_new(value: PathBuf) -> Result { + let dir_append = |mut path: PathBuf, str: String| { + path.push(str); + path + }; + + let value = if value.is_symlink() { + fs::read_link(value)? + } else { + value + }; + + let pid = { + value + .iter() + .last() + .ok_or(io::ErrorKind::Other)? + .to_str() + .ok_or(io::ErrorKind::InvalidData)? + .parse::() + .map_err(|_| io::ErrorKind::InvalidData)? + }; + let cmdline = fs::read_to_string(dir_append(value.clone(), "cmdline".into()))? + .replace('\0', " ") + .trim_end() + .into(); + + Ok(Self { + pid, + cmdline, + inner_status: fs::read_to_string(dir_append(value.clone(), "status".into()))?, + inner_stat: fs::read_to_string(dir_append(value, "stat".into()))?, + ..Default::default() + }) + } + + pub fn proc_status(&self) -> &str { + &self.inner_status + } + + pub fn proc_stat(&self) -> &str { + &self.inner_stat + } + + /// Collect information from `/proc//status` file + pub fn status(&mut self) -> Rc> { + if let Some(c) = &self.cached_status { + return Rc::clone(c); + } + + let result = self + .inner_status + .lines() + .filter_map(|it| it.split_once(':')) + .map(|it| (it.0.to_string(), it.1.trim_start().to_string())) + .collect::>(); + + let result = Rc::new(result); + self.cached_status = Some(Rc::clone(&result)); + Rc::clone(&result) + } + + /// Collect information from `/proc//stat` file + pub fn stat(&mut self) -> Rc> { + if let Some(c) = &self.cached_stat { + return Rc::clone(c); + } + + let result: Vec<_> = stat_split(&self.inner_stat); + + let result = Rc::new(result); + self.cached_stat = Some(Rc::clone(&result)); + Rc::clone(&result) + } + + /// Fetch start time from [ProcessInformation::cached_stat] + /// + /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) + pub fn start_time(&mut self) -> Result { + if let Some(time) = self.cached_start_time { + return Ok(time); + } + + // Kernel doc: https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories + // Table 1-4 + let time = self + .stat() + .get(21) + .ok_or(io::ErrorKind::InvalidData)? + .parse::() + .map_err(|_| io::ErrorKind::InvalidData)?; + + self.cached_start_time = Some(time); + + Ok(time) + } + + /// Fetch run state from [ProcessInformation::cached_stat] + /// + /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) + /// + /// # Error + /// + /// If parsing failed, this function will return [io::ErrorKind::InvalidInput] + pub fn run_state(&mut self) -> Result { + RunState::try_from(self.stat().get(2).unwrap().as_str()) + } + + /// This function will scan the `/proc//fd` directory + /// + /// If the process does not belong to any terminal and mismatched permission, + /// the result will contain [TerminalType::Unknown]. + /// + /// Otherwise [TerminalType::Unknown] does not appear in the result. + pub fn tty(&self) -> Teletype { + let path = PathBuf::from(format!("/proc/{}/fd", self.pid)); + + let Ok(result) = fs::read_dir(path) else { + return Teletype::Unknown; + }; + + for dir in result.flatten().filter(|it| it.path().is_symlink()) { + if let Ok(path) = fs::read_link(dir.path()) { + if let Ok(tty) = Teletype::try_from(path) { + return tty; + } + } + } + + Teletype::Unknown + } +} +impl TryFrom for ProcessInformation { + type Error = io::Error; + + fn try_from(value: DirEntry) -> Result { + let value = value.into_path(); + + Self::try_new(value) + } +} + +impl Hash for ProcessInformation { + fn hash(&self, state: &mut H) { + // Make it faster. + self.pid.hash(state); + self.inner_status.hash(state); + self.inner_stat.hash(state); + } +} + +/// Parsing `/proc/self/stat` file. +/// +/// In some case, the first pair (and the only one pair) will contains whitespace, +/// so if we want to parse it, we have to write new algorithm. +/// +/// TODO: If possible, test and use regex to replace this algorithm. +fn stat_split(stat: &str) -> Vec { + let stat = String::from(stat); + + let mut buf = String::with_capacity(stat.len()); + + let l = stat.find('('); + let r = stat.find(')'); + let content = if let (Some(l), Some(r)) = (l, r) { + let replaced = stat[(l + 1)..r].replace(' ', "$$"); + + buf.push_str(&stat[..l]); + buf.push_str(&replaced); + buf.push_str(&stat[(r + 1)..stat.len()]); + + &buf + } else { + &stat + }; + + content + .split_whitespace() + .map(|it| it.replace("$$", " ")) + .collect() +} + +/// Iterating pid in current system +pub fn walk_process() -> impl Iterator { + WalkDir::new("/proc/") + .max_depth(1) + .follow_links(false) + .into_iter() + .flatten() + .filter(|it| it.path().is_dir()) + .flat_map(ProcessInformation::try_from) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{collections::HashSet, str::FromStr}; + + #[test] + fn test_run_state_conversion() { + assert_eq!(RunState::try_from("R").unwrap(), RunState::Running); + assert_eq!(RunState::try_from("S").unwrap(), RunState::Sleeping); + assert_eq!( + RunState::try_from("D").unwrap(), + RunState::UninterruptibleWait + ); + assert_eq!(RunState::try_from("T").unwrap(), RunState::Stopped); + assert_eq!(RunState::try_from("Z").unwrap(), RunState::Zombie); + + assert!(RunState::try_from("G").is_err()); + assert!(RunState::try_from("Rg").is_err()); + } + + fn current_pid() -> usize { + // Direct read link of /proc/self. + // It's result must be current programs pid. + fs::read_link("/proc/self") + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap() + } + + #[test] + fn test_walk_pid() { + let current_pid = current_pid(); + + let find = walk_process().find(|it| it.pid == current_pid); + + assert!(find.is_some()); + } + + #[test] + fn test_pid_entry() { + let current_pid = current_pid(); + + let pid_entry = ProcessInformation::try_new( + PathBuf::from_str(&format!("/proc/{}", current_pid)).unwrap(), + ) + .unwrap(); + + let mut result = WalkDir::new(format!("/proc/{}/fd", current_pid)) + .into_iter() + .flatten() + .map(DirEntry::into_path) + .flat_map(|it| it.read_link()) + .flat_map(Teletype::try_from) + .collect::>(); + + if result.is_empty() { + result.insert(Teletype::Unknown); + } + + assert!(result.contains(&pid_entry.tty())); + } + + #[test] + fn test_stat_split() { + let case = "32 (idle_inject/3) S 2 0 0 0 -1 69238848 0 0 0 0 0 0 0 0 -51 0 1 0 34 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 3 50 1 0 0 0 0 0 0 0 0 0 0 0"; + assert!(stat_split(case)[1] == "idle_inject/3"); + + let case = "3508 (sh) S 3478 3478 3478 0 -1 4194304 67 0 0 0 0 0 0 0 20 0 1 0 11911 2961408 238 18446744073709551615 94340156948480 94340157028757 140736274114368 0 0 0 0 4096 65538 1 0 0 17 8 0 0 0 0 0 94340157054704 94340157059616 94340163108864 140736274122780 140736274122976 140736274122976 140736274124784 0"; + assert!(stat_split(case)[1] == "sh"); + + let case = "47246 (kworker /10:1-events) I 2 0 0 0 -1 69238880 0 0 0 0 17 29 0 0 20 0 1 0 1396260 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 10 0 0 0 0 0 0 0 0 0 0 0 0 0"; + assert!(stat_split(case)[1] == "kworker /10:1-events"); + } +} From 2d129207d1d0542c81e6991212563d1ba46cc650 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 24 Dec 2024 01:19:26 +0800 Subject: [PATCH 02/21] pkill: implement --- Cargo.lock | 1 + src/uu/pkill/Cargo.toml | 1 + src/uu/pkill/src/pkill.rs | 208 +++++++++++++++++++++++--------------- 3 files changed, 131 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a1eee8d..0950cbf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,6 +1042,7 @@ name = "uu_pkill" version = "0.0.1" dependencies = [ "clap", + "nix", "regex", "uucore", "walkdir", diff --git a/src/uu/pkill/Cargo.toml b/src/uu/pkill/Cargo.toml index 09f9aa06..dfd79ab4 100644 --- a/src/uu/pkill/Cargo.toml +++ b/src/uu/pkill/Cargo.toml @@ -17,6 +17,7 @@ uucore = { workspace = true } clap = { workspace = true } walkdir = { workspace = true } regex = { workspace = true } +nix = { workspace = true, features = ["signal"] } [lib] path = "src/pkill.rs" diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index cd6d1e59..adda7f2f 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -7,12 +7,19 @@ pub mod process; use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; +use nix::sys::signal::{self, Signal}; +use nix::unistd::Pid; use process::{walk_process, ProcessInformation, Teletype}; use regex::Regex; +use uucore::display::Quotable; +use uucore::error::FromIo; +use uucore::show; +use std::io::Error; use std::{collections::HashSet, sync::OnceLock}; use uucore::{ error::{UResult, USimpleError}, format_usage, help_about, help_usage, + signals::{signal_by_name_or_value, signal_name_by_value, ALL_SIGNALS}, }; const ABOUT: &str = help_about!("pkill.md"); @@ -24,7 +31,6 @@ struct Settings { exact: bool, full: bool, ignore_case: bool, - inverse: bool, newest: bool, oldest: bool, older: Option, @@ -33,21 +39,12 @@ struct Settings { terminal: Option>, } -/// # Conceptual model of `pgrep` -/// -/// At first, `pgrep` command will check the patterns is legal. -/// In this stage, `pgrep` will construct regex if `--exact` argument was passed. -/// -/// Then, `pgrep` will collect all *matched* pids, and filtering them. -/// In this stage `pgrep` command will collect all the pids and its information from __/proc/__ -/// file system. At the same time, `pgrep` will construct filters from command -/// line arguments to filter the collected pids. Note that the "-o" and "-n" flag filters works -/// if them enabled and based on general collecting result. -/// -/// Last, `pgrep` will construct output format from arguments, and print the processed result. #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { - let matches = uu_app().try_get_matches_from(args)?; + let mut args = args.collect_ignore(); + let obs_signal = handle_obsolete(&mut args); + + let matches = uu_app().try_get_matches_from(&args)?; let pattern = try_get_pattern_from(&matches)?; REGEX @@ -58,7 +55,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { exact: matches.get_flag("exact"), full: matches.get_flag("full"), ignore_case: matches.get_flag("ignore-case"), - inverse: matches.get_flag("inverse"), newest: matches.get_flag("newest"), oldest: matches.get_flag("oldest"), parent: matches @@ -83,10 +79,31 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { { return Err(USimpleError::new( 2, - "no matching criteria specified\nTry `pgrep --help' for more information.", + "no matching criteria specified\nTry `pkill --help' for more information.", )); } + // Parse signal + let sig = if let Some(signal) = obs_signal { + signal + } else if let Some(signal) = matches.get_one::("signal") { + parse_signal_value(signal)? + } else { + 15_usize //SIGTERM + }; + + let sig_name = signal_name_by_value(sig); + // Signal does not support converting from EXIT + // Instead, nix::signal::kill expects Option::None to properly handle EXIT + let sig: Option = if sig_name.is_some_and(|name| name == "EXIT") { + None + } else { + let sig = (sig as i32) + .try_into() + .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?; + Some(sig) + }; + // Collect pids let pids = { let mut pids = collect_matched_pids(&settings); @@ -96,40 +113,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { process_flag_o_n(&settings, &mut pids) } - }; - - // Processing output - let output = if matches.get_flag("count") { - format!("{}", pids.len()) - } else { - let delimiter = matches.get_one::("delimiter").unwrap(); - - let formatted: Vec<_> = if matches.get_flag("list-full") { - pids.into_iter() - .map(|it| { - // pgrep from procps-ng outputs the process name inside square brackets - // if /proc//cmdline is empty - if it.cmdline.is_empty() { - format!("{} [{}]", it.pid, it.clone().status().get("Name").unwrap()) - } else { - format!("{} {}", it.pid, it.cmdline) - } - }) - .collect() - } else if matches.get_flag("list-name") { - pids.into_iter() - .map(|it| format!("{} {}", it.pid, it.clone().status().get("Name").unwrap())) - .collect() - } else { - pids.into_iter().map(|it| format!("{}", it.pid)).collect() - }; + }.iter().map(|x| x.pid as i32).collect::>(); - formatted.join(delimiter) - }; - - if !output.is_empty() { - println!("{}", output); - }; + // TODO: Implement -H -q -e + kill(sig, &pids); + if matches.get_flag("count") { + println!("{}", pids.len()); + } Ok(()) } @@ -220,12 +210,11 @@ fn collect_matched_pids(settings: &Settings) -> Vec { _ => true, }; - if (run_state_matched + if run_state_matched && pattern_matched && tty_matched && older_matched - && parent_matched) - ^ settings.inverse + && parent_matched { tmp_vec.push(pid); } @@ -275,6 +264,54 @@ fn process_flag_o_n( } } +fn handle_obsolete(args: &mut Vec) -> Option { + // Sanity check + if args.len() > 2 { + // Old signal can only be in the first argument position + let slice = args[1].as_str(); + if let Some(signal) = slice.strip_prefix('-') { + // Check if it is a valid signal + let opt_signal = signal_by_name_or_value(signal); + if opt_signal.is_some() { + // remove the signal before return + args.remove(1); + return opt_signal; + } + } + } + None +} + +fn parse_signal_value(signal_name: &str) -> UResult { + let optional_signal_value = signal_by_name_or_value(signal_name); + match optional_signal_value { + Some(x) => Ok(x), + None => Err(USimpleError::new( + 1, + format!("Unknown signal {}", signal_name.quote()), + )), + } +} + +fn parse_pids(pids: &[String]) -> UResult> { + pids.iter() + .map(|x| { + x.parse::().map_err(|e| { + USimpleError::new(1, format!("failed to parse argument {}: {}", x.quote(), e)) + }) + }) + .collect() +} + +fn kill(sig: Option, pids: &[i32]) { + for &pid in pids { + if let Err(e) = signal::kill(Pid::from_raw(pid), sig) { + show!(Error::from_raw_os_error(e as i32) + .map_err_context(|| format!("sending signal to {pid} failed"))); + } + } +} + #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) @@ -282,38 +319,51 @@ pub fn uu_app() -> Command { .about(ABOUT) .override_usage(format_usage(USAGE)) .args_override_self(true) - .group(ArgGroup::new("oldest_newest").args(["oldest", "newest", "inverse"])) + .group(ArgGroup::new("oldest_newest").args(["oldest", "newest"])) .args([ - arg!(-d --delimiter "specify output delimiter") - .default_value("\n") - .hide_default_value(true), - arg!(-l --"list-name" "list PID and process name"), - arg!(-a --"list-full" "list PID and full command line"), - arg!(-v --inverse "negates the matching"), - // arg!(-w --lightweight "list all TID"), - arg!(-c --count "count of matching processes"), - arg!(-f --full "use full process name to match"), - // arg!(-g --pgroup ... "match listed process group IDs"), - // arg!(-G --group ... "match real group IDs"), - arg!(-i --"ignore-case" "match case insensitively"), - arg!(-n --newest "select most recently started"), - arg!(-o --oldest "select least recently started"), - arg!(-O --older "select where older than seconds") + // arg!(- "signal to send (either number or name)"), + arg!(-H --"require-handler" "match only if signal handler is present"), + arg!(-q --queue "integer value to be sent with the signal"), + arg!(-e --echo "display what is killed"), + arg!(-c --count "count of matching processes"), + arg!(-f --full "use full process name to match"), + arg!(-g --pgroup "match listed process group IDs") + .value_delimiter(',') .value_parser(clap::value_parser!(u64)), - arg!(-P --parent "match only child processes of the given parent") + arg!(-G --group "match real group IDs") .value_delimiter(',') .value_parser(clap::value_parser!(u64)), - // arg!(-s --session "match session IDs"), - arg!(-t --terminal "match by controlling terminal") + arg!(-i --"ignore-case" "match case insensitively"), + arg!(-n --newest "select most recently started"), + arg!(-o --oldest "select least recently started"), + arg!(-O --older "select where older than seconds") + .value_parser(clap::value_parser!(u64)), + arg!(-P --parent "match only child processes of the given parent") + .value_delimiter(',') + .value_parser(clap::value_parser!(u64)), + arg!(-s --session "match session IDs") + .value_delimiter(',') + .value_parser(clap::value_parser!(u64)), + arg!(--signal "signal to send (either number or name)"), + arg!(-t --terminal "match by controlling terminal") .value_delimiter(','), - // arg!(-u --euid ... "match by effective IDs"), - // arg!(-U --uid ... "match by real IDs"), - arg!(-x --exact "match exactly with the command name"), - // arg!(-F --pidfile "read PIDs from file"), - // arg!(-L --logpidfile "fail if PID file is not locked"), - arg!(-r --runstates "match runstates [D,S,Z,...]"), - // arg!( --ns "match the processes that belong to the same namespace as "), - // arg!( --nslist ... "list which namespaces will be considered for the --ns option."), + arg!(-u --euid "match by effective IDs") + .value_delimiter(',') + .value_parser(clap::value_parser!(u64)), + arg!(-U --uid "match by real IDs") + .value_delimiter(',') + .value_parser(clap::value_parser!(u64)), + arg!(-x --exact "match exactly with the command name"), + arg!(-F --pidfile "read PIDs from file"), + arg!(-L --logpidfile "fail if PID file is not locked"), + arg!(-r --runstates "match runstates [D,S,Z,...]"), + arg!(-A --"ignore-ancestors" "exclude our ancestors from results"), + arg!(--cgroup "match by cgroup v2 names") + .value_delimiter(','), + arg!(--ns "match the processes that belong to the same namespace as "), + arg!(--nslist "list which namespaces will be considered for the --ns option.") + .value_delimiter(',') + .value_parser(["ipc", "mnt", "net", "pid", "user", "uts"]), ]) .arg( Arg::new("pattern") From c206a7c6b285de33e78e2c8a897f0e7c8ed2920e Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 13:21:10 +0800 Subject: [PATCH 03/21] pkill: implement `-e` --- src/uu/pkill/src/pkill.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index adda7f2f..012d3540 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -11,11 +11,11 @@ use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use process::{walk_process, ProcessInformation, Teletype}; use regex::Regex; +use std::io::Error; +use std::{collections::HashSet, sync::OnceLock}; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::show; -use std::io::Error; -use std::{collections::HashSet, sync::OnceLock}; use uucore::{ error::{UResult, USimpleError}, format_usage, help_about, help_usage, @@ -43,7 +43,7 @@ struct Settings { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut args = args.collect_ignore(); let obs_signal = handle_obsolete(&mut args); - + let matches = uu_app().try_get_matches_from(&args)?; let pattern = try_get_pattern_from(&matches)?; @@ -113,10 +113,20 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { process_flag_o_n(&settings, &mut pids) } - }.iter().map(|x| x.pid as i32).collect::>(); + }; - // TODO: Implement -H -q -e - kill(sig, &pids); + // Send signal + // TODO: Implement -H -q + for pid in &pids { + if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { + show!(Error::from_raw_os_error(e as i32) + .map_err_context(|| format!("killing pid {} failed", pid.pid))); + } else { + if matches.get_flag("echo") { + println!("{} killed (pid {})", pid.cmdline.split(" ").next().unwrap_or(""), pid.pid); + } + } + } if matches.get_flag("count") { println!("{}", pids.len()); } @@ -303,15 +313,6 @@ fn parse_pids(pids: &[String]) -> UResult> { .collect() } -fn kill(sig: Option, pids: &[i32]) { - for &pid in pids { - if let Err(e) = signal::kill(Pid::from_raw(pid), sig) { - show!(Error::from_raw_os_error(e as i32) - .map_err_context(|| format!("sending signal to {pid} failed"))); - } - } -} - #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) From 6bacd589213eae3c62b99114b6c97bbb926f2daa Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 16:37:54 +0800 Subject: [PATCH 04/21] pkill: change `&mut self` to `&self` for `status`, `stat`, `start_time`'s args --- src/uu/pkill/src/process.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/uu/pkill/src/process.rs b/src/uu/pkill/src/process.rs index d0c63c70..5fdb27b5 100644 --- a/src/uu/pkill/src/process.rs +++ b/src/uu/pkill/src/process.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::cell::RefCell; use std::hash::Hash; use std::{ collections::HashMap, @@ -181,11 +182,11 @@ pub struct ProcessInformation { inner_stat: String, /// Processed `/proc/self/status` file - cached_status: Option>>, + cached_status: RefCell>>>, /// Processed `/proc/self/stat` file - cached_stat: Option>>, + cached_stat: RefCell>>>, - cached_start_time: Option, + cached_start_time: RefCell>, } impl ProcessInformation { @@ -242,8 +243,8 @@ impl ProcessInformation { } /// Collect information from `/proc//status` file - pub fn status(&mut self) -> Rc> { - if let Some(c) = &self.cached_status { + pub fn status(&self) -> Rc> { + if let Some(c) = &*self.cached_status.borrow() { return Rc::clone(c); } @@ -255,28 +256,28 @@ impl ProcessInformation { .collect::>(); let result = Rc::new(result); - self.cached_status = Some(Rc::clone(&result)); + *self.cached_status.borrow_mut() = Some(Rc::clone(&result)); Rc::clone(&result) } /// Collect information from `/proc//stat` file - pub fn stat(&mut self) -> Rc> { - if let Some(c) = &self.cached_stat { + pub fn stat(&self) -> Rc> { + if let Some(c) = &*self.cached_stat.borrow() { return Rc::clone(c); } let result: Vec<_> = stat_split(&self.inner_stat); let result = Rc::new(result); - self.cached_stat = Some(Rc::clone(&result)); + *self.cached_stat.borrow_mut() = Some(Rc::clone(&result)); Rc::clone(&result) } /// Fetch start time from [ProcessInformation::cached_stat] /// /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) - pub fn start_time(&mut self) -> Result { - if let Some(time) = self.cached_start_time { + pub fn start_time(&self) -> Result { + if let Some(time) = *self.cached_start_time.borrow() { return Ok(time); } @@ -289,7 +290,7 @@ impl ProcessInformation { .parse::() .map_err(|_| io::ErrorKind::InvalidData)?; - self.cached_start_time = Some(time); + *self.cached_start_time.borrow_mut() = Some(time); Ok(time) } @@ -301,7 +302,7 @@ impl ProcessInformation { /// # Error /// /// If parsing failed, this function will return [io::ErrorKind::InvalidInput] - pub fn run_state(&mut self) -> Result { + pub fn run_state(&self) -> Result { RunState::try_from(self.stat().get(2).unwrap().as_str()) } From ea31bfc50d7fb9bbf2175a00cb46758d33f5f5e7 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 16:38:38 +0800 Subject: [PATCH 05/21] pkill: implement `-H` --- src/uu/pkill/src/pkill.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 012d3540..f3244164 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -84,7 +84,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } // Parse signal - let sig = if let Some(signal) = obs_signal { + let sig_num = if let Some(signal) = obs_signal { signal } else if let Some(signal) = matches.get_one::("signal") { parse_signal_value(signal)? @@ -92,13 +92,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 15_usize //SIGTERM }; - let sig_name = signal_name_by_value(sig); + let sig_name = signal_name_by_value(sig_num); // Signal does not support converting from EXIT // Instead, nix::signal::kill expects Option::None to properly handle EXIT let sig: Option = if sig_name.is_some_and(|name| name == "EXIT") { None } else { - let sig = (sig as i32) + let sig = (sig_num as i32) .try_into() .map_err(|e| std::io::Error::from_raw_os_error(e as i32))?; Some(sig) @@ -107,6 +107,16 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Collect pids let pids = { let mut pids = collect_matched_pids(&settings); + if matches.get_flag("require-handler") { + pids = pids + .into_iter() + .filter(|pid| { + let mask = u32::from_str_radix(pid.status() + .get("SigCgt").unwrap(), 16).unwrap(); + mask & (1 << sig_num) != 0 + }) + .collect(); + } if pids.is_empty() { uucore::error::set_exit_code(1); pids @@ -116,7 +126,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; // Send signal - // TODO: Implement -H -q + // TODO: Implement -q for pid in &pids { if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { show!(Error::from_raw_os_error(e as i32) From 175d85ebf3de567549b90d36e2215398c05f48c8 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 16:45:36 +0800 Subject: [PATCH 06/21] pkill: format code --- src/uu/pkill/src/pkill.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index f3244164..b4a5c9dc 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -111,8 +111,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { pids = pids .into_iter() .filter(|pid| { - let mask = u32::from_str_radix(pid.status() - .get("SigCgt").unwrap(), 16).unwrap(); + let mask = + u32::from_str_radix(pid.status().get("SigCgt").unwrap(), 16).unwrap(); mask & (1 << sig_num) != 0 }) .collect(); @@ -133,7 +133,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map_err_context(|| format!("killing pid {} failed", pid.pid))); } else { if matches.get_flag("echo") { - println!("{} killed (pid {})", pid.cmdline.split(" ").next().unwrap_or(""), pid.pid); + println!( + "{} killed (pid {})", + pid.cmdline.split(" ").next().unwrap_or(""), + pid.pid + ); } } } From 43660b8e7706985f27ddc6236eb4381e9be7a21e Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 20:11:38 +0800 Subject: [PATCH 07/21] pkill: remove .idea dir --- .idea/.gitignore | 5 ----- .idea/modules.xml | 8 -------- .idea/procps.iml | 23 ----------------------- .idea/vcs.xml | 6 ------ 4 files changed, 42 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/modules.xml delete mode 100644 .idea/procps.iml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 10b731c5..00000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8b5ca44a..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/procps.iml b/.idea/procps.iml deleted file mode 100644 index 792021de..00000000 --- a/.idea/procps.iml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1ddf..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 0625ad9150ec5511fc45611ea4eac28ec844418e Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 20:17:49 +0800 Subject: [PATCH 08/21] pkill: beautify code --- src/uu/pkill/src/pkill.rs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index b4a5c9dc..c01ca15a 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -19,7 +19,7 @@ use uucore::show; use uucore::{ error::{UResult, USimpleError}, format_usage, help_about, help_usage, - signals::{signal_by_name_or_value, signal_name_by_value, ALL_SIGNALS}, + signals::{signal_by_name_or_value, signal_name_by_value}, }; const ABOUT: &str = help_about!("pkill.md"); @@ -183,7 +183,7 @@ fn collect_matched_pids(settings: &Settings) -> Vec { let filtered: Vec = { let mut tmp_vec = Vec::new(); - for mut pid in walk_process().collect::>() { + for pid in walk_process().collect::>() { let run_state_matched = match (&settings.runstates, (pid).run_state()) { (Some(arg_run_states), Ok(pid_state)) => { arg_run_states.contains(&pid_state.to_string()) @@ -317,16 +317,6 @@ fn parse_signal_value(signal_name: &str) -> UResult { } } -fn parse_pids(pids: &[String]) -> UResult> { - pids.iter() - .map(|x| { - x.parse::().map_err(|e| { - USimpleError::new(1, format!("failed to parse argument {}: {}", x.quote(), e)) - }) - }) - .collect() -} - #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) From 0649a4c2e1512bc3cbb4af437e7afa1673785fb4 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 26 Dec 2024 20:26:51 +0800 Subject: [PATCH 09/21] pkill: beautify code --- src/uu/pkill/src/pkill.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index c01ca15a..814a3389 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -108,14 +108,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let pids = { let mut pids = collect_matched_pids(&settings); if matches.get_flag("require-handler") { - pids = pids - .into_iter() - .filter(|pid| { - let mask = - u32::from_str_radix(pid.status().get("SigCgt").unwrap(), 16).unwrap(); - mask & (1 << sig_num) != 0 - }) - .collect(); + pids.retain(|pid| { + let mask = u32::from_str_radix(pid.status().get("SigCgt").unwrap(), 16).unwrap(); + mask & (1 << sig_num) != 0 + }); } if pids.is_empty() { uucore::error::set_exit_code(1); @@ -131,14 +127,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { show!(Error::from_raw_os_error(e as i32) .map_err_context(|| format!("killing pid {} failed", pid.pid))); - } else { - if matches.get_flag("echo") { - println!( - "{} killed (pid {})", - pid.cmdline.split(" ").next().unwrap_or(""), - pid.pid - ); - } + } else if matches.get_flag("echo") { + println!( + "{} killed (pid {})", + pid.cmdline.split(" ").next().unwrap_or(""), + pid.pid + ); } } if matches.get_flag("count") { From 065a456b445491af9c99eaace90f0b1e67d66da7 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 31 Dec 2024 16:11:25 +0800 Subject: [PATCH 10/21] pkill: fix Cargo.toml --- src/uu/pkill/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/pkill/Cargo.toml b/src/uu/pkill/Cargo.toml index dfd79ab4..20df061a 100644 --- a/src/uu/pkill/Cargo.toml +++ b/src/uu/pkill/Cargo.toml @@ -23,5 +23,5 @@ nix = { workspace = true, features = ["signal"] } path = "src/pkill.rs" [[bin]] -name = "pgrep" +name = "pkill" path = "src/main.rs" From 1a21770fb4f8716563bb9c66f35d9c74cef7a6ed Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 2 Jan 2025 00:23:47 +0800 Subject: [PATCH 11/21] pkill: remove duplicated process.rs --- Cargo.lock | 1 + src/uu/pkill/Cargo.toml | 2 + src/uu/pkill/src/pkill.rs | 10 +- src/uu/pkill/src/process.rs | 469 ------------------------------------ 4 files changed, 7 insertions(+), 475 deletions(-) delete mode 100644 src/uu/pkill/src/process.rs diff --git a/Cargo.lock b/Cargo.lock index 42e5bb59..3cacac5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1064,6 +1064,7 @@ dependencies = [ "clap", "nix", "regex", + "uu_pgrep", "uucore", "walkdir", ] diff --git a/src/uu/pkill/Cargo.toml b/src/uu/pkill/Cargo.toml index 20df061a..a7c042cb 100644 --- a/src/uu/pkill/Cargo.toml +++ b/src/uu/pkill/Cargo.toml @@ -19,6 +19,8 @@ walkdir = { workspace = true } regex = { workspace = true } nix = { workspace = true, features = ["signal"] } +uu_pgrep = { path = "../pgrep" } + [lib] path = "src/pkill.rs" diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 814a3389..12409106 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -4,12 +4,10 @@ // file that was distributed with this source code. // Pid utils -pub mod process; - use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; -use process::{walk_process, ProcessInformation, Teletype}; +use uu_pgrep::process::{walk_process, ProcessInformation, Teletype}; use regex::Regex; use std::io::Error; use std::{collections::HashSet, sync::OnceLock}; @@ -109,7 +107,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut pids = collect_matched_pids(&settings); if matches.get_flag("require-handler") { pids.retain(|pid| { - let mask = u32::from_str_radix(pid.status().get("SigCgt").unwrap(), 16).unwrap(); + let mask = u32::from_str_radix(pid.clone().status().get("SigCgt").unwrap(), 16).unwrap(); mask & (1 << sig_num) != 0 }); } @@ -177,8 +175,8 @@ fn collect_matched_pids(settings: &Settings) -> Vec { let filtered: Vec = { let mut tmp_vec = Vec::new(); - for pid in walk_process().collect::>() { - let run_state_matched = match (&settings.runstates, (pid).run_state()) { + for mut pid in walk_process().collect::>() { + let run_state_matched = match (&settings.runstates, pid.run_state()) { (Some(arg_run_states), Ok(pid_state)) => { arg_run_states.contains(&pid_state.to_string()) } diff --git a/src/uu/pkill/src/process.rs b/src/uu/pkill/src/process.rs deleted file mode 100644 index 5fdb27b5..00000000 --- a/src/uu/pkill/src/process.rs +++ /dev/null @@ -1,469 +0,0 @@ -// This file is part of the uutils procps package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. - -use std::cell::RefCell; -use std::hash::Hash; -use std::{ - collections::HashMap, - fmt::{self, Display, Formatter}, - fs, io, - path::PathBuf, - rc::Rc, -}; -use walkdir::{DirEntry, WalkDir}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Teletype { - Tty(u64), - TtyS(u64), - Pts(u64), - Unknown, -} - -impl Display for Teletype { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Tty(id) => write!(f, "/dev/pts/{}", id), - Self::TtyS(id) => write!(f, "/dev/tty{}", id), - Self::Pts(id) => write!(f, "/dev/ttyS{}", id), - Self::Unknown => write!(f, "?"), - } - } -} - -impl TryFrom for Teletype { - type Error = (); - - fn try_from(value: String) -> Result { - if value == "?" { - return Ok(Self::Unknown); - } - - Self::try_from(value.as_str()) - } -} - -impl TryFrom<&str> for Teletype { - type Error = (); - - fn try_from(value: &str) -> Result { - Self::try_from(PathBuf::from(value)) - } -} - -impl TryFrom for Teletype { - type Error = (); - - fn try_from(value: PathBuf) -> Result { - // Three case: /dev/pts/* , /dev/ttyS**, /dev/tty** - - let mut iter = value.iter(); - // Case 1 - - // Considering this format: **/**/pts/ - if let (Some(_), Some(num)) = (iter.find(|it| *it == "pts"), iter.next()) { - return num - .to_str() - .ok_or(())? - .parse::() - .map_err(|_| ()) - .map(Teletype::Pts); - }; - - // Considering this format: **/**/ttyS** then **/**/tty** - let path = value.to_str().ok_or(())?; - - let f = |prefix: &str| { - value - .iter() - .last()? - .to_str()? - .strip_prefix(prefix)? - .parse::() - .ok() - }; - - if path.contains("ttyS") { - // Case 2 - f("ttyS").ok_or(()).map(Teletype::TtyS) - } else if path.contains("tty") { - // Case 3 - f("tty").ok_or(()).map(Teletype::Tty) - } else { - Err(()) - } - } -} - -/// State or process -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RunState { - ///`R`, running - Running, - ///`S`, sleeping - Sleeping, - ///`D`, sleeping in an uninterruptible wait - UninterruptibleWait, - ///`Z`, zombie - Zombie, - ///`T`, traced or stopped - Stopped, -} - -impl Display for RunState { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - Self::Running => write!(f, "R"), - Self::Sleeping => write!(f, "S"), - Self::UninterruptibleWait => write!(f, "D"), - Self::Zombie => write!(f, "Z"), - Self::Stopped => write!(f, "T"), - } - } -} - -impl TryFrom for RunState { - type Error = io::Error; - - fn try_from(value: char) -> Result { - match value { - 'R' => Ok(Self::Running), - 'S' => Ok(Self::Sleeping), - 'D' => Ok(Self::UninterruptibleWait), - 'Z' => Ok(Self::Zombie), - 'T' => Ok(Self::Stopped), - _ => Err(io::ErrorKind::InvalidInput.into()), - } - } -} - -impl TryFrom<&str> for RunState { - type Error = io::Error; - - fn try_from(value: &str) -> Result { - if value.len() != 1 { - return Err(io::ErrorKind::InvalidInput.into()); - } - - Self::try_from( - value - .chars() - .nth(0) - .ok_or::(io::ErrorKind::InvalidInput.into())?, - ) - } -} - -impl TryFrom for RunState { - type Error = io::Error; - - fn try_from(value: String) -> Result { - Self::try_from(value.as_str()) - } -} - -impl TryFrom<&String> for RunState { - type Error = io::Error; - - fn try_from(value: &String) -> Result { - Self::try_from(value.as_str()) - } -} - -/// Process ID and its information -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub struct ProcessInformation { - pub pid: usize, - pub cmdline: String, - - inner_status: String, - inner_stat: String, - - /// Processed `/proc/self/status` file - cached_status: RefCell>>>, - /// Processed `/proc/self/stat` file - cached_stat: RefCell>>>, - - cached_start_time: RefCell>, -} - -impl ProcessInformation { - /// Try new with pid path such as `/proc/self` - /// - /// # Error - /// - /// If the files in path cannot be parsed into [ProcessInformation], - /// it almost caused by wrong filesystem structure. - /// - /// - [The /proc Filesystem](https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories) - pub fn try_new(value: PathBuf) -> Result { - let dir_append = |mut path: PathBuf, str: String| { - path.push(str); - path - }; - - let value = if value.is_symlink() { - fs::read_link(value)? - } else { - value - }; - - let pid = { - value - .iter() - .last() - .ok_or(io::ErrorKind::Other)? - .to_str() - .ok_or(io::ErrorKind::InvalidData)? - .parse::() - .map_err(|_| io::ErrorKind::InvalidData)? - }; - let cmdline = fs::read_to_string(dir_append(value.clone(), "cmdline".into()))? - .replace('\0', " ") - .trim_end() - .into(); - - Ok(Self { - pid, - cmdline, - inner_status: fs::read_to_string(dir_append(value.clone(), "status".into()))?, - inner_stat: fs::read_to_string(dir_append(value, "stat".into()))?, - ..Default::default() - }) - } - - pub fn proc_status(&self) -> &str { - &self.inner_status - } - - pub fn proc_stat(&self) -> &str { - &self.inner_stat - } - - /// Collect information from `/proc//status` file - pub fn status(&self) -> Rc> { - if let Some(c) = &*self.cached_status.borrow() { - return Rc::clone(c); - } - - let result = self - .inner_status - .lines() - .filter_map(|it| it.split_once(':')) - .map(|it| (it.0.to_string(), it.1.trim_start().to_string())) - .collect::>(); - - let result = Rc::new(result); - *self.cached_status.borrow_mut() = Some(Rc::clone(&result)); - Rc::clone(&result) - } - - /// Collect information from `/proc//stat` file - pub fn stat(&self) -> Rc> { - if let Some(c) = &*self.cached_stat.borrow() { - return Rc::clone(c); - } - - let result: Vec<_> = stat_split(&self.inner_stat); - - let result = Rc::new(result); - *self.cached_stat.borrow_mut() = Some(Rc::clone(&result)); - Rc::clone(&result) - } - - /// Fetch start time from [ProcessInformation::cached_stat] - /// - /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) - pub fn start_time(&self) -> Result { - if let Some(time) = *self.cached_start_time.borrow() { - return Ok(time); - } - - // Kernel doc: https://docs.kernel.org/filesystems/proc.html#process-specific-subdirectories - // Table 1-4 - let time = self - .stat() - .get(21) - .ok_or(io::ErrorKind::InvalidData)? - .parse::() - .map_err(|_| io::ErrorKind::InvalidData)?; - - *self.cached_start_time.borrow_mut() = Some(time); - - Ok(time) - } - - /// Fetch run state from [ProcessInformation::cached_stat] - /// - /// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10) - /// - /// # Error - /// - /// If parsing failed, this function will return [io::ErrorKind::InvalidInput] - pub fn run_state(&self) -> Result { - RunState::try_from(self.stat().get(2).unwrap().as_str()) - } - - /// This function will scan the `/proc//fd` directory - /// - /// If the process does not belong to any terminal and mismatched permission, - /// the result will contain [TerminalType::Unknown]. - /// - /// Otherwise [TerminalType::Unknown] does not appear in the result. - pub fn tty(&self) -> Teletype { - let path = PathBuf::from(format!("/proc/{}/fd", self.pid)); - - let Ok(result) = fs::read_dir(path) else { - return Teletype::Unknown; - }; - - for dir in result.flatten().filter(|it| it.path().is_symlink()) { - if let Ok(path) = fs::read_link(dir.path()) { - if let Ok(tty) = Teletype::try_from(path) { - return tty; - } - } - } - - Teletype::Unknown - } -} -impl TryFrom for ProcessInformation { - type Error = io::Error; - - fn try_from(value: DirEntry) -> Result { - let value = value.into_path(); - - Self::try_new(value) - } -} - -impl Hash for ProcessInformation { - fn hash(&self, state: &mut H) { - // Make it faster. - self.pid.hash(state); - self.inner_status.hash(state); - self.inner_stat.hash(state); - } -} - -/// Parsing `/proc/self/stat` file. -/// -/// In some case, the first pair (and the only one pair) will contains whitespace, -/// so if we want to parse it, we have to write new algorithm. -/// -/// TODO: If possible, test and use regex to replace this algorithm. -fn stat_split(stat: &str) -> Vec { - let stat = String::from(stat); - - let mut buf = String::with_capacity(stat.len()); - - let l = stat.find('('); - let r = stat.find(')'); - let content = if let (Some(l), Some(r)) = (l, r) { - let replaced = stat[(l + 1)..r].replace(' ', "$$"); - - buf.push_str(&stat[..l]); - buf.push_str(&replaced); - buf.push_str(&stat[(r + 1)..stat.len()]); - - &buf - } else { - &stat - }; - - content - .split_whitespace() - .map(|it| it.replace("$$", " ")) - .collect() -} - -/// Iterating pid in current system -pub fn walk_process() -> impl Iterator { - WalkDir::new("/proc/") - .max_depth(1) - .follow_links(false) - .into_iter() - .flatten() - .filter(|it| it.path().is_dir()) - .flat_map(ProcessInformation::try_from) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{collections::HashSet, str::FromStr}; - - #[test] - fn test_run_state_conversion() { - assert_eq!(RunState::try_from("R").unwrap(), RunState::Running); - assert_eq!(RunState::try_from("S").unwrap(), RunState::Sleeping); - assert_eq!( - RunState::try_from("D").unwrap(), - RunState::UninterruptibleWait - ); - assert_eq!(RunState::try_from("T").unwrap(), RunState::Stopped); - assert_eq!(RunState::try_from("Z").unwrap(), RunState::Zombie); - - assert!(RunState::try_from("G").is_err()); - assert!(RunState::try_from("Rg").is_err()); - } - - fn current_pid() -> usize { - // Direct read link of /proc/self. - // It's result must be current programs pid. - fs::read_link("/proc/self") - .unwrap() - .to_str() - .unwrap() - .parse::() - .unwrap() - } - - #[test] - fn test_walk_pid() { - let current_pid = current_pid(); - - let find = walk_process().find(|it| it.pid == current_pid); - - assert!(find.is_some()); - } - - #[test] - fn test_pid_entry() { - let current_pid = current_pid(); - - let pid_entry = ProcessInformation::try_new( - PathBuf::from_str(&format!("/proc/{}", current_pid)).unwrap(), - ) - .unwrap(); - - let mut result = WalkDir::new(format!("/proc/{}/fd", current_pid)) - .into_iter() - .flatten() - .map(DirEntry::into_path) - .flat_map(|it| it.read_link()) - .flat_map(Teletype::try_from) - .collect::>(); - - if result.is_empty() { - result.insert(Teletype::Unknown); - } - - assert!(result.contains(&pid_entry.tty())); - } - - #[test] - fn test_stat_split() { - let case = "32 (idle_inject/3) S 2 0 0 0 -1 69238848 0 0 0 0 0 0 0 0 -51 0 1 0 34 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 3 50 1 0 0 0 0 0 0 0 0 0 0 0"; - assert!(stat_split(case)[1] == "idle_inject/3"); - - let case = "3508 (sh) S 3478 3478 3478 0 -1 4194304 67 0 0 0 0 0 0 0 20 0 1 0 11911 2961408 238 18446744073709551615 94340156948480 94340157028757 140736274114368 0 0 0 0 4096 65538 1 0 0 17 8 0 0 0 0 0 94340157054704 94340157059616 94340163108864 140736274122780 140736274122976 140736274122976 140736274124784 0"; - assert!(stat_split(case)[1] == "sh"); - - let case = "47246 (kworker /10:1-events) I 2 0 0 0 -1 69238880 0 0 0 0 17 29 0 0 20 0 1 0 1396260 0 0 18446744073709551615 0 0 0 0 0 0 0 2147483647 0 0 0 0 17 10 0 0 0 0 0 0 0 0 0 0 0 0 0"; - assert!(stat_split(case)[1] == "kworker /10:1-events"); - } -} From dd39789f905e697a79cf5952b7feb8550dc77c22 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 2 Jan 2025 00:41:25 +0800 Subject: [PATCH 12/21] pkill: add unit tests --- tests/by-util/test_pkill.rs | 50 +++++++++++++++++++++++++++++++++++++ tests/tests.rs | 4 +++ 2 files changed, 54 insertions(+) create mode 100644 tests/by-util/test_pkill.rs diff --git a/tests/by-util/test_pkill.rs b/tests/by-util/test_pkill.rs new file mode 100644 index 00000000..1e77dcb3 --- /dev/null +++ b/tests/by-util/test_pkill.rs @@ -0,0 +1,50 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[cfg(target_os = "linux")] +#[test] +fn test_no_args() { + new_ucmd!() + .fails() + .code_is(2) + .no_stdout() + .stderr_contains("no matching criteria specified"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_non_matching_pattern() { + new_ucmd!() + .arg("THIS_PATTERN_DOES_NOT_MATCH") + .fails() + .code_is(1) + .no_output(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_too_many_patterns() { + new_ucmd!() + .arg("sh") + .arg("sh") + .fails() + .code_is(2) + .no_stdout() + .stderr_contains("only one pattern can be provided"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_help() { + new_ucmd!().arg("--help").succeeds(); +} \ No newline at end of file diff --git a/tests/tests.rs b/tests/tests.rs index 17b20762..f9759268 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -52,3 +52,7 @@ mod test_top; #[cfg(feature = "snice")] #[path = "by-util/test_snice.rs"] mod test_snice; + +#[cfg(feature = "pkill")] +#[path = "by-util/test_pkill.rs"] +mod test_pkill; \ No newline at end of file From 21e10e43dc34f39a8a73cc4a889149c6208ccb85 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Thu, 2 Jan 2025 00:55:07 +0800 Subject: [PATCH 13/21] pkill: beautify code --- src/uu/pkill/src/pkill.rs | 5 +++-- tests/by-util/test_pkill.rs | 2 +- tests/tests.rs | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 12409106..c826e13d 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -7,10 +7,10 @@ use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; -use uu_pgrep::process::{walk_process, ProcessInformation, Teletype}; use regex::Regex; use std::io::Error; use std::{collections::HashSet, sync::OnceLock}; +use uu_pgrep::process::{walk_process, ProcessInformation, Teletype}; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::show; @@ -107,7 +107,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut pids = collect_matched_pids(&settings); if matches.get_flag("require-handler") { pids.retain(|pid| { - let mask = u32::from_str_radix(pid.clone().status().get("SigCgt").unwrap(), 16).unwrap(); + let mask = + u32::from_str_radix(pid.clone().status().get("SigCgt").unwrap(), 16).unwrap(); mask & (1 << sig_num) != 0 }); } diff --git a/tests/by-util/test_pkill.rs b/tests/by-util/test_pkill.rs index 1e77dcb3..157735bf 100644 --- a/tests/by-util/test_pkill.rs +++ b/tests/by-util/test_pkill.rs @@ -47,4 +47,4 @@ fn test_invalid_arg() { #[test] fn test_help() { new_ucmd!().arg("--help").succeeds(); -} \ No newline at end of file +} diff --git a/tests/tests.rs b/tests/tests.rs index f9759268..77ce68f1 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -55,4 +55,4 @@ mod test_snice; #[cfg(feature = "pkill")] #[path = "by-util/test_pkill.rs"] -mod test_pkill; \ No newline at end of file +mod test_pkill; From cb1f9ea768c9ca2e9653cf4292755ce243ef0b4d Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 15:24:07 +0800 Subject: [PATCH 14/21] pkill: fix CI failures for windows --- src/uu/pkill/src/pkill.rs | 53 ++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index c826e13d..1f333f60 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -5,6 +5,9 @@ // Pid utils use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; +#[cfg(target_os = "linux")] +use nix::sys::signal::{self, Signal}; +#[cfg(target_os = "macos")] use nix::sys::signal::{self, Signal}; use nix::unistd::Pid; use regex::Regex; @@ -122,18 +125,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Send signal // TODO: Implement -q - for pid in &pids { - if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { - show!(Error::from_raw_os_error(e as i32) - .map_err_context(|| format!("killing pid {} failed", pid.pid))); - } else if matches.get_flag("echo") { - println!( - "{} killed (pid {})", - pid.cmdline.split(" ").next().unwrap_or(""), - pid.pid - ); - } - } + let echo = matches.get_flag("echo"); + kill(&pids, sig, echo); + if matches.get_flag("count") { println!("{}", pids.len()); } @@ -310,6 +304,41 @@ fn parse_signal_value(signal_name: &str) -> UResult { } } +#[cfg(target_os = "linux")] +fn kill(pids: &Vec, sig: Option, echo: bool) { + for pid in pids { + if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { + show!(Error::from_raw_os_error(e as i32) + .map_err_context(|| format!("killing pid {} failed", pid.pid))); + } else if echo { + println!( + "{} killed (pid {})", + pid.cmdline.split(" ").next().unwrap_or(""), + pid.pid + ); + } + } +} + +#[cfg(target_os = "macos")] +fn kill(pids: &Vec, sig: Option, echo: bool) { + for pid in pids { + if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { + show!(Error::from_raw_os_error(e as i32) + .map_err_context(|| format!("killing pid {} failed", pid.pid))); + } else if echo { + println!( + "{} killed (pid {})", + pid.cmdline.split(" ").next().unwrap_or(""), + pid.pid + ); + } + } +} + +#[cfg(target_os = "windows")] +fn kill(pids: &Vec, sig: Option, echo: bool) {} + #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) From 2f943c4df7ebd4ba1f8753773301278abb981d20 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 15:50:47 +0800 Subject: [PATCH 15/21] pkill: fix CI failures for windows --- src/uu/pkill/src/pkill.rs | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 1f333f60..7354fd66 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -5,10 +5,9 @@ // Pid utils use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; -#[cfg(target_os = "linux")] -use nix::sys::signal::{self, Signal}; -#[cfg(target_os = "macos")] +#[cfg(unix)] use nix::sys::signal::{self, Signal}; +#[cfg(unix)] use nix::unistd::Pid; use regex::Regex; use std::io::Error; @@ -126,6 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Send signal // TODO: Implement -q let echo = matches.get_flag("echo"); + #[cfg(unix)] kill(&pids, sig, echo); if matches.get_flag("count") { @@ -304,7 +304,7 @@ fn parse_signal_value(signal_name: &str) -> UResult { } } -#[cfg(target_os = "linux")] +#[cfg(unix)] fn kill(pids: &Vec, sig: Option, echo: bool) { for pid in pids { if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { @@ -320,25 +320,6 @@ fn kill(pids: &Vec, sig: Option, echo: bool) { } } -#[cfg(target_os = "macos")] -fn kill(pids: &Vec, sig: Option, echo: bool) { - for pid in pids { - if let Err(e) = signal::kill(Pid::from_raw(pid.pid as i32), sig) { - show!(Error::from_raw_os_error(e as i32) - .map_err_context(|| format!("killing pid {} failed", pid.pid))); - } else if echo { - println!( - "{} killed (pid {})", - pid.cmdline.split(" ").next().unwrap_or(""), - pid.pid - ); - } - } -} - -#[cfg(target_os = "windows")] -fn kill(pids: &Vec, sig: Option, echo: bool) {} - #[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) From 1d1e45b674f16f48de7fb88e5917dc2ef1485c02 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 16:07:27 +0800 Subject: [PATCH 16/21] pkill: fix CI failures for windows --- src/uu/pkill/src/pkill.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 7354fd66..4a67ede0 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -16,10 +16,11 @@ use uu_pgrep::process::{walk_process, ProcessInformation, Teletype}; use uucore::display::Quotable; use uucore::error::FromIo; use uucore::show; +#[cfg(unix)] +use uucore::signals::{signal_by_name_or_value, signal_name_by_value}; use uucore::{ error::{UResult, USimpleError}, format_usage, help_about, help_usage, - signals::{signal_by_name_or_value, signal_name_by_value}, }; const ABOUT: &str = help_about!("pkill.md"); @@ -95,6 +96,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let sig_name = signal_name_by_value(sig_num); // Signal does not support converting from EXIT // Instead, nix::signal::kill expects Option::None to properly handle EXIT + #[cfg(unix)] let sig: Option = if sig_name.is_some_and(|name| name == "EXIT") { None } else { From 467c584bf465b72fc38bb2a152e1a05de52c251a Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 16:22:22 +0800 Subject: [PATCH 17/21] pkill: fix CI failures for windows --- src/uu/pkill/src/pkill.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 4a67ede0..44f80d46 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -43,6 +43,7 @@ struct Settings { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut args = args.collect_ignore(); + #[cfg(unix)] let obs_signal = handle_obsolete(&mut args); let matches = uu_app().try_get_matches_from(&args)?; @@ -85,6 +86,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } // Parse signal + #[cfg(unix)] let sig_num = if let Some(signal) = obs_signal { signal } else if let Some(signal) = matches.get_one::("signal") { @@ -93,6 +95,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { 15_usize //SIGTERM }; + #[cfg(unix)] let sig_name = signal_name_by_value(sig_num); // Signal does not support converting from EXIT // Instead, nix::signal::kill expects Option::None to properly handle EXIT @@ -277,6 +280,7 @@ fn process_flag_o_n( } } +#[cfg(unix)] fn handle_obsolete(args: &mut Vec) -> Option { // Sanity check if args.len() > 2 { @@ -295,6 +299,7 @@ fn handle_obsolete(args: &mut Vec) -> Option { None } +#[cfg(unix)] fn parse_signal_value(signal_name: &str) -> UResult { let optional_signal_value = signal_by_name_or_value(signal_name); match optional_signal_value { From 794c2eee53dbafa7de34dcd145dc7c6f181e40f1 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 16:35:00 +0800 Subject: [PATCH 18/21] pkill: fix CI failures for windows --- src/uu/pkill/src/pkill.rs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index 44f80d46..a4f47823 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -6,18 +6,21 @@ // Pid utils use clap::{arg, crate_version, Arg, ArgAction, ArgGroup, ArgMatches, Command}; #[cfg(unix)] -use nix::sys::signal::{self, Signal}; -#[cfg(unix)] -use nix::unistd::Pid; +use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, +}; use regex::Regex; use std::io::Error; use std::{collections::HashSet, sync::OnceLock}; use uu_pgrep::process::{walk_process, ProcessInformation, Teletype}; -use uucore::display::Quotable; -use uucore::error::FromIo; -use uucore::show; #[cfg(unix)] -use uucore::signals::{signal_by_name_or_value, signal_name_by_value}; +use uucore::{ + display::Quotable, + error::FromIo, + show, + signals::{signal_by_name_or_value, signal_name_by_value}, +}; use uucore::{ error::{UResult, USimpleError}, format_usage, help_about, help_usage, @@ -112,6 +115,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Collect pids let pids = { let mut pids = collect_matched_pids(&settings); + #[cfg(unix)] if matches.get_flag("require-handler") { pids.retain(|pid| { let mask = From 44d208e38054a6569c52abb9d497a310db00b050 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 16:43:42 +0800 Subject: [PATCH 19/21] pkill: fix CI failures for MacOS --- tests/by-util/test_pkill.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/by-util/test_pkill.rs b/tests/by-util/test_pkill.rs index 157735bf..4e709a0d 100644 --- a/tests/by-util/test_pkill.rs +++ b/tests/by-util/test_pkill.rs @@ -5,7 +5,7 @@ use crate::common::util::TestScenario; -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_no_args() { new_ucmd!() @@ -15,7 +15,7 @@ fn test_no_args() { .stderr_contains("no matching criteria specified"); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_non_matching_pattern() { new_ucmd!() @@ -25,7 +25,7 @@ fn test_non_matching_pattern() { .no_output(); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_too_many_patterns() { new_ucmd!() @@ -37,13 +37,13 @@ fn test_too_many_patterns() { .stderr_contains("only one pattern can be provided"); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails().code_is(1); } -#[cfg(target_os = "linux")] +#[cfg(unix)] #[test] fn test_help() { new_ucmd!().arg("--help").succeeds(); From bb2309336f72023dbe537943e07657928b033fe1 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 16:46:46 +0800 Subject: [PATCH 20/21] pkill: fix CI failures for windows --- src/uu/pkill/src/pkill.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/uu/pkill/src/pkill.rs b/src/uu/pkill/src/pkill.rs index a4f47823..9d3fb782 100644 --- a/src/uu/pkill/src/pkill.rs +++ b/src/uu/pkill/src/pkill.rs @@ -11,6 +11,7 @@ use nix::{ unistd::Pid, }; use regex::Regex; +#[cfg(unix)] use std::io::Error; use std::{collections::HashSet, sync::OnceLock}; use uu_pgrep::process::{walk_process, ProcessInformation, Teletype}; @@ -45,7 +46,10 @@ struct Settings { #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + #[cfg(unix)] let mut args = args.collect_ignore(); + #[cfg(target_os = "windows")] + let args = args.collect_ignore(); #[cfg(unix)] let obs_signal = handle_obsolete(&mut args); @@ -133,6 +137,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Send signal // TODO: Implement -q + #[cfg(unix)] let echo = matches.get_flag("echo"); #[cfg(unix)] kill(&pids, sig, echo); From 61e91cbdf423e00896f5a9ab8f8276f7ef5aa835 Mon Sep 17 00:00:00 2001 From: MCredbear <2363764471@qq.com> Date: Tue, 7 Jan 2025 16:51:52 +0800 Subject: [PATCH 21/21] pkill: fix CI failures for windows --- tests/by-util/test_pkill.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/by-util/test_pkill.rs b/tests/by-util/test_pkill.rs index 4e709a0d..84935028 100644 --- a/tests/by-util/test_pkill.rs +++ b/tests/by-util/test_pkill.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(unix)] use crate::common::util::TestScenario; #[cfg(unix)]