rust/rhg/src/blackbox.rs
author Raphaël Gomès <rgomes@octobus.net>
Thu, 26 Sep 2024 13:55:26 +0200
changeset 52169 84b5802ba7d3
parent 50766 58aa5ee9c846
permissions -rw-r--r--
rust: populate mmap by default if available See 522b4d729e89edc76544fa549ed36de4aea0b7fb for more details. Background population to follow in a later patch.

//! 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};
use std::ffi::OsString;

// Python does not support %.3f, only %f
const DEFAULT_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.3f";

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 {
            if !invocation.config.is_extension_enabled(b"blackbox") {
                None
            } else {
                Some(ConfiguredBlackbox {
                    repo,
                    max_size: invocation
                        .config
                        .get_byte_size(b"blackbox", b"maxsize")?
                        .expect(
                            "blackbox.maxsize should have a default value",
                        ),
                    max_files: invocation
                        .config
                        .get_u32(b"blackbox", b"maxfiles")?
                        .expect(
                            "blackbox.maxfiles should have a default value",
                        ),
                    date_format: invocation
                        .config
                        .get_str(b"blackbox", b"date-format")?
                        .map(|f| {
                            if f.is_empty() {
                                DEFAULT_DATE_FORMAT
                            } else {
                                f
                            }
                        })
                        .expect(
                            "blackbox.date-format should have a default value",
                        ),
                })
            }
        } else {
            // Without a local repository there’s no `.hg/blackbox.log` to
            // write to.
            None
        };
        Ok(Self {
            process_start_time,
            configured,
        })
    }

    pub fn log_command_start<'arg>(
        &self,
        argv: impl Iterator<Item = &'arg OsString>,
    ) {
        if let Some(configured) = &self.configured {
            let message = format_bytes!(b"(rust) {}", format_cli_args(argv));
            configured.log(&self.process_start_time.calendar_based, &message);
        }
    }

    pub fn log_command_end<'arg>(
        &self,
        argv: impl Iterator<Item = &'arg OsString>,
        exit_code: i32,
    ) {
        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",
                format_cli_args(argv),
                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));
        let user = get_bytes_from_os_str(whoami::username_os());
        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
            }
        }
    }
}

fn format_cli_args<'a>(
    mut args: impl Iterator<Item = &'a OsString>,
) -> Vec<u8> {
    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
}