Mercurial > hg
comparison rust/rhg/src/blackbox.rs @ 46601:755c31a1caf9
rhg: Add support for the blackbox extension
Only `command` and `commandfinish` events are logged.
The `dirty`, `logsource`, `track` and `ignore` configuration items
are not supported yet.
To indicate commands executed without Python, a `(rust) ` prefix is added
in corresponding log messages.
Differential Revision: https://phab.mercurial-scm.org/D10012
author | Simon Sapin <simon.sapin@octobus.net> |
---|---|
date | Tue, 16 Feb 2021 13:08:37 +0100 |
parents | |
children | 7284b524b441 |
comparison
equal
deleted
inserted
replaced
46600:36f3a64846c8 | 46601:755c31a1caf9 |
---|---|
1 //! Logging for repository events, including commands run in the repository. | |
2 | |
3 use crate::CliInvocation; | |
4 use format_bytes::format_bytes; | |
5 use hg::errors::HgError; | |
6 use hg::repo::Repo; | |
7 use hg::utils::{files::get_bytes_from_os_str, shell_quote}; | |
8 | |
9 const ONE_MEBIBYTE: u64 = 1 << 20; | |
10 | |
11 // TODO: somehow keep defaults in sync with `configitem` in `hgext/blackbox.py` | |
12 const DEFAULT_MAX_SIZE: u64 = ONE_MEBIBYTE; | |
13 const DEFAULT_MAX_FILES: u32 = 7; | |
14 | |
15 // Python does not support %.3f, only %f | |
16 const DEFAULT_DATE_FORMAT: &str = "%Y/%m/%d %H:%M:%S%.3f"; | |
17 | |
18 type DateTime = chrono::DateTime<chrono::Local>; | |
19 | |
20 pub struct ProcessStartTime { | |
21 /// For measuring duration | |
22 monotonic_clock: std::time::Instant, | |
23 /// For formatting with year, month, day, etc. | |
24 calendar_based: DateTime, | |
25 } | |
26 | |
27 impl ProcessStartTime { | |
28 pub fn now() -> Self { | |
29 Self { | |
30 monotonic_clock: std::time::Instant::now(), | |
31 calendar_based: chrono::Local::now(), | |
32 } | |
33 } | |
34 } | |
35 | |
36 pub struct Blackbox<'a> { | |
37 process_start_time: &'a ProcessStartTime, | |
38 /// Do nothing if this is `None` | |
39 configured: Option<ConfiguredBlackbox<'a>>, | |
40 } | |
41 | |
42 struct ConfiguredBlackbox<'a> { | |
43 repo: &'a Repo, | |
44 max_size: u64, | |
45 max_files: u32, | |
46 date_format: &'a str, | |
47 } | |
48 | |
49 impl<'a> Blackbox<'a> { | |
50 pub fn new( | |
51 invocation: &'a CliInvocation<'a>, | |
52 process_start_time: &'a ProcessStartTime, | |
53 ) -> Result<Self, HgError> { | |
54 let configured = if let Ok(repo) = invocation.repo { | |
55 let config = invocation.config(); | |
56 if config.get(b"extensions", b"blackbox").is_none() { | |
57 // The extension is not enabled | |
58 None | |
59 } else { | |
60 Some(ConfiguredBlackbox { | |
61 repo, | |
62 max_size: config | |
63 .get_byte_size(b"blackbox", b"maxsize")? | |
64 .unwrap_or(DEFAULT_MAX_SIZE), | |
65 max_files: config | |
66 .get_u32(b"blackbox", b"maxfiles")? | |
67 .unwrap_or(DEFAULT_MAX_FILES), | |
68 date_format: config | |
69 .get_str(b"blackbox", b"date-format")? | |
70 .unwrap_or(DEFAULT_DATE_FORMAT), | |
71 }) | |
72 } | |
73 } else { | |
74 // Without a local repository there’s no `.hg/blackbox.log` to | |
75 // write to. | |
76 None | |
77 }; | |
78 Ok(Self { | |
79 process_start_time, | |
80 configured, | |
81 }) | |
82 } | |
83 | |
84 pub fn log_command_start(&self) { | |
85 if let Some(configured) = &self.configured { | |
86 let message = format_bytes!(b"(rust) {}", format_cli_args()); | |
87 configured.log(&self.process_start_time.calendar_based, &message); | |
88 } | |
89 } | |
90 | |
91 pub fn log_command_end(&self, exit_code: i32) { | |
92 if let Some(configured) = &self.configured { | |
93 let now = chrono::Local::now(); | |
94 let duration = self | |
95 .process_start_time | |
96 .monotonic_clock | |
97 .elapsed() | |
98 .as_secs_f64(); | |
99 let message = format_bytes!( | |
100 b"(rust) {} exited {} after {} seconds", | |
101 format_cli_args(), | |
102 exit_code, | |
103 format_bytes::Utf8(format_args!("{:.03}", duration)) | |
104 ); | |
105 configured.log(&now, &message); | |
106 } | |
107 } | |
108 } | |
109 | |
110 impl ConfiguredBlackbox<'_> { | |
111 fn log(&self, date_time: &DateTime, message: &[u8]) { | |
112 let date = format_bytes::Utf8(date_time.format(self.date_format)); | |
113 let user = users::get_current_username().map(get_bytes_from_os_str); | |
114 let user = user.as_deref().unwrap_or(b"???"); | |
115 let rev = format_bytes::Utf8(match self.repo.dirstate_parents() { | |
116 Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => { | |
117 format!("{:x}", parents.p1) | |
118 } | |
119 Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2), | |
120 Err(_dirstate_corruption_error) => { | |
121 // TODO: log a non-fatal warning to stderr | |
122 "???".to_owned() | |
123 } | |
124 }); | |
125 let pid = std::process::id(); | |
126 let line = format_bytes!( | |
127 b"{} {} @{} ({})> {}\n", | |
128 date, | |
129 user, | |
130 rev, | |
131 pid, | |
132 message | |
133 ); | |
134 let result = | |
135 hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log") | |
136 .max_size(Some(self.max_size)) | |
137 .max_files(self.max_files) | |
138 .write(&line); | |
139 match result { | |
140 Ok(()) => {} | |
141 Err(_io_error) => { | |
142 // TODO: log a non-fatal warning to stderr | |
143 } | |
144 } | |
145 } | |
146 } | |
147 | |
148 fn format_cli_args() -> Vec<u8> { | |
149 let mut args = std::env::args_os(); | |
150 let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable | |
151 let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg))); | |
152 let mut formatted = Vec::new(); | |
153 if let Some(arg) = args.next() { | |
154 formatted.extend(arg) | |
155 } | |
156 for arg in args { | |
157 formatted.push(b' '); | |
158 formatted.extend(arg) | |
159 } | |
160 formatted | |
161 } |