blackbox.rs
178 lines
| 5.5 KiB
| application/rls-services+xml
|
RustLexer
|
r47343 | //! Logging for repository events, including commands run in the repository. | ||
use crate::CliInvocation; | ||||
use format_bytes::format_bytes; | ||||
use hg::errors::HgError; | ||||
use hg::repo::Repo; | ||||
use hg::utils::{files::get_bytes_from_os_str, shell_quote}; | ||||
|
r49961 | use std::ffi::OsString; | ||
|
r47343 | |||
// Python does not support %.3f, only %f | ||||
|
r49421 | const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.3f"; | ||
|
r47343 | |||
type DateTime = chrono::DateTime<chrono::Local>; | ||||
pub struct ProcessStartTime { | ||||
/// For measuring duration | ||||
monotonic_clock: std::time::Instant, | ||||
/// For formatting with year, month, day, etc. | ||||
calendar_based: DateTime, | ||||
} | ||||
impl ProcessStartTime { | ||||
pub fn now() -> Self { | ||||
Self { | ||||
monotonic_clock: std::time::Instant::now(), | ||||
calendar_based: chrono::Local::now(), | ||||
} | ||||
} | ||||
} | ||||
pub struct Blackbox<'a> { | ||||
process_start_time: &'a ProcessStartTime, | ||||
/// Do nothing if this is `None` | ||||
configured: Option<ConfiguredBlackbox<'a>>, | ||||
} | ||||
struct ConfiguredBlackbox<'a> { | ||||
repo: &'a Repo, | ||||
max_size: u64, | ||||
max_files: u32, | ||||
date_format: &'a str, | ||||
} | ||||
impl<'a> Blackbox<'a> { | ||||
pub fn new( | ||||
invocation: &'a CliInvocation<'a>, | ||||
process_start_time: &'a ProcessStartTime, | ||||
) -> Result<Self, HgError> { | ||||
let configured = if let Ok(repo) = invocation.repo { | ||||
|
r47423 | if invocation.config.get(b"extensions", b"blackbox").is_none() { | ||
|
r47343 | // The extension is not enabled | ||
None | ||||
} else { | ||||
Some(ConfiguredBlackbox { | ||||
repo, | ||||
|
r47423 | max_size: invocation | ||
.config | ||||
|
r47343 | .get_byte_size(b"blackbox", b"maxsize")? | ||
|
r51658 | .expect( | ||
"blackbox.maxsize should have a default value", | ||||
), | ||||
|
r47423 | max_files: invocation | ||
.config | ||||
|
r47343 | .get_u32(b"blackbox", b"maxfiles")? | ||
|
r51658 | .expect( | ||
"blackbox.maxfiles should have a default value", | ||||
), | ||||
|
r47423 | date_format: invocation | ||
.config | ||||
|
r47343 | .get_str(b"blackbox", b"date-format")? | ||
|
r51658 | .map(|f| { | ||
if f.is_empty() { | ||||
DEFAULT_DATE_FORMAT | ||||
} else { | ||||
f | ||||
} | ||||
}) | ||||
.expect( | ||||
"blackbox.date-format should have a default value", | ||||
), | ||||
|
r47343 | }) | ||
} | ||||
} else { | ||||
// Without a local repository there’s no `.hg/blackbox.log` to | ||||
// write to. | ||||
None | ||||
}; | ||||
Ok(Self { | ||||
process_start_time, | ||||
configured, | ||||
}) | ||||
} | ||||
|
r49961 | pub fn log_command_start<'arg>( | ||
&self, | ||||
argv: impl Iterator<Item = &'arg OsString>, | ||||
) { | ||||
|
r47343 | if let Some(configured) = &self.configured { | ||
|
r49961 | let message = format_bytes!(b"(rust) {}", format_cli_args(argv)); | ||
|
r47343 | configured.log(&self.process_start_time.calendar_based, &message); | ||
} | ||||
} | ||||
|
r49961 | pub fn log_command_end<'arg>( | ||
&self, | ||||
argv: impl Iterator<Item = &'arg OsString>, | ||||
exit_code: i32, | ||||
) { | ||||
|
r47343 | if let Some(configured) = &self.configured { | ||
let now = chrono::Local::now(); | ||||
let duration = self | ||||
.process_start_time | ||||
.monotonic_clock | ||||
.elapsed() | ||||
.as_secs_f64(); | ||||
let message = format_bytes!( | ||||
b"(rust) {} exited {} after {} seconds", | ||||
|
r49961 | format_cli_args(argv), | ||
|
r47343 | exit_code, | ||
format_bytes::Utf8(format_args!("{:.03}", duration)) | ||||
); | ||||
configured.log(&now, &message); | ||||
} | ||||
} | ||||
} | ||||
impl ConfiguredBlackbox<'_> { | ||||
fn log(&self, date_time: &DateTime, message: &[u8]) { | ||||
let date = format_bytes::Utf8(date_time.format(self.date_format)); | ||||
|
r51576 | let user = get_bytes_from_os_str(whoami::username_os()); | ||
|
r47343 | let rev = format_bytes::Utf8(match self.repo.dirstate_parents() { | ||
Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => { | ||||
format!("{:x}", parents.p1) | ||||
} | ||||
Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2), | ||||
Err(_dirstate_corruption_error) => { | ||||
// TODO: log a non-fatal warning to stderr | ||||
"???".to_owned() | ||||
} | ||||
}); | ||||
let pid = std::process::id(); | ||||
let line = format_bytes!( | ||||
b"{} {} @{} ({})> {}\n", | ||||
date, | ||||
user, | ||||
rev, | ||||
pid, | ||||
message | ||||
); | ||||
let result = | ||||
hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log") | ||||
.max_size(Some(self.max_size)) | ||||
.max_files(self.max_files) | ||||
.write(&line); | ||||
match result { | ||||
Ok(()) => {} | ||||
Err(_io_error) => { | ||||
// TODO: log a non-fatal warning to stderr | ||||
} | ||||
} | ||||
} | ||||
} | ||||
|
r49961 | fn format_cli_args<'a>( | ||
mut args: impl Iterator<Item = &'a OsString>, | ||||
) -> Vec<u8> { | ||||
|
r47343 | let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable | ||
let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg))); | ||||
let mut formatted = Vec::new(); | ||||
if let Some(arg) = args.next() { | ||||
formatted.extend(arg) | ||||
} | ||||
for arg in args { | ||||
formatted.push(b' '); | ||||
formatted.extend(arg) | ||||
} | ||||
formatted | ||||
} | ||||