diff --git a/Cargo.lock b/Cargo.lock index f2492ae..fa0efd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -632,6 +632,7 @@ dependencies = [ "uu_free", "uu_pgrep", "uu_pidof", + "uu_pidwait", "uu_pmap", "uu_ps", "uu_pwdx", @@ -1039,6 +1040,17 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_pidwait" +version = "0.0.1" +dependencies = [ + "clap", + "nix 0.29.0", + "regex", + "uu_pgrep", + "uucore", +] + [[package]] name = "uu_pmap" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 06135b2..051b7c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ feat_common_core = [ "pgrep", "pidof", "ps", + "pidwait", ] [workspace.dependencies] @@ -56,7 +57,7 @@ bytesize = "1.3.0" chrono = { version = "0.4.38", default-features = false, features = ["clock"] } walkdir = "2.5.0" prettytable-rs = "0.10.0" -nix = { version = "0.29", default-features = false } +nix = { version = "0.29", default-features = false, features = ["process"] } [dependencies] clap = { workspace = true } @@ -78,6 +79,7 @@ slabtop = { optional = true, version = "0.0.1", package = "uu_slabtop", path = " pgrep = { optional = true, version = "0.0.1", package = "uu_pgrep", path = "src/uu/pgrep" } pidof = { optional = true, version = "0.0.1", package = "uu_pidof", path = "src/uu/pidof" } 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" } [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/src/uu/pgrep/src/process.rs b/src/uu/pgrep/src/process.rs index 67229f4..d0c63c7 100644 --- a/src/uu/pgrep/src/process.rs +++ b/src/uu/pgrep/src/process.rs @@ -97,7 +97,7 @@ impl TryFrom for Teletype { } /// State or process -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum RunState { ///`R`, running Running, diff --git a/src/uu/pidwait/Cargo.toml b/src/uu/pidwait/Cargo.toml new file mode 100644 index 0000000..db8f040 --- /dev/null +++ b/src/uu/pidwait/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "uu_pidwait" +version = "0.0.1" +edition = "2021" +authors = ["uutils developers"] +license = "MIT" +description = "pidwait ~ (uutils) Wait for processes based on name" + +homepage = "https://github.com/uutils/procps" +repository = "https://github.com/uutils/procps/tree/main/src/uu/pidwait" +keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] +categories = ["command-line-utilities"] + + +[dependencies] +nix = { workspace = true } +uucore = { workspace = true } +clap = { workspace = true } +regex = { workspace = true } +uu_pgrep = { path = "../pgrep" } + +[lib] +path = "src/pidwait.rs" + +[[bin]] +name = "pidwait" +path = "src/main.rs" diff --git a/src/uu/pidwait/pidwait.md b/src/uu/pidwait/pidwait.md new file mode 100644 index 0000000..fb60ef5 --- /dev/null +++ b/src/uu/pidwait/pidwait.md @@ -0,0 +1,7 @@ +# pidwait + +``` +pidwait [options] pattern +``` + +Wait for processes based on name. diff --git a/src/uu/pidwait/src/main.rs b/src/uu/pidwait/src/main.rs new file mode 100644 index 0000000..5a989ee --- /dev/null +++ b/src/uu/pidwait/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_pidwait); diff --git a/src/uu/pidwait/src/pidwait.rs b/src/uu/pidwait/src/pidwait.rs new file mode 100644 index 0000000..bad2a51 --- /dev/null +++ b/src/uu/pidwait/src/pidwait.rs @@ -0,0 +1,268 @@ +// 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, ArgAction, ArgMatches, Command}; +use regex::Regex; +use std::{collections::HashSet, env, sync::OnceLock}; +use uu_pgrep::process::{walk_process, ProcessInformation, RunState, Teletype}; +use uucore::{ + error::{UResult, USimpleError}, + format_usage, help_about, help_usage, +}; +use wait::wait; + +mod wait; + +const ABOUT: &str = help_about!("pidwait.md"); +const USAGE: &str = help_usage!("pidwait.md"); + +static REGEX: OnceLock = OnceLock::new(); + +#[derive(Debug)] +struct Settings { + echo: bool, + count: bool, + full: bool, + ignore_case: bool, + newest: bool, + oldest: bool, + older: Option, + terminal: Option>, + exact: bool, + runstates: Option>, +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let matches = uu_app().try_get_matches_from(args)?; + + let settings = Settings { + echo: matches.get_flag("echo"), + count: matches.get_flag("count"), + full: matches.get_flag("full"), + ignore_case: matches.get_flag("ignore-case"), + newest: matches.get_flag("newest"), + oldest: matches.get_flag("oldest"), + older: matches.get_one::("older").copied(), + terminal: matches.get_many::("terminal").map(|ttys| { + ttys.cloned() + .flat_map(Teletype::try_from) + .collect::>() + }), + exact: matches.get_flag("exact"), + runstates: matches + .get_many::("runstates") + .map(|it| it.cloned().flat_map(RunState::try_from).collect()), + }; + + let pattern = initialize_pattern(&matches, &settings)?; + REGEX + .set(Regex::new(&pattern).map_err(|e| USimpleError::new(2, e.to_string()))?) + .unwrap(); + + if (!settings.newest + && !settings.oldest + && settings.runstates.is_none() + && settings.older.is_none() + && settings.terminal.is_none()) + && pattern.is_empty() + { + return Err(USimpleError::new( + 2, + "no matching criteria specified\nTry `pidwait --help' for more information.", + )); + } + + let mut proc_infos = collect_proc_infos(&settings); + + // For empty result + if proc_infos.is_empty() { + uucore::error::set_exit_code(1); + } + + // Process outputs + if settings.count { + println!("{}", proc_infos.len()) + } + + if settings.echo { + if settings.newest || settings.oldest { + for ele in &proc_infos { + println!("waiting for (pid {})", ele.pid) + } + } else { + for ele in proc_infos.iter_mut() { + println!("waiting for {} (pid {})", ele.status()["Name"], ele.pid) + } + } + } + + wait(&proc_infos); + + Ok(()) +} + +fn initialize_pattern(matches: &ArgMatches, settings: &Settings) -> 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 `pidwait --help' for more information.", + )) + } + Some(mut patterns) => patterns.next().unwrap(), + None => return Ok(String::new()), + }; + + let pattern = if settings.ignore_case { + &pattern.to_lowercase() + } else { + pattern + }; + + let pattern = if settings.exact { + &format!("^{}$", pattern) + } else { + pattern + }; + + if !settings.full && pattern.len() >= 15 { + const MSG_0: &str= "pidwait: pattern that searches for process name longer than 15 characters will result in zero matches"; + const MSG_1: &str = "Try `pidwait -f' option to match against the complete command line."; + return Err(USimpleError::new(1, format!("{MSG_0}\n{MSG_1}"))); + } + + Ok(pattern.to_string()) +} + +fn collect_proc_infos(settings: &Settings) -> Vec { + // Process pattern + let proc_infos = { + let mut temp = Vec::new(); + for mut it in walk_process() { + let matched = { + let binding = it.status(); + let name = binding.get("Name").unwrap(); + let name = if settings.ignore_case { + name.to_lowercase() + } else { + name.into() + }; + + let want = if settings.exact { + &name + } else if settings.full { + &it.cmdline + } else { + &it.proc_stat()[..15] + }; + + REGEX.get().unwrap().is_match(want) + }; + if matched { + temp.push(it) + } + } + temp + }; + + // Process `-O` + let proc_infos = { + let mut temp: Vec = Vec::new(); + let older = settings.older.unwrap_or_default(); + for mut proc_info in proc_infos { + if proc_info.start_time().unwrap() >= older { + temp.push(proc_info) + } + } + temp + }; + + let mut proc_infos = { + if let Some(terminals) = &settings.terminal { + proc_infos + .into_iter() + .filter(|it| terminals.contains(&it.tty())) + .collect() + } else { + proc_infos + } + }; + + if proc_infos.is_empty() { + return proc_infos; + } + + // Sorting oldest and newest + let proc_infos = if settings.oldest || settings.newest { + proc_infos.sort_by(|a, b| { + b.clone() + .start_time() + .unwrap() + .cmp(&a.clone().start_time().unwrap()) + }); + + let start_time = if settings.newest { + proc_infos.first().cloned().unwrap().start_time().unwrap() + } else { + proc_infos.last().cloned().unwrap().start_time().unwrap() + }; + + // There might be some process start at same time, so need to be filtered. + let mut filtered = proc_infos + .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 { + proc_infos + }; + + proc_infos +} + +#[allow(clippy::cognitive_complexity)] +pub fn uu_app() -> Command { + Command::new(env!("CARGO_PKG_NAME")) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .args([ + arg!(-e --echo "display PIDs before waiting"), + 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(value_parser!(u64)), + // arg!(-P --parent "match only child processes of the given parent"), + // arg!(-s --session "match session IDs"), + arg!(-t --terminal "match by controlling terminal"), + // 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!(-A --"ignore-ancestors" "exclude our ancestors from results"), + ]) + .arg( + Arg::new("pattern") + .help("Name of the program to find the PID of") + .action(ArgAction::Append) + .index(1), + ) +} diff --git a/src/uu/pidwait/src/wait.rs b/src/uu/pidwait/src/wait.rs new file mode 100644 index 0000000..cee49fd --- /dev/null +++ b/src/uu/pidwait/src/wait.rs @@ -0,0 +1,53 @@ +// 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 uu_pgrep::process::ProcessInformation; + +// Dirty, but it works. +// TODO: Use better implementation instead +#[cfg(target_os = "linux")] +pub(crate) fn wait(procs: &[ProcessInformation]) { + use std::{thread::sleep, time::Duration}; + + let mut list = procs.to_vec(); + + loop { + for proc in &list.clone() { + // Check is running + if !is_running(proc.pid) { + list.retain(|it| it.pid != proc.pid) + } + } + + if list.is_empty() { + return; + } + + sleep(Duration::from_millis(50)); + } +} +#[cfg(target_os = "linux")] +fn is_running(pid: usize) -> bool { + use std::{path::PathBuf, str::FromStr}; + use uu_pgrep::process::RunState; + + let proc = PathBuf::from_str(&format!("/proc/{}", pid)).unwrap(); + + if !proc.exists() { + return false; + } + + match ProcessInformation::try_new(proc) { + Ok(mut proc) => proc + .run_state() + .map(|it| it != RunState::Stopped) + .unwrap_or(false), + Err(_) => false, + } +} + +// Just for passing compile on other system. +#[cfg(not(target_os = "linux"))] +pub(crate) fn wait(_procs: &[ProcessInformation]) {} diff --git a/tests/by-util/test_pidwait.rs b/tests/by-util/test_pidwait.rs new file mode 100644 index 0000000..6791eda --- /dev/null +++ b/tests/by-util/test_pidwait.rs @@ -0,0 +1,46 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::common::util::TestScenario; + +#[test] +fn test_invalid_arg() { + new_ucmd!().arg("--definitely-invalid").fails().code_is(1); +} + +#[test] +fn test_non_matching_pattern() { + new_ucmd!() + .arg("THIS_PATTERN_DOES_NOT_MATCH") + .fails() + .code_is(1) + .stderr_contains("pidwait: pattern that searches for process name longer than 15 characters will result in zero matches"); + + new_ucmd!() + .arg("DOES_NOT_MATCH") + .fails() + .code_is(1) + .no_output(); +} + +#[test] +fn test_no_args() { + new_ucmd!() + .fails() + .code_is(2) + .no_stdout() + .stderr_contains("no matching criteria specified"); +} + +#[test] +fn test_too_many_patterns() { + new_ucmd!() + .arg("sh") + .arg("sh") + .fails() + .code_is(2) + .no_stdout() + .stderr_contains("only one pattern can be provided"); +} diff --git a/tests/tests.rs b/tests/tests.rs index d1cb34e..dc69d60 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -40,3 +40,7 @@ mod test_pidof; #[cfg(feature = "ps")] #[path = "by-util/test_ps.rs"] mod test_ps; + +#[cfg(feature = "pidwait")] +#[path = "by-util/test_pidwait.rs"] +mod test_pidwait;