use std::{
cell::RefCell,
collections::BTreeMap,
fmt::{
Debug,
Display,
},
io::{
self,
Write,
},
sync::Arc,
};
use itertools::chain;
use nix::{
fcntl::OFlag,
libc::ENOENT,
unistd::Pid,
};
use owo_colors::{
OwoColorize,
Style,
};
use crate::{
cache::ArcStr,
cli::{
args::{
LogModeArgs,
ModifierArgs,
},
theme::THEME,
},
event::{
FriendlyError,
OutputMsg,
},
proc::{
BaselineInfo,
FileDescriptorInfo,
FileDescriptorInfoCollection,
Interpreter,
diff_env,
},
timestamp::TimestampFormat,
tracer::ExecData,
};
macro_rules! escape_str_for_bash {
($x:expr) => {{
let result: String = shell_quote::QuoteRefExt::quoted($x, shell_quote::Bash);
result
}};
}
#[derive(Debug, Clone, Copy)]
pub enum EnvPrintFormat {
Diff,
Raw,
None,
}
#[derive(Debug, Clone, Copy)]
pub enum FdPrintFormat {
Diff,
Raw,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
pub enum ColorLevel {
Less,
Normal,
More,
}
#[derive(Debug, Clone)]
pub struct PrinterArgs {
pub trace_comm: bool,
pub trace_argv: bool,
pub trace_env: EnvPrintFormat,
pub trace_fd: FdPrintFormat,
pub trace_cwd: bool,
pub print_cmdline: bool,
pub successful_only: bool,
pub trace_interpreter: bool,
pub trace_filename: bool,
pub decode_errno: bool,
pub color: ColorLevel,
pub stdio_in_cmdline: bool,
pub fd_in_cmdline: bool,
pub hide_cloexec_fds: bool,
pub inline_timestamp_format: Option<TimestampFormat>,
}
impl PrinterArgs {
pub fn from_cli(tracing_args: &LogModeArgs, modifier_args: &ModifierArgs) -> Self {
Self {
trace_comm: !tracing_args.no_show_comm,
trace_argv: !tracing_args.no_show_argv && !tracing_args.show_cmdline,
trace_env: match (
tracing_args.show_cmdline,
tracing_args.diff_env,
tracing_args.no_diff_env,
tracing_args.show_env,
tracing_args.no_show_env,
) {
(true, ..) | (.., true) => EnvPrintFormat::None,
(false, .., true, _) | (false, _, true, ..) => EnvPrintFormat::Raw,
_ => EnvPrintFormat::Diff, },
trace_fd: match (
tracing_args.diff_fd,
tracing_args.no_diff_fd,
tracing_args.show_fd,
tracing_args.no_show_fd,
) {
(false, _, true, false) => FdPrintFormat::Raw,
(_, true, _, _) => FdPrintFormat::None,
(true, _, _, _) => FdPrintFormat::Diff,
_ => {
if modifier_args.fd_in_cmdline || modifier_args.stdio_in_cmdline {
FdPrintFormat::None
} else {
FdPrintFormat::Diff
}
}
},
trace_cwd: tracing_args.show_cwd,
print_cmdline: tracing_args.show_cmdline,
successful_only: modifier_args.successful_only,
trace_interpreter: tracing_args.show_interpreter,
trace_filename: match (tracing_args.show_filename, tracing_args.no_show_filename) {
(_, true) => false,
(true, _) => true,
_ => true,
},
decode_errno: !tracing_args.no_decode_errno,
color: match (tracing_args.more_colors, tracing_args.less_colors) {
(false, false) => ColorLevel::Normal,
(true, false) => ColorLevel::More,
(false, true) => ColorLevel::Less,
_ => unreachable!(),
},
stdio_in_cmdline: modifier_args.stdio_in_cmdline,
fd_in_cmdline: modifier_args.fd_in_cmdline,
hide_cloexec_fds: modifier_args.hide_cloexec_fds,
inline_timestamp_format: modifier_args.timestamp.then(|| {
#[expect(clippy::unwrap_used)]
let value = modifier_args.inline_timestamp_format.clone().unwrap();
value
}),
}
}
}
pub type PrinterOut = dyn Write + Send + Sync + 'static;
enum DeferredWarningKind {
NoArgv0,
FailedReadingArgv(FriendlyError),
FailedReadingFilename(FriendlyError),
FailedReadingEnvp(FriendlyError),
}
struct DeferredWarnings {
warning: DeferredWarningKind,
pid: Pid,
}
impl Drop for DeferredWarnings {
#[expect(clippy::unwrap_used)]
fn drop(&mut self) {
Printer::OUT.with_borrow_mut(|out| {
if let Some(out) = out {
write!(out, "{}", self.pid.bright_red()).unwrap();
write!(out, "[{}]: ", "warning".bright_yellow()).unwrap();
match self.warning {
DeferredWarningKind::NoArgv0 => {
write!(
out,
"No argv[0] provided! The printed commandline might be incorrect!"
)
.unwrap();
}
DeferredWarningKind::FailedReadingArgv(e) => {
write!(out, "Failed to read argv: {e}").unwrap();
}
DeferredWarningKind::FailedReadingFilename(e) => {
write!(out, "Failed to read filename: {e}").unwrap();
}
DeferredWarningKind::FailedReadingEnvp(e) => {
write!(out, "Failed to read envp: {e}").unwrap();
}
};
writeln!(out).unwrap();
};
})
}
}
pub struct ListPrinter {
style: owo_colors::Style,
}
impl ListPrinter {
pub fn new(color: ColorLevel) -> Self {
if color > ColorLevel::Normal {
Self {
style: Style::new().bright_white().bold(),
}
} else {
Self {
style: Style::new(),
}
}
}
pub fn begin(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "{}", "[".style(self.style))
}
pub fn end(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "{}", "]".style(self.style))
}
pub fn comma(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "{}", ", ".style(self.style))
}
pub fn print_string_list(&self, out: &mut dyn Write, list: &[impl Display]) -> io::Result<()> {
self.begin(out)?;
if let Some((last, rest)) = list.split_last() {
#[allow(clippy::branches_sharing_code)]
if rest.is_empty() {
write!(out, "{last}")?;
} else {
for s in rest {
write!(out, "{s}")?;
self.comma(out)?;
}
write!(out, "{last}")?;
}
}
self.end(out)
}
pub fn print_env(
&self,
out: &mut dyn Write,
env: &BTreeMap<OutputMsg, OutputMsg>,
) -> io::Result<()> {
self.begin(out)?;
let mut first_item_written = false;
let mut write_separator = |out: &mut dyn Write| -> io::Result<()> {
if first_item_written {
self.comma(out)?;
} else {
first_item_written = true;
}
Ok(())
};
for (k, v) in env.iter() {
write_separator(out)?;
write!(out, "{k}={v}")?;
}
self.end(out)
}
}
pub struct Printer {
pub args: PrinterArgs,
baseline: Arc<BaselineInfo>,
}
impl Printer {
pub fn new(args: PrinterArgs, baseline: Arc<BaselineInfo>) -> Self {
Self { args, baseline }
}
thread_local! {
pub static OUT: RefCell<Option<Box<PrinterOut>>> = RefCell::new(None);
}
pub fn init_thread_local(&self, output: Option<Box<PrinterOut>>) {
Self::OUT.with(|out| {
*out.borrow_mut() = output;
});
}
pub fn print_new_child(&self, parent: Pid, comm: &str, child: Pid) -> color_eyre::Result<()> {
Self::OUT.with_borrow_mut(|out| {
let Some(out) = out else {
return Ok(());
};
write!(out, "{}", parent.bright_green())?;
if self.args.trace_comm {
write!(out, "<{}>", comm.cyan())?;
}
writeln!(out, ": {}: {}", "new child".purple(), child.bright_green())?;
out.flush()?;
Ok(())
})
}
fn print_stdio_fd(
&self,
out: &mut dyn Write,
fd: i32,
orig_fd: &FileDescriptorInfo,
curr_fd: Option<&FileDescriptorInfo>,
fd_collection_reliable: bool,
list_printer: &ListPrinter,
) -> io::Result<()> {
let desc = match fd {
0 => "stdin",
1 => "stdout",
2 => "stderr",
_ => unreachable!(),
};
if let Some(fdinfo) = curr_fd {
if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
if !self.args.hide_cloexec_fds {
write!(
out,
"{}{}",
"cloexec: ".bright_red().bold(),
desc.bright_red().bold()
)?;
} else {
write!(
out,
"{}{}",
"closed: ".bright_red().bold(),
desc.bright_red().bold()
)?;
}
list_printer.comma(out)?;
} else if fdinfo.not_same_file_as(orig_fd) {
write!(out, "{}", desc.bright_yellow().bold())?;
write!(out, "={}", fdinfo.path.bright_yellow())?;
list_printer.comma(out)?;
}
} else if fd_collection_reliable {
write!(
out,
"{}{}",
"closed: ".bright_red().bold(),
desc.bright_red().bold()
)?;
list_printer.comma(out)?;
}
Ok(())
}
fn print_stdio_fds_in_cmdline(
&self,
out: &mut dyn Write,
curr_fds: &FileDescriptorInfoCollection,
) -> io::Result<()> {
for (fd, orig_fd) in self.baseline.fdinfo.stdio() {
let (redirect, closed) = match fd {
0 => ("<", "0>&-"),
1 => (">", "1>&-"),
2 => ("2>", "2>&-"),
_ => unreachable!(),
};
if let Some(fdinfo) = curr_fds.get(fd) {
if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
write!(out, " {}", closed.bright_red().bold().italic())?;
} else if fdinfo.not_same_file_as(orig_fd) {
write!(
out,
" {}{}",
redirect.bright_yellow().bold(),
fdinfo.path.cli_bash_escaped_with_style(THEME.modified_fd)
)?;
}
} else if curr_fds.is_reliable() {
write!(out, " {}", closed.bright_red().bold())?;
}
}
Ok(())
}
pub fn print_fd(
&self,
out: &mut dyn Write,
fds: &FileDescriptorInfoCollection,
) -> io::Result<()> {
match self.args.trace_fd {
FdPrintFormat::Diff => {
write!(out, " {} ", "fd".purple())?;
let list_printer = ListPrinter::new(self.args.color);
list_printer.begin(out)?;
if let Some(error) = &fds.error {
write!(out, "{}", <&'static str>::from(error).bright_red())?;
list_printer.comma(out)?;
}
for (fd, fdinfo_orig) in self.baseline.fdinfo.stdio() {
self.print_stdio_fd(
out,
fd,
fdinfo_orig,
fds.fdinfo.get(&fd),
fds.is_reliable(),
&list_printer,
)?;
}
for (&fd, fdinfo) in fds.fdinfo.iter() {
if fd < 3 {
continue;
}
if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
if !self.args.hide_cloexec_fds {
write!(
out,
"{} {}",
"cloexec:".bright_red().bold(),
fd.bright_green().bold()
)?;
write!(out, "={}", fdinfo.path.bright_red())?;
list_printer.comma(out)?;
}
} else {
write!(out, "{}", fd.bright_green().bold())?;
write!(out, "={}", fdinfo.path.bright_green())?;
list_printer.comma(out)?;
}
}
list_printer.end(out)?;
}
FdPrintFormat::Raw => {
write!(out, " {} ", "fd".purple())?;
let list_printer = ListPrinter::new(self.args.color);
list_printer.begin(out)?;
if let Some(error) = &fds.error {
write!(out, "{}", <&'static str>::from(error).bright_red())?;
if !fds.fdinfo.is_empty() {
list_printer.comma(out)?;
}
}
let last = fds.fdinfo.len().saturating_sub(1);
for (idx, (fd, fdinfo)) in fds.fdinfo.iter().enumerate() {
if fdinfo.flags.contains(OFlag::O_CLOEXEC) {
if self.args.hide_cloexec_fds {
continue;
}
write!(out, "{}", fd.bright_red().bold())?;
} else {
write!(out, "{}", fd.bright_cyan().bold())?;
}
write!(out, "={}", fdinfo.path)?;
if idx != last {
list_printer.comma(out)?;
}
}
list_printer.end(out)?;
}
FdPrintFormat::None => {}
}
Ok(())
}
pub fn print_exec_trace(
&self,
pid: Pid,
comm: ArcStr,
result: i64,
exec_data: &ExecData,
env: &BTreeMap<OutputMsg, OutputMsg>,
cwd: &OutputMsg,
) -> color_eyre::Result<()> {
#[allow(clippy::collection_is_never_read)]
let mut _deferred_warnings = vec![];
Self::OUT.with_borrow_mut(|out| {
let Some(out) = out else {
return Ok(());
};
let list_printer = ListPrinter::new(self.args.color);
if let Some(f) = self.args.inline_timestamp_format.as_deref() {
write!(out, "{} ", exec_data.timestamp.format(f).bright_cyan())?;
}
if result == 0 {
write!(out, "{}", pid.bright_green())?;
} else if result == -ENOENT as i64 {
write!(out, "{}", pid.bright_yellow())?;
} else {
write!(out, "{}", pid.bright_red())?;
}
if self.args.trace_comm {
write!(out, "<{}>", comm.cyan())?;
}
write!(out, ":")?;
if self.args.trace_filename {
write!(
out,
" {}",
exec_data.filename.cli_escaped_styled(THEME.filename)
)?;
}
if let OutputMsg::Err(e) = exec_data.filename {
_deferred_warnings.push(DeferredWarnings {
warning: DeferredWarningKind::FailedReadingFilename(e),
pid,
});
}
match exec_data.argv.as_ref() {
Err(e) => {
_deferred_warnings.push(DeferredWarnings {
warning: DeferredWarningKind::FailedReadingArgv(FriendlyError::InspectError(*e)),
pid,
});
}
Ok(argv) => {
if self.args.trace_argv {
write!(out, " ")?;
list_printer.print_string_list(out, argv)?;
}
}
}
if self.args.trace_cwd {
write!(
out,
" {} {}",
"at".purple(),
exec_data
.cwd
.cli_escaped_styled(if self.args.color >= ColorLevel::Normal {
THEME.cwd
} else {
THEME.plain
})
)?;
}
if self.args.trace_interpreter
&& result == 0
&& let Some(interpreters) = exec_data.interpreters.as_ref()
{
write!(out, " {} ", "interpreter".purple(),)?;
match interpreters.len() {
0 => {
write!(out, "{}", Interpreter::None)?;
}
1 => {
write!(out, "{}", interpreters[0])?;
}
_ => {
list_printer.begin(out)?;
for (idx, interpreter) in interpreters.iter().enumerate() {
if idx != 0 {
list_printer.comma(out)?;
}
write!(out, "{interpreter}")?;
}
list_printer.end(out)?;
}
}
}
self.print_fd(out, &exec_data.fdinfo)?;
match exec_data.envp.as_ref() {
Ok(envp) => {
match self.args.trace_env {
EnvPrintFormat::Diff => {
write!(out, " {} ", "with".purple())?;
list_printer.begin(out)?;
let env = env.clone();
let mut first_item_written = false;
let mut write_separator = |out: &mut dyn Write| -> io::Result<()> {
if first_item_written {
list_printer.comma(out)?;
} else {
first_item_written = true;
}
Ok(())
};
let diff = diff_env(&env, envp);
for (k, v) in &diff.added {
write_separator(out)?;
write!(
out,
"{}{}{}{}",
"+".bright_green().bold(),
k.cli_escaped_styled(THEME.added_env_var),
"=".bright_green().bold(),
v.cli_escaped_styled(THEME.added_env_var)
)?;
}
for (k, _, v) in diff.modified_with_values(&env) {
write_separator(out)?;
write!(
out,
"{}{}{}{}",
"M".bright_yellow().bold(),
k.cli_escaped_styled(THEME.modified_env_key),
"=".bright_yellow().bold(),
v.cli_escaped_styled(THEME.modified_env_val)
)?;
}
for (k, value) in diff.removed_with_values(&env) {
write_separator(out)?;
write!(
out,
"{}{}{}{}",
"-".bright_red().bold(),
k.cli_escaped_styled(THEME.removed_env_var),
"=".bright_red().strikethrough(),
value.cli_escaped_styled(THEME.removed_env_var)
)?;
}
list_printer.end(out)?;
if owo_colors::control::should_colorize() {
write!(out, "\x1B[49m\x1B[K")?;
}
}
EnvPrintFormat::Raw => {
write!(out, " {} ", "with".purple())?;
list_printer.print_env(out, envp)?;
}
EnvPrintFormat::None => (),
}
}
Err(e) => {
match self.args.trace_env {
EnvPrintFormat::Diff | EnvPrintFormat::Raw => {
write!(
out,
" {} {}",
"with".purple(),
format!("[Failed to read envp: {e}]")
.bright_red()
.blink()
.bold()
)?;
}
EnvPrintFormat::None => {}
}
_deferred_warnings.push(DeferredWarnings {
warning: DeferredWarningKind::FailedReadingEnvp(FriendlyError::InspectError(*e)),
pid,
});
}
}
if self.args.print_cmdline {
write!(out, " {}", "cmdline".purple())?;
write!(out, " env")?;
if self.args.stdio_in_cmdline {
self.print_stdio_fds_in_cmdline(out, &exec_data.fdinfo)?;
}
if self.args.fd_in_cmdline {
for (&fd, fdinfo) in exec_data.fdinfo.fdinfo.iter() {
if fd < 3 {
continue;
}
if fdinfo.flags.ok().is_none() || fdinfo.flags.contains(OFlag::O_CLOEXEC) {
continue;
}
write!(
out,
" {}{}{}",
fd.bright_green().bold(),
"<>".bright_green().bold(),
fdinfo.path.cli_bash_escaped_with_style(THEME.added_fd)
)?;
}
}
match exec_data.argv.as_ref() {
Ok(argv) => {
if let Some(arg0) = argv.first() {
if &exec_data.filename != arg0 {
write!(
out,
" {} {}",
"-a".bright_white().italic(),
escape_str_for_bash!(arg0.as_ref()).bright_white().italic()
)?;
}
} else {
_deferred_warnings.push(DeferredWarnings {
warning: DeferredWarningKind::NoArgv0,
pid,
});
}
if cwd != &exec_data.cwd {
if self.args.color >= ColorLevel::Normal {
write!(
out,
" -C {}",
exec_data.cwd.cli_bash_escaped_with_style(THEME.cwd)
)?;
} else {
write!(out, " -C {}", exec_data.cwd.bash_escaped())?;
}
}
if let Ok(envp) = exec_data.envp.as_ref() {
let diff = diff_env(env, envp);
let need_env_argument_separator =
diff.need_env_argument_separator(&exec_data.filename);
for k in diff.removed.into_iter() {
if self.args.color >= ColorLevel::Normal {
write!(
out,
" {}{}",
"-u ".bright_red(),
k.cli_bash_escaped_with_style(THEME.removed_env_key)
)?;
} else {
write!(out, " -u {}", k.bash_escaped())?;
}
}
if need_env_argument_separator {
write!(out, " --")?;
}
if self.args.color >= ColorLevel::Normal {
for (k, v) in diff.added.into_iter() {
write!(
out,
" {}{}{}",
k.cli_bash_escaped_with_style(THEME.added_env_var),
"=".green().bold(),
v.cli_bash_escaped_with_style(THEME.added_env_var)
)?;
}
for (k, v) in diff.modified.into_iter() {
write!(
out,
" {}{}{}",
k.bash_escaped(),
"=".bright_yellow().bold(),
v.cli_bash_escaped_with_style(THEME.modified_env_val)
)?;
}
} else {
for (k, v) in chain!(diff.added.into_iter(), diff.modified.into_iter()) {
write!(out, " {}={}", k.bash_escaped(), v.bash_escaped())?;
}
}
}
write!(out, " {}", exec_data.filename.bash_escaped())?;
for arg in argv.iter().skip(1) {
write!(out, " {}", arg.bash_escaped())?;
}
}
Err(e) => {
_deferred_warnings.push(DeferredWarnings {
warning: DeferredWarningKind::FailedReadingArgv(FriendlyError::InspectError(*e)),
pid,
});
}
}
}
if result == 0 {
writeln!(out)?;
} else {
write!(out, " {} ", "=".purple())?;
if self.args.decode_errno {
writeln!(
out,
"{} ({})",
result.bright_red().bold(),
nix::errno::Errno::from_raw(-result as i32).red()
)?;
} else {
writeln!(out, "{}", result.bright_red().bold())?;
}
}
out.flush()?;
Ok(())
})
}
}
#[cfg(test)]
mod tests {
use std::{
collections::BTreeMap,
io,
sync::{
Arc,
Mutex,
},
};
use chrono::Local;
use nix::{
errno::Errno,
fcntl::OFlag,
unistd::Pid,
};
use test_that::prelude::*;
use super::*;
use crate::{
proc::{
CgroupInfo,
Cred,
},
timestamp::TimestampFormat,
};
#[derive(Clone, Default)]
struct CaptureWriter(Arc<Mutex<Vec<u8>>>);
impl Write for CaptureWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn msg(value: &str) -> OutputMsg {
OutputMsg::Ok(ArcStr::from(value))
}
fn fd(fd: i32, path: &str, ino: u64, flags: OFlag) -> FileDescriptorInfo {
FileDescriptorInfo {
fd,
path: msg(path),
pos: 0.into(),
flags: flags.into(),
mnt_id: 1.into(),
ino: ino.into(),
mnt: ArcStr::from("mnt"),
extra: Vec::new(),
}
}
fn fd_collection(
entries: impl IntoIterator<Item = FileDescriptorInfo>,
) -> FileDescriptorInfoCollection {
FileDescriptorInfoCollection {
fdinfo: entries.into_iter().map(|info| (info.fd, info)).collect(),
error: None,
}
}
fn baseline() -> Arc<BaselineInfo> {
let mut env = BTreeMap::new();
env.insert(msg("KEEP"), msg("same"));
env.insert(msg("MODIFIED"), msg("old"));
env.insert(msg("REMOVED"), msg("gone"));
Arc::new(BaselineInfo {
cwd: msg("/baseline"),
env,
fdinfo: fd_collection([
fd(0, "/dev/stdin", 10, OFlag::empty()),
fd(1, "/dev/stdout", 11, OFlag::empty()),
fd(2, "/dev/stderr", 12, OFlag::empty()),
]),
})
}
fn printer_args() -> PrinterArgs {
PrinterArgs {
trace_comm: true,
trace_argv: true,
trace_env: EnvPrintFormat::Diff,
trace_fd: FdPrintFormat::Diff,
trace_cwd: true,
print_cmdline: false,
successful_only: false,
trace_interpreter: true,
trace_filename: true,
decode_errno: true,
color: ColorLevel::Less,
stdio_in_cmdline: false,
fd_in_cmdline: false,
hide_cloexec_fds: false,
inline_timestamp_format: None,
}
}
fn exec_data(
argv: Result<Vec<OutputMsg>, Errno>,
envp: Result<BTreeMap<OutputMsg, OutputMsg>, Errno>,
fdinfo: FileDescriptorInfoCollection,
) -> ExecData {
ExecData::new(
Pid::from_raw(123),
msg("/bin/echo"),
argv,
envp,
false,
Ok(Cred::default()),
msg("/exec-cwd"),
Some(vec![
Interpreter::Shebang(ArcStr::from("/usr/bin/env sh")),
Interpreter::None,
]),
fdinfo,
Local::now(),
CgroupInfo::NotCollected,
)
}
fn run_with_output(printer: &Printer, f: impl FnOnce() -> color_eyre::Result<()>) -> String {
let bytes = Arc::new(Mutex::new(Vec::new()));
printer.init_thread_local(Some(Box::new(CaptureWriter(bytes.clone()))));
owo_colors::control::set_should_colorize(false);
f().unwrap();
printer.init_thread_local(None);
String::from_utf8(bytes.lock().unwrap().clone()).unwrap()
}
#[test]
fn list_printer_formats_lists_and_env_maps() {
let list = ListPrinter::new(ColorLevel::Less);
let mut out = Vec::new();
list.print_string_list(&mut out, &["one", "two"]).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), "[one, two]");
let mut env = BTreeMap::new();
env.insert(msg("A"), msg("1"));
env.insert(msg("B"), msg("2"));
let mut out = Vec::new();
list.print_env(&mut out, &env).unwrap();
assert_eq!(
String::from_utf8(out).unwrap(),
"[\"A\"=\"1\", \"B\"=\"2\"]"
);
}
#[test]
fn print_fd_covers_diff_raw_hidden_and_none_modes() {
owo_colors::control::set_should_colorize(false);
let baseline = baseline();
let fds = fd_collection([
fd(1, "/tmp/stdout.log", 20, OFlag::empty()),
fd(2, "/dev/stderr", 12, OFlag::O_CLOEXEC),
fd(3, "/tmp/extra", 30, OFlag::empty()),
fd(4, "/tmp/closed-on-exec", 40, OFlag::O_CLOEXEC),
]);
let mut args = printer_args();
args.trace_fd = FdPrintFormat::Diff;
let printer = Printer::new(args.clone(), baseline.clone());
let mut out = Vec::new();
printer.print_fd(&mut out, &fds).unwrap();
let rendered = String::from_utf8(out).unwrap();
dbg!(&rendered);
assert_that!(rendered, contains_substring("closed: stdin"));
assert_that!(rendered, contains_substring("stdout=\"/tmp/stdout.log\""));
assert_that!(rendered, contains_substring("cloexec: stderr"));
assert_that!(rendered, contains_substring("3=\"/tmp/extra\""));
assert_that!(
rendered,
contains_substring("cloexec: 4=\"/tmp/closed-on-exec\"")
);
args.trace_fd = FdPrintFormat::Raw;
args.hide_cloexec_fds = true;
let printer = Printer::new(args.clone(), baseline.clone());
let mut out = Vec::new();
printer.print_fd(&mut out, &fds).unwrap();
let rendered = String::from_utf8(out).unwrap();
assert_that!(rendered, contains_substring("1=\"/tmp/stdout.log\""));
assert_that!(rendered, not(contains_substring("closed-on-exec")));
args.trace_fd = FdPrintFormat::None;
let printer = Printer::new(args, baseline);
let mut out = Vec::new();
printer.print_fd(&mut out, &fds).unwrap();
assert_that!(out, empty());
}
#[test]
fn print_stdio_fds_in_cmdline_handles_partial_baseline() {
owo_colors::control::set_should_colorize(false);
let mut baseline = baseline();
Arc::make_mut(&mut baseline).fdinfo.fdinfo.remove(&0);
let fds = fd_collection([
fd(0, "/tmp/stdin", 20, OFlag::empty()),
fd(1, "/tmp/stdout", 21, OFlag::empty()),
]);
let printer = Printer::new(printer_args(), baseline);
let mut out = Vec::new();
printer.print_stdio_fds_in_cmdline(&mut out, &fds).unwrap();
assert_eq!(String::from_utf8(out).unwrap(), " >/tmp/stdout 2>&-");
}
#[test]
fn print_exec_trace_renders_diff_env_interpreters_and_errno() {
owo_colors::control::set_should_colorize(false);
let baseline = baseline();
let mut envp = BTreeMap::new();
envp.insert(msg("KEEP"), msg("same"));
envp.insert(msg("MODIFIED"), msg("new"));
envp.insert(msg("ADDED"), msg("value"));
let fds = fd_collection([
fd(0, "/dev/stdin", 10, OFlag::empty()),
fd(1, "/dev/stdout", 11, OFlag::empty()),
fd(2, "/dev/stderr", 12, OFlag::empty()),
]);
let exec = exec_data(
Ok(vec![msg("/bin/echo"), msg("hello world")]),
Ok(envp),
fds,
);
let mut args = printer_args();
args.inline_timestamp_format = Some(TimestampFormat::try_new("%H:%M:%S".to_string()).unwrap());
let printer = Printer::new(args, baseline.clone());
let rendered = run_with_output(&printer, || {
printer.print_exec_trace(
Pid::from_raw(123),
ArcStr::from("echo"),
0,
&exec,
&baseline.env,
&baseline.cwd,
)
});
dbg!(&rendered);
assert_that!(rendered, contains_substring("123<echo>: \"/bin/echo\""));
assert_that!(
rendered,
contains_substring("[\"/bin/echo\", \"hello world\"]")
);
assert_that!(rendered, contains_substring("at \"/exec-cwd\""));
assert_that!(rendered, contains_substring("interpreter"));
assert_that!(rendered, contains_substring("+\"ADDED\"=\"value\""));
assert_that!(rendered, contains_substring("M\"MODIFIED\"=\"new\""));
assert_that!(rendered, contains_substring("-\"REMOVED\"=\"gone\""));
}
#[test]
fn print_exec_trace_renders_raw_env_and_warning_paths() {
owo_colors::control::set_should_colorize(false);
let baseline = baseline();
let fds = fd_collection([
fd(0, "/dev/stdin", 10, OFlag::empty()),
fd(1, "/dev/stdout", 11, OFlag::empty()),
fd(2, "/dev/stderr", 12, OFlag::empty()),
]);
let exec = exec_data(Err(Errno::EACCES), Err(Errno::EPERM), fds);
let mut args = printer_args();
args.trace_env = EnvPrintFormat::Raw;
args.trace_argv = false;
let printer = Printer::new(args, baseline.clone());
let rendered = run_with_output(&printer, || {
printer.print_exec_trace(
Pid::from_raw(321),
ArcStr::from("bad"),
1,
&exec,
&baseline.env,
&baseline.cwd,
)
});
assert_that!(rendered, contains_substring("Failed to read envp"));
assert_that!(rendered, contains_substring("Failed to read argv"));
assert_that!(rendered, contains_substring("warning"));
}
#[test]
fn print_exec_trace_renders_reconstructed_cmdline() {
owo_colors::control::set_should_colorize(false);
let baseline = baseline();
let mut envp = BTreeMap::new();
envp.insert(msg("KEEP"), msg("same"));
envp.insert(msg("MODIFIED"), msg("new value"));
envp.insert(msg("ADDED"), msg("value"));
let fds = fd_collection([
fd(0, "/tmp/stdin", 20, OFlag::empty()),
fd(1, "/tmp/stdout", 21, OFlag::empty()),
fd(2, "/tmp/stderr", 22, OFlag::O_CLOEXEC),
fd(5, "/tmp/fd5", 25, OFlag::empty()),
fd(6, "/tmp/fd6", 26, OFlag::O_CLOEXEC),
]);
let exec = exec_data(
Ok(vec![msg("custom-argv0"), msg("hello world")]),
Ok(envp),
fds,
);
let mut args = printer_args();
args.trace_argv = false;
args.trace_env = EnvPrintFormat::None;
args.trace_fd = FdPrintFormat::None;
args.print_cmdline = true;
args.stdio_in_cmdline = true;
args.fd_in_cmdline = true;
args.trace_interpreter = false;
let printer = Printer::new(args, baseline.clone());
let rendered = run_with_output(&printer, || {
printer.print_exec_trace(
Pid::from_raw(456),
ArcStr::from("echo"),
0,
&exec,
&baseline.env,
&baseline.cwd,
)
});
assert_that!(rendered, contains_substring("cmdline env"));
assert_that!(rendered, contains_substring("</tmp/stdin"));
assert_that!(rendered, contains_substring(">/tmp/stdout"));
assert_that!(rendered, contains_substring("2>&-"));
assert_that!(rendered, contains_substring("5<>/tmp/fd5"));
assert_that!(rendered, contains_substring("-a custom-argv0"));
assert_that!(rendered, contains_substring("-C /exec-cwd"));
assert_that!(rendered, contains_substring("-u REMOVED"));
assert_that!(rendered, contains_substring("ADDED=value"));
assert_that!(rendered, contains_substring("MODIFIED=$'new value'"));
assert_that!(rendered, contains_substring("/bin/echo $'hello world'"));
}
#[test]
fn reconstructed_cmdline_separates_dash_filename_when_env_is_unchanged() {
owo_colors::control::set_should_colorize(false);
let baseline = baseline();
let filename = msg("--ignore-signal");
let mut exec = exec_data(
Ok(vec![filename.clone()]),
Ok(baseline.env.clone()),
FileDescriptorInfoCollection::default(),
);
exec.filename = filename;
exec.cwd = baseline.cwd.clone();
let mut args = printer_args();
args.trace_argv = false;
args.trace_env = EnvPrintFormat::None;
args.trace_fd = FdPrintFormat::None;
args.trace_cwd = false;
args.trace_filename = false;
args.trace_interpreter = false;
args.print_cmdline = true;
let printer = Printer::new(args, baseline.clone());
let rendered = run_with_output(&printer, || {
printer.print_exec_trace(
Pid::from_raw(456),
ArcStr::from("false"),
0,
&exec,
&baseline.env,
&baseline.cwd,
)
});
assert_that!(
rendered,
contains_substring("cmdline env -- --ignore-signal")
);
}
#[test]
fn print_new_child_respects_missing_output_and_comm_flag() {
owo_colors::control::set_should_colorize(false);
let baseline = baseline();
let mut args = printer_args();
args.trace_comm = false;
let printer = Printer::new(args, baseline);
printer.init_thread_local(None);
printer
.print_new_child(Pid::from_raw(1), "ignored", Pid::from_raw(2))
.unwrap();
let rendered = run_with_output(&printer, || {
printer.print_new_child(Pid::from_raw(1), "ignored", Pid::from_raw(2))
});
assert_eq!(rendered, "1: new child: 2\n");
}
}