pacwrap/pacwrap/src/proc.rs

311 lines
9.8 KiB
Rust

/*
* pacwrap
*
* Copyright (C) 2023-2024 Xavier Moffett <sapphirus@azorium.net>
* SPDX-License-Identifier: GPL-3.0-only
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::{
fmt::{Display, Formatter, Result as FmtResult},
str::FromStr,
};
use indexmap::IndexMap;
use nix::{
sys::signal::{kill, Signal},
unistd::Pid,
};
use pacwrap_core::{
config::cache,
constants::{ARROW_GREEN, BOLD, DIM, RESET},
err,
impl_error,
process::{self, Process},
utils::{
arguments::{InvalidArgument, Operand},
print_warning,
prompt::prompt_targets,
table::{ColumnAttribute, Table},
Arguments,
},
Error,
ErrorGeneric,
ErrorTrait,
Result,
};
#[derive(Debug)]
pub enum ProcError {
NotEnumerable,
SpecifiedNotEnumerable,
InvalidSignalSpecified,
InvalidDepthInput,
}
impl_error!(ProcError);
impl Display for ProcError {
fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult {
match self {
ProcError::NotEnumerable => write!(fmt, "No containers running for pacwrap to enumerate."),
ProcError::SpecifiedNotEnumerable => write!(fmt, "Specified containers are not enumerable."),
ProcError::InvalidSignalSpecified => write!(fmt, "Invalid UNIX signal specified."),
ProcError::InvalidDepthInput => write!(fmt, "Depth can only be specified with a valid integer."),
}?;
write!(fmt, "\nTry 'pacwrap -h' for more information on valid operational parameters.")
}
}
pub fn process(args: &mut Arguments) -> Result<()> {
match args.next().unwrap_or_default() {
Operand::Long("summary") | Operand::Short('s') => summary(args),
Operand::Long("id-list") | Operand::Short('i') => process_id(args),
Operand::Long("kill") | Operand::Short('k') => process_kill(args),
Operand::Nothing =>
if let Operand::Value("ps") = args[0] {
summary(args)
} else {
err!(InvalidArgument::OperationUnspecified)
},
_ =>
if let Operand::Value("ps") = args[0] {
summary(args)
} else {
args.invalid_operand()
},
}
}
fn summary(args: &mut Arguments) -> Result<()> {
let mut all = false;
let mut max_depth = 1;
let mut cmd = 0;
let mut exec = 0;
let mut instances = Vec::new();
args.set_index(1);
while let Some(arg) = args.next() {
match arg {
Operand::Value("ps") | Operand::Short('s') => continue,
Operand::Short('d') | Operand::Short('t') | Operand::Long("depth") | Operand::Long("target") => continue,
Operand::Short('x') | Operand::Long("exec") => exec += 1,
Operand::Short('a') | Operand::Long("all") => all = true,
Operand::Short('c') | Operand::Long("command") => cmd += 1,
Operand::ShortPos('t', val) | Operand::LongPos("target", val) => instances.push(val),
Operand::ShortPos('d', val) | Operand::LongPos("depth", val) => match val.parse() {
Ok(val) => max_depth = val,
Err(_) => err!(ProcError::InvalidDepthInput)?,
},
_ => args.invalid_operand()?,
}
}
let col = (exec > 0, exec > 1 || cmd > 0, (exec > 0) as usize);
let cache = cache::populate()?;
let list = process::list(&cache)?;
let list: Vec<_> = match !instances.is_empty() {
true => list
.list()
.iter()
.filter_map(|a| match instances.contains(&a.instance()) && (a.depth() <= max_depth || all) {
true => Some(*a),
false => None,
})
.collect(),
false => list.list().iter().filter(|a| a.depth() <= max_depth || all).copied().collect(),
};
if list.is_empty() {
err!(ProcError::NotEnumerable)?
}
let table_header = &match col {
(true, false, _) => vec!["PID", "Container", "Executable"],
(false, true, _) => vec!["PID", "Container", "Command"],
(true, true, _) => vec!["PID", "Container", "Executable", "Arguments"],
_ => vec!["PID", "Container"],
};
let mut table = if let (true, false, _) | (true, true, _) = col {
Table::new()
.header(table_header)
.col_attribute(0, ColumnAttribute::AlignRight)
.col_attribute(1, ColumnAttribute::AlignLeftMax(15))
.col_attribute(2, ColumnAttribute::AlignLeftMax(15))
} else {
Table::new()
.header(table_header)
.col_attribute(0, ColumnAttribute::AlignRight)
.col_attribute(1, ColumnAttribute::AlignLeftMax(15))
};
for process in list {
let pid = process.pid().to_string();
let ins = process.instance().to_string();
let row = table.insert(match col {
(true, false, _) => vec![pid, ins, process.exec().into()],
(false, true, i) => vec![pid, ins, process.cmdlist_string(i)],
(true, true, i) => vec![pid, ins, process.exec().into(), process.cmdlist_string(i)],
_ => vec![pid, ins],
});
if process.fork() {
fork_warn(process);
table.mark(row);
}
}
print!("{}{}", if table.marked() { "\n" } else { "" }, table.build().expect("Failed to build table"));
Ok(())
}
fn process_id(args: &mut Arguments) -> Result<()> {
let mut instance = Vec::new();
let mut all = false;
while let Some(arg) = args.next() {
match arg {
Operand::Short('d') => continue,
Operand::Short('a') | Operand::Long("all") => all = true,
Operand::Value(val)
| Operand::ShortPos('i', val)
| Operand::ShortPos('d', val)
| Operand::LongPos("id-list", val) => instance.push(val),
_ => args.invalid_operand()?,
}
}
if instance.is_empty() && !all {
err!(InvalidArgument::TargetUnspecified)?
}
let cache = cache::populate()?;
let list = process::list(&cache)?;
let list: Vec<_> = match all {
false => list.list().iter().filter(|a| instance.contains(&a.instance())).copied().collect(),
true => list.list(),
};
if list.is_empty() {
err!(ProcError::NotEnumerable)?
}
for idx in 0 .. list.len() {
let process = list[idx];
let pid = process.pid();
if process.fork() {
fork_warn(process);
}
if idx == list.len() - 1 {
print!("{}", pid)
} else {
print!("{} ", pid);
}
}
Ok(())
}
fn process_kill(args: &mut Arguments) -> Result<()> {
let mut process: Vec<&str> = Vec::new();
let mut sigint = Signal::SIGHUP;
let mut all = false;
let mut no_confirm = false;
while let Some(arg) = args.next() {
match arg {
Operand::Short('s') | Operand::Long("signal") => continue,
Operand::Long("noconfirm") => no_confirm = true,
Operand::Long("all") => all = true,
Operand::ShortPos('s', val) | Operand::LongPos("signal", val) =>
sigint = match Signal::from_str(&val.to_uppercase()) {
Ok(sig) => sig,
Err(_) => err!(ProcError::InvalidSignalSpecified)?,
},
Operand::ShortPos(_, val) | Operand::LongPos(_, val) | Operand::Value(val) => process.push(val),
_ => args.invalid_operand()?,
}
}
if process.is_empty() && !all {
err!(InvalidArgument::TargetUnspecified)?
}
let mut instances = IndexMap::new();
let cache = cache::populate()?;
let list = process::list(&cache)?;
let list = match all {
false => list
.list()
.iter()
.filter_map(|a| match process.contains(&a.instance()) {
true => Some(*a),
false => None,
})
.collect(),
true => list.list(),
};
if list.is_empty() {
err!(ProcError::SpecifiedNotEnumerable)?
}
for process in list.iter() {
if process.fork() {
fork_warn(process);
}
match instances.get(process.instance()) {
Some(value) => instances.insert(process.instance(), value + 1),
None => instances.insert(process.instance(), 1),
};
}
let instances: Vec<String> = instances.iter().map(|a| format!("{} ({}{}{})", a.0, *DIM, a.1, *RESET)).collect();
let instances: Vec<&str> = instances.iter().map(|a| a.as_ref()).collect();
match no_confirm || prompt_targets(&instances, "Kill container processes?", false) {
true => kill_processes(&list, sigint),
false => Ok(()),
}
}
fn fork_warn(process: &Process) {
print_warning(&format!(
"Process fork detected with PID {}{}{} from an instance of {}{}{}.",
*BOLD,
process.pid(),
*RESET,
*BOLD,
process.instance(),
*RESET
));
}
fn kill_processes(process_list: &Vec<&Process>, sigint: Signal) -> Result<()> {
for list in process_list {
if let Err(err) = kill(Pid::from_raw(list.pid()), sigint).prepend(|| format!("Error killing '{}'", list.pid())) {
err.warn();
continue;
}
eprintln!("{} Killed process {} ({}{}{}) ", *ARROW_GREEN, list.pid(), *BOLD, list.instance(), *RESET);
}
Ok(())
}