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