use super::StderrMode;
use super::command_env::env_names_eq;
use super::redaction::{CommandArg, redact_sensitive_args};
use camino::Utf8PathBuf;
use std::{
ffi::OsStr,
io,
path::PathBuf,
process::{Command, ExitStatus},
};
use tracing::{field, info, info_span, warn};
pub(super) struct CommandLogContext {
pub(super) program_display: String,
redacted_command: String,
arg_count: usize,
env_override_count: usize,
is_path_overridden: bool,
}
fn summarize_env_overrides(cmd: &Command) -> (usize, bool) {
let mut count = 0usize;
let mut is_path_overridden = false;
for (key, _) in cmd.get_envs() {
count += 1;
is_path_overridden |= env_names_eq(key, OsStr::new("PATH"));
}
(count, is_path_overridden)
}
impl CommandLogContext {
pub(super) fn from_command(cmd: &Command) -> Self {
let program_path = PathBuf::from(cmd.get_program());
let program_display = match Utf8PathBuf::from_path_buf(program_path) {
Ok(path) => path.into_string(),
Err(path) => path.to_string_lossy().into_owned(),
};
let args: Vec<CommandArg> = cmd
.get_args()
.map(|a| CommandArg::new(a.to_string_lossy().into_owned()))
.collect();
let redacted_args = redact_sensitive_args(&args);
let arg_count = redacted_args.len();
let arg_strings: Vec<&str> = redacted_args.iter().map(CommandArg::as_str).collect();
let redacted_command = format!("{} {}", program_display, arg_strings.join(" "));
let (env_override_count, is_path_overridden) = summarize_env_overrides(cmd);
Self {
program_display,
redacted_command,
arg_count,
env_override_count,
is_path_overridden,
}
}
}
pub(super) fn log_command_execution(
context: &CommandLogContext,
operation: &str,
stderr_mode: StderrMode,
) {
info!(
operation,
ninja_program = %context.program_display,
arg_count = context.arg_count,
env_override_count = context.env_override_count,
path_overridden = context.is_path_overridden,
suppress_stderr = stderr_mode.is_suppress(),
"Executing command: {}",
context.redacted_command,
);
}
pub(super) fn log_command_spawn_failure(
context: &CommandLogContext,
operation: &str,
stderr_mode: StderrMode,
err: &io::Error,
) {
warn!(
operation,
ninja_program = %context.program_display,
env_override_count = context.env_override_count,
path_overridden = context.is_path_overridden,
suppress_stderr = stderr_mode.is_suppress(),
failure_category = "spawn",
error.kind = ?err.kind(),
error = %err,
"Ninja command failed to spawn",
);
}
pub(super) fn log_command_exit_failure(
context: &CommandLogContext,
operation: &str,
stderr_mode: StderrMode,
status: ExitStatus,
) {
warn!(
operation,
ninja_program = %context.program_display,
env_override_count = context.env_override_count,
path_overridden = context.is_path_overridden,
suppress_stderr = stderr_mode.is_suppress(),
failure_category = "exit_status",
%status,
"Ninja command exited unsuccessfully",
);
}
pub(super) fn command_span(
context: &CommandLogContext,
operation: &str,
stderr_mode: StderrMode,
) -> tracing::Span {
info_span!(
"ninja_subprocess",
operation,
ninja_program = %context.program_display,
arg_count = context.arg_count,
env_override_count = context.env_override_count,
path_overridden = context.is_path_overridden,
suppress_stderr = stderr_mode.is_suppress(),
failure_category = field::Empty,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runner::CommandEnv;
use rstest::rstest;
#[rstest]
#[case(CommandEnv::inherit(), 0, false)]
#[case(CommandEnv::inherit().with_var("NINJA_STATUS", "[%f/%t] "), 1, false)]
#[case(CommandEnv::inherit().with_path("/opt/toolchain/bin"), 1, true)]
#[case(
CommandEnv::inherit()
.with_var("NINJA_STATUS", "[%f/%t] ")
.with_path("/opt/toolchain/bin"),
2,
true
)]
fn from_command_summarizes_env_overrides(
#[case] env: CommandEnv,
#[case] expected_count: usize,
#[case] expected_path_overridden: bool,
) {
let mut cmd = Command::new("ninja");
env.apply(&mut cmd);
let context = CommandLogContext::from_command(&cmd);
assert_eq!(context.env_override_count, expected_count);
assert_eq!(context.is_path_overridden, expected_path_overridden);
}
#[cfg(unix)]
#[test]
fn mixed_case_path_is_not_a_path_override_on_unix() {
let mut cmd = Command::new("ninja");
CommandEnv::inherit()
.with_var("Path", "/mixed/case")
.apply(&mut cmd);
let context = CommandLogContext::from_command(&cmd);
assert_eq!(context.env_override_count, 1);
assert!(
!context.is_path_overridden,
"a Unix variable named `Path` is not `PATH`"
);
}
#[cfg(windows)]
#[test]
fn mixed_case_path_is_a_path_override_on_windows() {
let mut cmd = Command::new("ninja");
CommandEnv::inherit()
.with_var("Path", "C:\\mixed")
.apply(&mut cmd);
let context = CommandLogContext::from_command(&cmd);
assert_eq!(context.env_override_count, 1);
assert!(
context.is_path_overridden,
"Windows resolves `Path` and `PATH` to one variable"
);
}
#[cfg(unix)]
#[test]
fn from_command_uses_lossy_display_for_non_utf8_program() {
use std::os::unix::ffi::OsStringExt;
let cmd = Command::new(std::ffi::OsString::from_vec(b"ninja-\xff".to_vec()));
let context = CommandLogContext::from_command(&cmd);
assert_eq!(context.program_display, "ninja-\u{fffd}");
}
}