diff --git a/Cargo.lock b/Cargo.lock index fdb13a67..7c031339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -993,6 +993,7 @@ dependencies = [ "uu_pmap", "uu_ps", "uu_pwdx", + "uu_skill", "uu_slabtop", "uu_snice", "uu_sysctl", @@ -1607,6 +1608,16 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_skill" +version = "0.0.1" +dependencies = [ + "clap", + "nix", + "uu_pgrep", + "uucore", +] + [[package]] name = "uu_slabtop" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 7f1e7373..55910e24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ feat_common_core = [ "pmap", "ps", "pwdx", + "skill", "slabtop", "snice", "sysctl", @@ -89,6 +90,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/pgrep/src/process.rs b/src/uu/pgrep/src/process.rs index 1fbd8f52..22ced590 100644 --- a/src/uu/pgrep/src/process.rs +++ b/src/uu/pgrep/src/process.rs @@ -27,9 +27,9 @@ pub enum Teletype { 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::Tty(id) => write!(f, "/dev/tty{}", id), + Self::TtyS(id) => write!(f, "/dev/ttyS{}", id), + Self::Pts(id) => write!(f, "/dev/pts/{}", id), Self::Unknown => write!(f, "?"), } } diff --git a/src/uu/skill/Cargo.toml b/src/uu/skill/Cargo.toml new file mode 100644 index 00000000..11113e8f --- /dev/null +++ b/src/uu/skill/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "uu_skill" +version = "0.0.1" +edition = "2021" +authors = ["uutils developers"] +license = "MIT" +description = "skill ~ (uutils) Sends a signal to processes based on criteria" + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps/tree/main/src/uu/skill" +keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + +[dependencies] +uucore = { workspace = true , features = ["signals"]} +clap = { workspace = true } +nix = { workspace = true } +uu_pgrep = { path = "../pgrep" } + +[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..d4029f23 --- /dev/null +++ b/src/uu/skill/skill.md @@ -0,0 +1,7 @@ +# skill + +``` +skill [signal] [options] +``` + +Send a signal or report process status diff --git a/src/uu/skill/src/command.rs b/src/uu/skill/src/command.rs new file mode 100644 index 00000000..af0cd7ce --- /dev/null +++ b/src/uu/skill/src/command.rs @@ -0,0 +1,162 @@ +// 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::ArgMatches; +use std::collections::HashSet; +use uucore::signals::{ALL_SIGNALS, DEFAULT_SIGNAL}; + +#[derive(Debug, Clone)] +pub struct Settings { + // Arguments + pub signal: String, + pub expression: Expr, + // Flags + pub fast: bool, + pub interactive: bool, + pub list: bool, + pub table: bool, + pub no_action: bool, + pub verbose: bool, + pub warnings: bool, +} + +#[derive(Debug, Clone)] +pub enum Expr { + Terminal(Vec), + User(Vec), + Pid(Vec), + Command(Vec), + Raw(Vec), +} + +impl Settings { + pub fn new(args: ArgMatches) -> Self { + let mut signal = args.get_one::("signal").unwrap().to_string(); + if signal.starts_with("-") { + signal.remove(0); + } + let literal: Vec = args + .get_many("expression") + .unwrap_or_default() + .cloned() + .collect(); + let expr = if args.get_flag("command") { + Expr::Command(literal) + } else if args.get_flag("user") { + Expr::User(literal) + } else if args.get_flag("pid") { + Expr::Pid(literal.iter().map(|s| s.parse::().unwrap()).collect()) + } else if args.get_flag("tty") { + Expr::Terminal(literal) + } else { + Expr::Raw(literal) + }; + + Self { + signal, + expression: expr, + fast: args.get_flag("fast"), + interactive: args.get_flag("interactive"), + list: args.get_flag("list"), + table: args.get_flag("table"), + no_action: args.get_flag("no-action"), + verbose: args.get_flag("verbose"), + warnings: args.get_flag("warnings"), + } + } +} + +// Pre-parses the command line arguments and returns a vector of OsString + +// Mainly used to parse the signal to make sure it is valid +// and insert the default signal if it's not present +pub fn parse_command(args: &mut impl uucore::Args) -> Vec { + let option_char_set: HashSet = HashSet::from([ + '-', 'f', 'i', 'l', 'L', 'n', 'v', 'w', 'c', 'p', 't', 'u', 'h', 'V', + ]); + let option_set: HashSet<&str> = HashSet::from([ + "--table", + "--list", + "--no-action", + "--verbose", + "--warnings", + "--interactive", + "--fast", + "--command", + "--user", + "--pid", + "--tty", + "--help", + "--version", + ]); + let args = args + .map(|str| str.to_str().unwrap().into()) + .collect::>(); + + let exprs = |arg: &String| !arg.starts_with('-'); + let options = |arg: &String| { + (arg.starts_with('-') && arg.chars().all(|c| option_char_set.contains(&c))) + || (arg.starts_with("--") && option_set.contains(&arg.as_str())) + }; + + let signals = args + .iter() + .filter(|arg: &&String| !exprs(arg)) + .filter(|arg: &&String| !options(arg)) + .collect::>(); + if signals.len() == 1 { + let signal = &signals[0].as_str()[1..]; // Remove the leading '-' + if ALL_SIGNALS.contains(&signal) { + args.to_vec() + } else { + eprintln!("Invalid signal: {}", &signal); + std::process::exit(2); + } + } else if signals.is_empty() { + // If no signal is provided, return the original args with default signal + let mut new = args.to_vec(); + new.insert(1, ALL_SIGNALS[DEFAULT_SIGNAL].to_string()); + new + } else { + eprintln!("Too many signals"); + std::process::exit(2); + } +} + +#[cfg(test)] +mod test { + use super::parse_command; + use std::ffi::OsString; + + #[test] + fn test_parse_command_normal() { + let args: Vec = vec!["skill", "-TERM", "-v", "1234"] + .into_iter() + .map(|s| OsString::from(s)) + .collect(); + let parsed = parse_command(&mut args.iter().map(|s| s.clone())); + assert_eq!(parsed, vec!["skill", "-TERM", "-v", "1234"]); + } + + #[test] + fn test_parse_command_default_signal() { + let args: Vec = vec!["skill", "-v", "-l", "1234"] + .into_iter() + .map(|s| OsString::from(s)) + .collect(); + let parsed = parse_command(&mut args.iter().cloned()); + assert_eq!(parsed, vec!["skill", "TERM", "-v", "-l", "1234"]); + } + + #[test] + fn test_parse_command_unordered() { + let args: Vec = vec!["skill", "-v", "-l", "-KILL", "1234"] + .into_iter() + .map(|s| OsString::from(s)) + .collect(); + let parsed = parse_command(&mut args.iter().map(|s| s.clone())); + assert_eq!(parsed, vec!["skill", "-v", "-l", "-KILL", "1234"]); + } +} 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..5b395ed9 --- /dev/null +++ b/src/uu/skill/src/skill.rs @@ -0,0 +1,342 @@ +// 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. + +mod command; +mod util; + +use crate::command::{parse_command, Expr, Settings}; +use clap::{crate_version, Arg, ArgAction, Command}; +use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, +}; +use uu_pgrep::process::ProcessInformation; +use uucore::signals::ALL_SIGNALS; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +const ABOUT: &str = help_about!("skill.md"); +const USAGE: &str = help_usage!("skill.md"); +const SIGNALS_PER_ROW: usize = 7; // Be consistent with procps-ng + +#[uucore::main] +pub fn uumain(mut args: impl uucore::Args) -> UResult<()> { + let new = parse_command(&mut args); + let matches = uu_app().try_get_matches_from(new)?; + let mut cli = Settings::new(matches); + + // If list or table is specified, print the list of signals and return + if cli.list || cli.table { + list_signals(&cli); + return Ok(()); + } + + if cli.fast { + //TODO: implement this option + } + + let signal = parse_signal_str(&cli.signal); + + // parse the expression if not specify its type + parse_expression(&mut cli); + + let matching_processes = find_matching_processes(&cli.expression); + + if matching_processes.is_empty() { + eprintln!("No matching processes found"); + return Ok(()); + } + + if cli.verbose || cli.no_action { + for process in &matching_processes { + println!( + "Would send signal {} to process {} with cmd {}", + &cli.signal, process.pid, process.cmdline + ); + } + if cli.no_action { + return Ok(()); + } + } + + if cli.interactive { + for mut process in matching_processes { + let cmd = process.cmdline.clone(); + let owner = + util::get_process_owner(&mut process).unwrap_or_else(|| "".to_string()); + let tty = + util::get_process_terminal(&process).unwrap_or_else(|| "".to_string()); + if confirm_action(&tty, &owner, process.pid as i32, &cmd) { + if let Err(e) = send_signal(process.pid as i32, signal) { + if cli.warnings { + eprintln!("Failed to send signal to process {}: {}", process.pid, e); + } + } + } else { + println!("Skipping process {}", process.pid); + } + } + } else { + for process in matching_processes { + if let Err(e) = send_signal(process.pid as i32, signal) { + if cli.warnings { + eprintln!("Failed to send signal to process {}: {}", process.pid, e); + } + } + } + } + + Ok(()) +} + +// TODO: add more strict check according to the usage +fn parse_expression(cli: &mut Settings) { + if let Expr::Raw(raw_expr) = &cli.expression { + // Check if any strings in the raw expression match active users, commands, or terminals + if raw_expr.iter().all(|s| s.parse::().is_ok()) { + cli.expression = + Expr::Pid(raw_expr.iter().map(|s| s.parse::().unwrap()).collect()); + } else { + let mut processes = util::get_all_processes(); + let is_user_expr = raw_expr + .iter() + .any(|s| util::get_active_users(&mut processes).contains(s)); + let is_command_expr = raw_expr + .iter() + .any(|s| util::get_active_commands(&processes).contains(s)); + let is_terminal_expr = raw_expr + .iter() + .any(|s| util::get_active_terminals(&processes).contains(s)); + // Only perform the replacement if we found matching users + let raw_clone = raw_expr.clone(); + if is_user_expr { + cli.expression = Expr::User(raw_clone); + } else if is_command_expr { + cli.expression = Expr::Command(raw_clone); + } else if is_terminal_expr { + cli.expression = Expr::Terminal(raw_clone); + } + } + } +} + +fn send_signal(pid: i32, signal: Signal) -> UResult<()> { + match signal::kill(Pid::from_raw(pid), signal) { + Ok(_) => Ok(()), + Err(e) => Err(e.into()), + } +} + +fn find_matching_processes(expression: &Expr) -> Vec { + let mut processes = Vec::new(); + + match expression { + Expr::Pid(p) => { + processes.extend(util::filter_processes_by_pid(p)); + } + Expr::User(u) => { + processes.extend(util::filter_processes_by_user(u)); + } + Expr::Command(c) => { + processes.extend(util::filter_processes_by_command(c)); + } + Expr::Terminal(t) => { + processes.extend(util::filter_processes_by_terminal(t)); + } + _ => { + return Vec::new(); + } + } + processes +} + +fn confirm_action(tty: &str, owner: &str, pid: i32, cmd: &str) -> bool { + use std::io::{stdin, stdout, Write}; + + print!("{}: {} {} {} y/N? ", tty, owner, pid, cmd); + stdout().flush().unwrap(); + + let mut input = String::new(); + if stdin().read_line(&mut input).is_err() { + return false; + } + + let input = input.trim().to_lowercase(); + input == "y" || input == "yes" +} + +fn list_signals(cli: &Settings) { + if cli.list { + for signal in ALL_SIGNALS[1..].iter() { + print!("{} ", signal); + if signal == ALL_SIGNALS.last().unwrap() { + println!(); + } + } + } else if cli.table { + let mut result = Vec::new(); + let mut signal_num = 1; + + // Group signals into rows of 7 + for chunk in ALL_SIGNALS[1..].chunks(SIGNALS_PER_ROW) { + let mut row = String::new(); + // Format each signal with number in the row + for signal in chunk.iter() { + row.push_str(&format!("{:2} {:<7}", signal_num, signal)); + signal_num += 1; + } + result.push(row); + } + + for row in result { + println!("{}", row); + } + } +} + +fn parse_signal_str(signal: &str) -> Signal { + match signal { + "HUP" => Signal::SIGHUP, + "INT" => Signal::SIGINT, + "QUIT" => Signal::SIGQUIT, + "ILL" => Signal::SIGILL, + "TRAP" => Signal::SIGTRAP, + "ABRT" => Signal::SIGABRT, + "BUS" => Signal::SIGBUS, + "FPE" => Signal::SIGFPE, + "KILL" => Signal::SIGKILL, + "USR1" => Signal::SIGUSR1, + "SEGV" => Signal::SIGSEGV, + "USR2" => Signal::SIGUSR2, + "PIPE" => Signal::SIGPIPE, + "ALRM" => Signal::SIGALRM, + "TERM" => Signal::SIGTERM, + "STKFLT" => Signal::SIGSTKFLT, + "CHLD" => Signal::SIGCHLD, + "CONT" => Signal::SIGCONT, + "STOP" => Signal::SIGSTOP, + "TSTP" => Signal::SIGTSTP, + "TTIN" => Signal::SIGTTIN, + "TTOU" => Signal::SIGTTOU, + "URG" => Signal::SIGURG, + "XCPU" => Signal::SIGXCPU, + "XFSZ" => Signal::SIGXFSZ, + "VTALRM" => Signal::SIGVTALRM, + "PROF" => Signal::SIGPROF, + "WINCH" => Signal::SIGWINCH, + "POLL" => Signal::SIGIO, + "PWR" => Signal::SIGPWR, + "SYS" => Signal::SIGSYS, + _ => panic!("Unknown signal: {}", signal), + } +} + +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") + .required(true) + .index(1) + .allow_hyphen_values(true) + .default_value("TERM"), + ) + .arg( + Arg::new("expression") + .help("Expression to match, can be: terminal, user, pid, command.") + .value_name("expression") + .required_unless_present_any(["table", "list"]) + .num_args(1..) + .index(2), + ) + // Flag options + .arg( + Arg::new("fast") + .short('f') + .long("fast") + .help("fast mode (not implemented)") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("interactive") + .short('i') + .long("interactive") + .help("interactive") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("list") + .short('l') + .long("list") + .help("list all signal names") + .action(ArgAction::SetTrue) + .conflicts_with("table"), + ) + .arg( + Arg::new("table") + .short('L') + .long("table") + .help("list all signal names in a nice table") + .action(ArgAction::SetTrue) + .conflicts_with("list"), + ) + .arg( + Arg::new("no-action") + .short('n') + .long("no-action") + .help("do not actually kill processes; just print what would happen") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("verbose") + .short('v') + .long("verbose") + .help("explain what is being done") + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new("warnings") + .short('w') + .long("warnings") + .help("enable warnings (not implemented)") + .action(ArgAction::SetTrue), + ) + // Non-flag options + .arg( + Arg::new("command") + .short('c') + .long("command") + .help("expression is a command name") + .action(ArgAction::SetTrue) + .help_heading("The options below may be used to ensure correct interpretation."), + ) + .arg( + Arg::new("pid") + .short('p') + .long("pid") + .help("expression is a process id number") + .action(ArgAction::SetTrue) + .help_heading("The options below may be used to ensure correct interpretation."), + ) + .arg( + Arg::new("tty") + .short('t') + .long("tty") + .help("expression is a terminal") + .action(ArgAction::SetTrue) + .help_heading("The options below may be used to ensure correct interpretation."), + ) + .arg( + Arg::new("user") + .short('u') + .long("user") + .help("expression is a username") + .action(ArgAction::SetTrue) + .help_heading("The options below may be used to ensure correct interpretation."), + ) +} diff --git a/src/uu/skill/src/util.rs b/src/uu/skill/src/util.rs new file mode 100644 index 00000000..f56f9a11 --- /dev/null +++ b/src/uu/skill/src/util.rs @@ -0,0 +1,139 @@ +// 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::collections::HashSet; +use uu_pgrep::process::{walk_process, ProcessInformation}; +#[cfg(target_os = "linux")] +pub fn get_all_processes() -> Vec { + walk_process().collect() +} + +#[cfg(target_os = "linux")] +pub fn filter_processes_by_pid(pids: &[i32]) -> Vec { + let mut matching_pids = Vec::new(); + + for process in get_all_processes() { + if pids.iter().any(|pid| *pid == process.pid as i32) { + matching_pids.push(process); + } + } + + matching_pids +} + +#[cfg(target_os = "linux")] +pub fn filter_processes_by_user(users: &[String]) -> Vec { + let mut matching_pids = Vec::new(); + + for mut process in get_all_processes() { + if let Some(owner) = get_process_owner(&mut process) { + if users.iter().any(|u| *u == owner) { + matching_pids.push(process); + } + } + } + + matching_pids +} + +#[cfg(target_os = "linux")] +pub fn filter_processes_by_command(commands: &[String]) -> Vec { + let mut matching_processes = Vec::new(); + + for process in get_all_processes() { + let cmdline = process.cmdline.split(" ").collect::>()[0]; + let cmd_name = cmdline.split("/").last().unwrap_or(cmdline); + if commands.iter().any(|c| c == cmd_name) { + matching_processes.push(process); + } + } + + matching_processes +} + +#[cfg(target_os = "linux")] +pub fn filter_processes_by_terminal(terminals: &[String]) -> Vec { + let mut matching_processes = Vec::new(); + + for process in get_all_processes() { + if let Some(tty) = get_process_terminal(&process) { + if terminals.iter().any(|t| tty.contains(t)) { + matching_processes.push(process); + } + } + } + + matching_processes +} + +#[cfg(target_os = "linux")] +pub fn get_process_owner(process: &mut ProcessInformation) -> Option { + // let status_path = format!("/proc/{}/status", pid); + let uid = process.uid().ok()?; + + // Read /etc/passwd to look up the username for this UID + std::fs::read_to_string("/etc/passwd") + .ok()? + .lines() + .find_map(|line| { + let parts: Vec<&str> = line.split(':').collect(); + if parts.len() >= 3 { + if let Ok(entry_uid) = parts[2].parse::() { + if entry_uid == uid { + return Some(parts[0].to_string()); + } + } + } + None + }) +} + +#[cfg(target_os = "linux")] +pub fn get_process_terminal(process: &ProcessInformation) -> Option { + use uu_pgrep::process::Teletype; + match process.tty() { + Teletype::Tty(id) => Some(format!("tty{}", id)), + Teletype::TtyS(id) => Some(format!("ttyS{}", id)), + Teletype::Pts(id) => Some(format!("pts/{}", id)), + Teletype::Unknown => None, + } +} + +#[cfg(target_os = "linux")] +pub fn get_active_users(processes: &mut [ProcessInformation]) -> HashSet { + let mut users = HashSet::new(); + + for process in processes { + if let Some(user) = get_process_owner(process) { + users.insert(user); + } + } + + users +} + +#[cfg(target_os = "linux")] +pub fn get_active_terminals(processes: &[ProcessInformation]) -> HashSet { + let mut terminals = HashSet::new(); + + for process in processes { + if let Some(tty) = get_process_terminal(process) { + terminals.insert(tty); + } + } + + terminals +} + +#[cfg(target_os = "linux")] +pub fn get_active_commands(processes: &[ProcessInformation]) -> HashSet { + let mut commands = HashSet::new(); + + for process in processes { + commands.insert(process.cmdline.to_string()); + } + + commands +} diff --git a/tests/by-util/test_skill.rs b/tests/by-util/test_skill.rs new file mode 100644 index 00000000..ed6de237 --- /dev/null +++ b/tests/by-util/test_skill.rs @@ -0,0 +1,152 @@ +// 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. + +#[cfg(unix)] +use crate::common::util::TestScenario; + +#[cfg(target_os = "linux")] +#[test] +fn test_missing_expression() { + new_ucmd!().fails().no_stdout().code_is(1).stderr_contains( + "the following required arguments were not provided: + ...", + ); +} +#[cfg(target_os = "linux")] +#[test] +fn test_default_signal() { + new_ucmd!() + .arg("-nv") // no action + verbose + .arg("1") + .succeeds() + .stdout_contains("Would send signal TERM to process 1"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_invalid_signal() { + new_ucmd!() + .arg("-INVALID") + .fails() + .code_is(2) + .no_stdout() + .stderr_contains("Invalid signal: INVALID"); +} +#[cfg(target_os = "linux")] +#[test] +fn test_mutiple_signal() { + new_ucmd!() + .arg("-HUP") + .arg("-TERM") + .fails() + .code_is(2) + .no_stdout() + .stderr_contains("Too many signals"); +} +#[cfg(target_os = "linux")] +#[test] +fn test_verbose_option() { + new_ucmd!() + .arg("-n") // no action + .arg("-v") + .arg("1") + .succeeds() + .stdout_contains("Would send signal TERM to process 1") + .no_stderr(); +} +#[cfg(target_os = "linux")] +#[test] +fn test_list_option() { + new_ucmd!() + .arg("-l") + .succeeds() + .no_stderr() + .stdout_contains("HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_list_option_long() { + new_ucmd!() + .arg("--list") + .succeeds() + .no_stderr() + .stdout_contains("HUP INT QUIT ILL TRAP ABRT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_table_option() { + new_ucmd!() + .arg("-L") + .succeeds() + .no_stderr() + .stdout_contains("1 HUP 2 INT 3 QUIT 4 ILL 5 TRAP 6 ABRT 7 BUS") + .stdout_contains("8 FPE 9 KILL 10 USR1 11 SEGV 12 USR2 13 PIPE 14 ALRM") + .stdout_contains("15 TERM 16 STKFLT 17 CHLD 18 CONT 19 STOP 20 TSTP 21 TTIN") + .stdout_contains("22 TTOU 23 URG 24 XCPU 25 XFSZ 26 VTALRM 27 PROF 28 WINCH") + .stdout_contains("29 POLL 30 PWR 31 SYS"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_table_option_long() { + new_ucmd!() + .arg("--table") + .succeeds() + .no_stderr() + .stdout_contains("1 HUP 2 INT 3 QUIT 4 ILL 5 TRAP 6 ABRT 7 BUS") + .stdout_contains("8 FPE 9 KILL 10 USR1 11 SEGV 12 USR2 13 PIPE 14 ALRM") + .stdout_contains("15 TERM 16 STKFLT 17 CHLD 18 CONT 19 STOP 20 TSTP 21 TTIN") + .stdout_contains("22 TTOU 23 URG 24 XCPU 25 XFSZ 26 VTALRM 27 PROF 28 WINCH") + .stdout_contains("29 POLL 30 PWR 31 SYS"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_mutiple_options() { + new_ucmd!() + .arg("-nv") // no action + verbose + .arg("1") + .succeeds() + .stdout_contains("Would send signal TERM to process 1"); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_command_option() { + use std::process::Command; + + Command::new("sleep") + .arg("5") + .spawn() + .expect("Failed to start sleep process"); + + new_ucmd!() + .arg("-n") // no action + .arg("-c") + .arg("sleep") + .succeeds(); +} + +#[cfg(target_os = "linux")] +#[test] +fn test_user_option() { + use std::process::Command; + let output = Command::new("whoami") + .output() + .expect("Failed to execute whoami"); + let current_user = String::from_utf8(output.stdout) + .expect("Invalid UTF-8 output") + .trim() + .to_string(); + + new_ucmd!() + .arg("-n") // no action + .arg("-u") + .arg(¤t_user) + .succeeds() + .no_stderr(); +} diff --git a/tests/tests.rs b/tests/tests.rs index 620d49f8..0cfb3c9c 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -25,6 +25,10 @@ mod test_watch; #[path = "by-util/test_pmap.rs"] mod test_pmap; +#[cfg(feature = "skill")] +#[path = "by-util/test_skill.rs"] +mod test_skill; + #[cfg(feature = "slabtop")] #[path = "by-util/test_slabtop.rs"] mod test_slabtop;