mirror of
https://github.com/uutils/procps.git
synced 2026-06-10 16:14:00 -07:00
Merge pull request #167 from Krysztal112233/impl_pidwait
pidwait: Add command `pidwait`
This commit is contained in:
Generated
+12
@@ -632,6 +632,7 @@ dependencies = [
|
|||||||
"uu_free",
|
"uu_free",
|
||||||
"uu_pgrep",
|
"uu_pgrep",
|
||||||
"uu_pidof",
|
"uu_pidof",
|
||||||
|
"uu_pidwait",
|
||||||
"uu_pmap",
|
"uu_pmap",
|
||||||
"uu_ps",
|
"uu_ps",
|
||||||
"uu_pwdx",
|
"uu_pwdx",
|
||||||
@@ -1039,6 +1040,17 @@ dependencies = [
|
|||||||
"uucore",
|
"uucore",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uu_pidwait"
|
||||||
|
version = "0.0.1"
|
||||||
|
dependencies = [
|
||||||
|
"clap",
|
||||||
|
"nix 0.29.0",
|
||||||
|
"regex",
|
||||||
|
"uu_pgrep",
|
||||||
|
"uucore",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uu_pmap"
|
name = "uu_pmap"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|||||||
+3
-1
@@ -35,6 +35,7 @@ feat_common_core = [
|
|||||||
"pgrep",
|
"pgrep",
|
||||||
"pidof",
|
"pidof",
|
||||||
"ps",
|
"ps",
|
||||||
|
"pidwait",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -56,7 +57,7 @@ bytesize = "1.3.0"
|
|||||||
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
prettytable-rs = "0.10.0"
|
prettytable-rs = "0.10.0"
|
||||||
nix = { version = "0.29", default-features = false }
|
nix = { version = "0.29", default-features = false, features = ["process"] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { workspace = true }
|
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" }
|
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" }
|
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" }
|
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]
|
[dev-dependencies]
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ impl TryFrom<PathBuf> for Teletype {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// State or process
|
/// State or process
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum RunState {
|
pub enum RunState {
|
||||||
///`R`, running
|
///`R`, running
|
||||||
Running,
|
Running,
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# pidwait
|
||||||
|
|
||||||
|
```
|
||||||
|
pidwait [options] pattern
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait for processes based on name.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uucore::bin!(uu_pidwait);
|
||||||
@@ -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<Regex> = OnceLock::new();
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Settings {
|
||||||
|
echo: bool,
|
||||||
|
count: bool,
|
||||||
|
full: bool,
|
||||||
|
ignore_case: bool,
|
||||||
|
newest: bool,
|
||||||
|
oldest: bool,
|
||||||
|
older: Option<u64>,
|
||||||
|
terminal: Option<HashSet<Teletype>>,
|
||||||
|
exact: bool,
|
||||||
|
runstates: Option<HashSet<RunState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<u64>("older").copied(),
|
||||||
|
terminal: matches.get_many::<String>("terminal").map(|ttys| {
|
||||||
|
ttys.cloned()
|
||||||
|
.flat_map(Teletype::try_from)
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
}),
|
||||||
|
exact: matches.get_flag("exact"),
|
||||||
|
runstates: matches
|
||||||
|
.get_many::<String>("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<String> {
|
||||||
|
let pattern = match matches.get_many::<String>("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<ProcessInformation> {
|
||||||
|
// 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<ProcessInformation> = 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::<Vec<_>>();
|
||||||
|
|
||||||
|
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 <PGID> "match listed process group IDs"),
|
||||||
|
// arg!(-G --group <GID> "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 <seconds> "select where older than seconds")
|
||||||
|
.value_parser(value_parser!(u64)),
|
||||||
|
// arg!(-P --parent <PPID> "match only child processes of the given parent"),
|
||||||
|
// arg!(-s --session <SID> "match session IDs"),
|
||||||
|
arg!(-t --terminal <tty> "match by controlling terminal"),
|
||||||
|
// arg!(-u --euid <ID> "match by effective IDs"),
|
||||||
|
// arg!(-U --uid <ID> "match by real IDs"),
|
||||||
|
arg!(-x --exact "match exactly with the command name"),
|
||||||
|
// arg!(-F --pidfile <file> "read PIDs from file"),
|
||||||
|
// arg!(-L --logpidfile "fail if PID file is not locked"),
|
||||||
|
arg!(-r --runstates <state> "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),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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]) {}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
@@ -40,3 +40,7 @@ mod test_pidof;
|
|||||||
#[cfg(feature = "ps")]
|
#[cfg(feature = "ps")]
|
||||||
#[path = "by-util/test_ps.rs"]
|
#[path = "by-util/test_ps.rs"]
|
||||||
mod test_ps;
|
mod test_ps;
|
||||||
|
|
||||||
|
#[cfg(feature = "pidwait")]
|
||||||
|
#[path = "by-util/test_pidwait.rs"]
|
||||||
|
mod test_pidwait;
|
||||||
|
|||||||
Reference in New Issue
Block a user