diff --git a/Cargo.lock b/Cargo.lock index 294cd987..7aa8e014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1068,6 +1068,7 @@ dependencies = [ "uu_pmap", "uu_ps", "uu_pwdx", + "uu_skill", "uu_slabtop", "uu_snice", "uu_sysctl", @@ -1740,6 +1741,16 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_skill" +version = "0.0.1" +dependencies = [ + "clap", + "nix", + "uu_snice", + "uucore", +] + [[package]] name = "uu_slabtop" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index b1621dfa..cdec2b3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ feat_common_core = [ "pmap", "ps", "pwdx", + "skill", "slabtop", "snice", "sysctl", @@ -98,6 +99,7 @@ pkill = { optional = true, version = "0.0.1", package = "uu_pkill", path = "src/ pmap = { optional = true, version = "0.0.1", package = "uu_pmap", path = "src/uu/pmap" } ps = { optional = true, version = "0.0.1", package = "uu_ps", path = "src/uu/ps" } pwdx = { optional = true, version = "0.0.1", package = "uu_pwdx", path = "src/uu/pwdx" } +skill = { optional = true, version = "0.0.1", package = "uu_skill", path = "src/uu/skill" } slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = "src/uu/slabtop" } snice = { optional = true, version = "0.0.1", package = "uu_snice", path = "src/uu/snice" } sysctl = { optional = true, version = "0.0.1", package = "uu_sysctl", path = "src/uu/sysctl" } diff --git a/src/uu/skill/Cargo.toml b/src/uu/skill/Cargo.toml new file mode 100644 index 00000000..dedc7bf8 --- /dev/null +++ b/src/uu/skill/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "uu_skill" +description = "skill - (uutils) send a signal or report process status" +repository = "https://github.com/uutils/procps/tree/main/src/uu/skill" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +uucore = { workspace = true, features = ["signals"] } +clap = { workspace = true } +nix = { workspace = true } + +uu_snice = { path = "../snice" } + + +[lib] +path = "src/skill.rs" + +[[bin]] +name = "skill" +path = "src/main.rs" diff --git a/src/uu/skill/skill.md b/src/uu/skill/skill.md new file mode 100644 index 00000000..8fdb454c --- /dev/null +++ b/src/uu/skill/skill.md @@ -0,0 +1,7 @@ +# skill + +``` +skill [signal] [options] +``` + +Report processes matching an expression and send a signal to them. \ No newline at end of file diff --git a/src/uu/skill/src/main.rs b/src/uu/skill/src/main.rs new file mode 100644 index 00000000..2e827bc4 --- /dev/null +++ b/src/uu/skill/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_skill); diff --git a/src/uu/skill/src/skill.rs b/src/uu/skill/src/skill.rs new file mode 100644 index 00000000..e1c47c6c --- /dev/null +++ b/src/uu/skill/src/skill.rs @@ -0,0 +1,106 @@ +// 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 clap::{arg, crate_version, value_parser, Arg, Command}; +#[cfg(unix)] +use nix::{sys::signal, sys::signal::Signal, unistd::Pid}; +use uu_snice::{ + collect_pids, construct_verbose_result, print_signals, process_matcher, ActionResult, +}; +use uucore::error::USimpleError; +#[cfg(unix)] +use uucore::signals::signal_by_name_or_value; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("skill.md"); +const USAGE: &str = help_usage!("skill.md"); + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + let settings = process_matcher::Settings::try_new(&matches)?; + + // Case0: Print SIGNALS + if let Some(display) = &settings.display { + print_signals(display); + return Ok(()); + } + + // Case1: Send signal + if let Some(targets) = settings.expressions { + let pids = collect_pids(&targets); + + #[cfg(unix)] + let signal_str = matches.get_one::("signal").cloned(); + + #[cfg(unix)] + let signal = if let Some(sig) = signal_str { + (signal_by_name_or_value(sig.strip_prefix('-').unwrap()).unwrap() as i32).try_into()? + } else { + Signal::SIGTERM + }; + + #[cfg(unix)] + let results = perform_action(&pids, &signal); + #[cfg(not(unix))] + let results: Vec> = Vec::new(); + + if results.iter().all(|it| it.is_none()) || results.is_empty() { + return Err(USimpleError::new(1, "no process selection criteria")); + } + + if settings.verbose { + let output = construct_verbose_result(&pids, &results).trim().to_owned(); + println!("{output}"); + } + } + + Ok(()) +} + +#[cfg(unix)] +fn perform_action(pids: &[u32], signal: &Signal) -> Vec> { + pids.iter() + .map(|pid| { + { + Some(match signal::kill(Pid::from_raw(*pid as i32), *signal) { + Ok(_) => ActionResult::Success, + + Err(_) => ActionResult::PermissionDenied, + }) + } + }) + .collect() +} + +#[allow(clippy::cognitive_complexity)] +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg_required_else_help(true) + .arg(Arg::new("signal")) + .args([ + // arg!(-f --fast "fast mode (not implemented)"), + // arg!(-i --interactive "interactive"), + arg!(-l --list "list all signal names"), + arg!(-L --table "list all signal names in a nice table"), + // arg!(-n --"no-action" "do not actually kill processes; just print what would happen"), + arg!(-v --verbose "explain what is being done"), + // arg!(-w --warnings "enable warnings (not implemented)"), + // Expressions + arg!(-c --command ... "expression is a command name"), + arg!(-p --pid ... "expression is a process id number") + .value_parser(value_parser!(u32)), + arg!(-t --tty ... "expression is a terminal"), + arg!(-u --user ... "expression is a username"), + // 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"]), + ]) +} diff --git a/src/uu/snice/src/action.rs b/src/uu/snice/src/action.rs index 0a25d169..dff7bd34 100644 --- a/src/uu/snice/src/action.rs +++ b/src/uu/snice/src/action.rs @@ -24,7 +24,7 @@ pub(crate) fn users() -> &'static Users { } #[derive(Debug)] -pub(crate) enum SelectedTarget { +pub enum SelectedTarget { Command(String), Pid(u32), Tty(Teletype), @@ -93,7 +93,7 @@ impl SelectedTarget { #[allow(unused)] #[derive(Debug, Clone)] -pub(crate) enum ActionResult { +pub enum ActionResult { PermissionDenied, Success, } diff --git a/src/uu/snice/src/priority.rs b/src/uu/snice/src/priority.rs index d87dd9a6..889ebdb6 100644 --- a/src/uu/snice/src/priority.rs +++ b/src/uu/snice/src/priority.rs @@ -13,7 +13,7 @@ pub enum Error { } #[derive(Debug, PartialEq, Eq)] -pub(crate) enum Priority { +pub enum Priority { // The default priority is +4. (snice +4 ...) Increase(u32), Decrease(u32), diff --git a/src/uu/snice/src/process_matcher.rs b/src/uu/snice/src/process_matcher.rs new file mode 100644 index 00000000..939d326c --- /dev/null +++ b/src/uu/snice/src/process_matcher.rs @@ -0,0 +1,101 @@ +// 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::action::SelectedTarget; +use crate::SignalDisplay; +use clap::{arg, value_parser, Arg, ArgMatches}; +use uu_pgrep::process::Teletype; +use uucore::error::UResult; + +#[derive(Debug)] +pub struct Settings { + pub display: Option, + pub expressions: Option>, + pub verbose: bool, +} + +impl Settings { + pub fn try_new(matches: &ArgMatches) -> UResult { + let display = if matches.get_flag("table") { + Some(SignalDisplay::Table) + } else if matches.get_flag("list") { + Some(SignalDisplay::List) + } else { + None + }; + + Ok(Self { + display, + expressions: Self::targets(matches), + verbose: matches.get_flag("verbose"), + }) + } + + fn targets(matches: &ArgMatches) -> Option> { + let cmd = matches + .get_many::("command") + .unwrap_or_default() + .map(Into::into) + .map(SelectedTarget::Command) + .collect::>(); + + let pid = matches + .get_many::("pid") + .unwrap_or_default() + .map(Clone::clone) + .map(SelectedTarget::Pid) + .collect::>(); + + let tty = matches + .get_many::("tty") + .unwrap_or_default() + .flat_map(|it| Teletype::try_from(it.as_str())) + .map(SelectedTarget::Tty) + .collect::>(); + + let user = matches + .get_many::("user") + .unwrap_or_default() + .map(Into::into) + .map(SelectedTarget::User) + .collect::>(); + + let collected = cmd + .into_iter() + .chain(pid) + .chain(tty) + .chain(user) + .collect::>(); + + if collected.is_empty() { + None + } else { + Some(collected) + } + } +} + +#[allow(clippy::cognitive_complexity)] +pub fn clap_args() -> Vec { + vec![ + // arg!(-f --fast "fast mode (not implemented)"), + // arg!(-i --interactive "interactive"), + arg!(-l --list "list all signal names"), + arg!(-L --table "list all signal names in a nice table"), + arg!(-n --"no-action" "do not actually kill processes; just print what would happen"), + arg!(-v --verbose "explain what is being done"), + // arg!(-w --warnings "enable warnings (not implemented)"), + // Expressions + arg!(-c --command ... "expression is a command name"), + arg!(-p --pid ... "expression is a process id number") + .value_parser(value_parser!(u32)), + arg!(-t --tty ... "expression is a terminal"), + arg!(-u --user ... "expression is a username"), + // 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"]), + ] +} diff --git a/src/uu/snice/src/snice.rs b/src/uu/snice/src/snice.rs index caa7c9ee..5bb0c06e 100644 --- a/src/uu/snice/src/snice.rs +++ b/src/uu/snice/src/snice.rs @@ -5,12 +5,14 @@ use std::{collections::HashSet, path::PathBuf, str::FromStr}; -use action::{perform_action, process_snapshot, users, ActionResult, SelectedTarget}; -use clap::{arg, crate_version, value_parser, Arg, ArgMatches, Command}; +use crate::priority::Priority; +pub use action::ActionResult; +use action::{perform_action, process_snapshot, users, SelectedTarget}; +use clap::{crate_version, Arg, Command}; use prettytable::{format::consts::FORMAT_CLEAN, row, Table}; -use priority::Priority; +use process_matcher::*; use sysinfo::Pid; -use uu_pgrep::process::{ProcessInformation, Teletype}; +use uu_pgrep::process::ProcessInformation; #[cfg(target_family = "unix")] use uucore::signals::ALL_SIGNALS; use uucore::{ @@ -23,9 +25,10 @@ const USAGE: &str = help_usage!("snice.md"); mod action; mod priority; +pub mod process_matcher; #[derive(Debug)] -enum SignalDisplay { +pub enum SignalDisplay { List, Table, } @@ -66,85 +69,13 @@ impl SignalDisplay { } } -#[derive(Debug)] -struct Settings { - display: Option, - expressions: Option>, - priority: Priority, - verbose: bool, -} - -impl Settings { - fn try_new(matches: &ArgMatches) -> UResult { - let priority = matches - .try_get_one::("priority") - .unwrap_or(Some(&String::new())) - .cloned(); - - let expression = match priority { - Some(expr) => { - Priority::try_from(expr).map_err(|err| USimpleError::new(1, err.to_string()))? - } - None => Priority::default(), - }; - - let display = if matches.get_flag("table") { - Some(SignalDisplay::Table) - } else if matches.get_flag("list") { - Some(SignalDisplay::List) - } else { - None - }; - - Ok(Self { - display, - expressions: Self::targets(matches), - priority: expression, - verbose: matches.get_flag("verbose"), - }) - } - - fn targets(matches: &ArgMatches) -> Option> { - let cmd = matches - .get_many::("command") - .unwrap_or_default() - .map(Into::into) - .map(SelectedTarget::Command) - .collect::>(); - - let pid = matches - .get_many::("pid") - .unwrap_or_default() - .map(Clone::clone) - .map(SelectedTarget::Pid) - .collect::>(); - - let tty = matches - .get_many::("tty") - .unwrap_or_default() - .flat_map(|it| Teletype::try_from(it.as_str())) - .map(SelectedTarget::Tty) - .collect::>(); - - let user = matches - .get_many::("user") - .unwrap_or_default() - .map(Into::into) - .map(SelectedTarget::User) - .collect::>(); - - let collected = cmd - .into_iter() - .chain(pid) - .chain(tty) - .chain(user) - .collect::>(); +#[allow(unused)] // unused argument under non-unix targets +pub fn print_signals(display: &SignalDisplay) { + #[cfg(target_family = "unix")] + { + let result = display.display(&ALL_SIGNALS); - if collected.is_empty() { - None - } else { - Some(collected) - } + println!("{result}"); } } @@ -155,28 +86,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = Settings::try_new(&matches)?; // Case0: Print SIGNALS - #[cfg(target_family = "unix")] - { - if let Some(display) = settings.display { - let result = display.display(&ALL_SIGNALS); - - println!("{result}"); - return Ok(()); - } - } - - #[cfg(not(target_family = "unix"))] - { - if let Some(_display) = settings.display { - return Ok(()); - } + if let Some(display) = &settings.display { + print_signals(display); + return Ok(()); } // Case1: Perform priority let take_action = !matches.get_flag("no-action"); if let Some(targets) = settings.expressions { + let priority_str = matches.get_one::("priority").cloned(); + + let priority = match priority_str { + Some(expr) => { + Priority::try_from(expr).map_err(|err| USimpleError::new(1, err.to_string()))? + } + None => Priority::default(), + }; + let pids = collect_pids(&targets); - let results = perform_action(&pids, &settings.priority, take_action); + let results = perform_action(&pids, &priority, take_action); if results.iter().all(|it| it.is_none()) || results.is_empty() { return Err(USimpleError::new(1, "no process selection criteria")); @@ -194,7 +122,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } #[allow(unused)] -fn construct_verbose_result(pids: &[u32], action_results: &[Option]) -> String { +pub fn construct_verbose_result(pids: &[u32], action_results: &[Option]) -> String { let mut table = action_results .iter() .enumerate() @@ -232,7 +160,7 @@ fn construct_verbose_result(pids: &[u32], action_results: &[Option } /// Map and sort `SelectedTarget` to pids. -fn collect_pids(targets: &[SelectedTarget]) -> Vec { +pub fn collect_pids(targets: &[SelectedTarget]) -> Vec { let collected = targets .iter() .flat_map(SelectedTarget::to_pids) @@ -243,7 +171,6 @@ fn collect_pids(targets: &[SelectedTarget]) -> Vec { collected } -#[allow(clippy::cognitive_complexity)] pub fn uu_app() -> Command { Command::new(uucore::util_name()) .version(crate_version!()) @@ -252,22 +179,7 @@ pub fn uu_app() -> Command { .infer_long_args(true) .arg_required_else_help(true) .arg(Arg::new("priority")) - .args([ - // Options - // arg!(-f --fast "fast mode (not implemented)"), - // arg!(-i --interactive "interactive"), - arg!(-l --list "list all signal names"), - arg!(-L --table "list all signal names in a nice table"), - arg!(-n --"no-action" "do not actually kill processes; just print what would happen"), - arg!(-v --verbose "explain what is being done"), - // arg!(-w --warnings "enable warnings (not implemented)"), - // Expressions - arg!(-c --command ... "expression is a command name"), - arg!(-p --pid ... "expression is a process id number") - .value_parser(value_parser!(u32)), - arg!(-t --tty ... "expression is a terminal"), - arg!(-u --user ... "expression is a username"), - ]) + .args(clap_args()) } #[cfg(test)] diff --git a/tests/by-util/test_skill.rs b/tests/by-util/test_skill.rs new file mode 100644 index 00000000..a9e5f167 --- /dev/null +++ b/tests/by-util/test_skill.rs @@ -0,0 +1,13 @@ +// 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 uutests::new_ucmd; +use uutests::util::TestScenario; +use uutests::util_name; + +#[test] +fn test_no_args() { + new_ucmd!().fails().code_is(1); +} diff --git a/tests/tests.rs b/tests/tests.rs index 6078611f..8df70274 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -63,6 +63,10 @@ mod test_top; #[path = "by-util/test_vmstat.rs"] mod test_vmstat; +#[cfg(feature = "skill")] +#[path = "by-util/test_skill.rs"] +mod test_skill; + #[cfg(feature = "snice")] #[path = "by-util/test_snice.rs"] mod test_snice;