Merge pull request #466 from Bluemangoo/feature/skill

skill: add basic implementation
This commit is contained in:
Daniel Hofstetter
2025-08-08 16:19:37 +02:00
committed by GitHub
12 changed files with 304 additions and 121 deletions
Generated
+11
View File
@@ -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"
+2
View File
@@ -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" }
+26
View File
@@ -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"
+7
View File
@@ -0,0 +1,7 @@
# skill
```
skill [signal] [options] <expression>
```
Report processes matching an expression and send a signal to them.
+1
View File
@@ -0,0 +1 @@
uucore::bin!(uu_skill);
+106
View File
@@ -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::<String>("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<Option<ActionResult>> = 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<Option<ActionResult>> {
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 <command> ... "expression is a command name"),
arg!(-p --pid <pid> ... "expression is a process id number")
.value_parser(value_parser!(u32)),
arg!(-t --tty <tty> ... "expression is a terminal"),
arg!(-u --user <username> ... "expression is a username"),
// arg!(--ns <PID> "match the processes that belong to the same namespace as <pid>"),
// arg!(--nslist <ns> "list which namespaces will be considered for the --ns option.")
// .value_delimiter(',')
// .value_parser(["ipc", "mnt", "net", "pid", "user", "uts"]),
])
}
+2 -2
View File
@@ -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,
}
+1 -1
View File
@@ -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),
+101
View File
@@ -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<SignalDisplay>,
pub expressions: Option<Vec<SelectedTarget>>,
pub verbose: bool,
}
impl Settings {
pub fn try_new(matches: &ArgMatches) -> UResult<Self> {
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<Vec<SelectedTarget>> {
let cmd = matches
.get_many::<String>("command")
.unwrap_or_default()
.map(Into::into)
.map(SelectedTarget::Command)
.collect::<Vec<_>>();
let pid = matches
.get_many::<u32>("pid")
.unwrap_or_default()
.map(Clone::clone)
.map(SelectedTarget::Pid)
.collect::<Vec<_>>();
let tty = matches
.get_many::<String>("tty")
.unwrap_or_default()
.flat_map(|it| Teletype::try_from(it.as_str()))
.map(SelectedTarget::Tty)
.collect::<Vec<_>>();
let user = matches
.get_many::<String>("user")
.unwrap_or_default()
.map(Into::into)
.map(SelectedTarget::User)
.collect::<Vec<_>>();
let collected = cmd
.into_iter()
.chain(pid)
.chain(tty)
.chain(user)
.collect::<Vec<_>>();
if collected.is_empty() {
None
} else {
Some(collected)
}
}
}
#[allow(clippy::cognitive_complexity)]
pub fn clap_args() -> Vec<Arg> {
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 <command> ... "expression is a command name"),
arg!(-p --pid <pid> ... "expression is a process id number")
.value_parser(value_parser!(u32)),
arg!(-t --tty <tty> ... "expression is a terminal"),
arg!(-u --user <username> ... "expression is a username"),
// arg!(--ns <PID> "match the processes that belong to the same namespace as <pid>"),
// arg!(--nslist <ns> "list which namespaces will be considered for the --ns option.")
// .value_delimiter(',')
// .value_parser(["ipc", "mnt", "net", "pid", "user", "uts"]),
]
}
+30 -118
View File
@@ -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<SignalDisplay>,
expressions: Option<Vec<SelectedTarget>>,
priority: Priority,
verbose: bool,
}
#[allow(unused)] // unused argument under non-unix targets
pub fn print_signals(display: &SignalDisplay) {
#[cfg(target_family = "unix")]
{
let result = display.display(&ALL_SIGNALS);
impl Settings {
fn try_new(matches: &ArgMatches) -> UResult<Self> {
let priority = matches
.try_get_one::<String>("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<Vec<SelectedTarget>> {
let cmd = matches
.get_many::<String>("command")
.unwrap_or_default()
.map(Into::into)
.map(SelectedTarget::Command)
.collect::<Vec<_>>();
let pid = matches
.get_many::<u32>("pid")
.unwrap_or_default()
.map(Clone::clone)
.map(SelectedTarget::Pid)
.collect::<Vec<_>>();
let tty = matches
.get_many::<String>("tty")
.unwrap_or_default()
.flat_map(|it| Teletype::try_from(it.as_str()))
.map(SelectedTarget::Tty)
.collect::<Vec<_>>();
let user = matches
.get_many::<String>("user")
.unwrap_or_default()
.map(Into::into)
.map(SelectedTarget::User)
.collect::<Vec<_>>();
let collected = cmd
.into_iter()
.chain(pid)
.chain(tty)
.chain(user)
.collect::<Vec<_>>();
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::<String>("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<ActionResult>]) -> String {
pub fn construct_verbose_result(pids: &[u32], action_results: &[Option<ActionResult>]) -> String {
let mut table = action_results
.iter()
.enumerate()
@@ -232,7 +160,7 @@ fn construct_verbose_result(pids: &[u32], action_results: &[Option<ActionResult>
}
/// Map and sort `SelectedTarget` to pids.
fn collect_pids(targets: &[SelectedTarget]) -> Vec<u32> {
pub fn collect_pids(targets: &[SelectedTarget]) -> Vec<u32> {
let collected = targets
.iter()
.flat_map(SelectedTarget::to_pids)
@@ -243,7 +171,6 @@ fn collect_pids(targets: &[SelectedTarget]) -> Vec<u32> {
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 <command> ... "expression is a command name"),
arg!(-p --pid <pid> ... "expression is a process id number")
.value_parser(value_parser!(u32)),
arg!(-t --tty <tty> ... "expression is a terminal"),
arg!(-u --user <username> ... "expression is a username"),
])
.args(clap_args())
}
#[cfg(test)]
+13
View File
@@ -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);
}
+4
View File
@@ -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;