##// END OF EJS Templates
rhg: Add support for the blackbox extension...
Simon Sapin -
r47343:755c31a1 default
parent child Browse files
Show More
@@ -0,0 +1,161 b''
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 }
@@ -91,6 +91,18 b' version = "1.0.0"'
91 91 source = "registry+https://github.com/rust-lang/crates.io-index"
92 92
93 93 [[package]]
94 name = "chrono"
95 version = "0.4.19"
96 source = "registry+https://github.com/rust-lang/crates.io-index"
97 dependencies = [
98 "libc 0.2.81 (registry+https://github.com/rust-lang/crates.io-index)",
99 "num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
100 "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
101 "time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
102 "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
103 ]
104
105 [[package]]
94 106 name = "clap"
95 107 version = "2.33.3"
96 108 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -467,6 +479,15 b' dependencies = ['
467 479 ]
468 480
469 481 [[package]]
482 name = "num-integer"
483 version = "0.1.44"
484 source = "registry+https://github.com/rust-lang/crates.io-index"
485 dependencies = [
486 "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
487 "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
488 ]
489
490 [[package]]
470 491 name = "num-traits"
471 492 version = "0.2.14"
472 493 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -715,6 +736,7 b' dependencies = ['
715 736 name = "rhg"
716 737 version = "0.1.0"
717 738 dependencies = [
739 "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)",
718 740 "clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)",
719 741 "derive_more 0.99.11 (registry+https://github.com/rust-lang/crates.io-index)",
720 742 "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -722,6 +744,7 b' dependencies = ['
722 744 "hg-core 0.1.0",
723 745 "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
724 746 "micro-timer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
747 "users 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
725 748 ]
726 749
727 750 [[package]]
@@ -856,6 +879,15 b' version = "0.2.1"'
856 879 source = "registry+https://github.com/rust-lang/crates.io-index"
857 880
858 881 [[package]]
882 name = "users"
883 version = "0.11.0"
884 source = "registry+https://github.com/rust-lang/crates.io-index"
885 dependencies = [
886 "libc 0.2.81 (registry+https://github.com/rust-lang/crates.io-index)",
887 "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
888 ]
889
890 [[package]]
859 891 name = "vcpkg"
860 892 version = "0.2.11"
861 893 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -949,6 +981,7 b' dependencies = ['
949 981 "checksum cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)" = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
950 982 "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
951 983 "checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
984 "checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
952 985 "checksum clap 2.33.3 (registry+https://github.com/rust-lang/crates.io-index)" = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
953 986 "checksum const_fn 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
954 987 "checksum cpython 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bfaf3847ab963e40c4f6dd8d6be279bdf74007ae2413786a0dcbb28c52139a95"
@@ -988,6 +1021,7 b' dependencies = ['
988 1021 "checksum micro-timer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2620153e1d903d26b72b89f0e9c48d8c4756cba941c185461dddc234980c298c"
989 1022 "checksum micro-timer-macros 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e28a3473e6abd6e9aab36aaeef32ad22ae0bd34e79f376643594c2b152ec1c5d"
990 1023 "checksum miniz_oxide 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0f2d26ec3309788e423cfbf68ad1800f061638098d76a83681af979dc4eda19d"
1024 "checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
991 1025 "checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
992 1026 "checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
993 1027 "checksum output_vt100 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "53cdc5b785b7a58c5aad8216b3dfa114df64b0b06ae6e1501cef91df2fbdf8f9"
@@ -1035,6 +1069,7 b' dependencies = ['
1035 1069 "checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
1036 1070 "checksum unicode-width 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
1037 1071 "checksum unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
1072 "checksum users 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
1038 1073 "checksum vcpkg 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
1039 1074 "checksum vec_map 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
1040 1075 "checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
@@ -26,6 +26,14 b" type ParseResult<'a> = ("
26 26 Vec<(&'a HgPath, &'a HgPath)>,
27 27 );
28 28
29 pub fn parse_dirstate_parents(
30 contents: &[u8],
31 ) -> Result<&DirstateParents, HgError> {
32 let (parents, _rest) = DirstateParents::from_bytes(contents)
33 .map_err(|_| HgError::corrupted("Too little data for dirstate."))?;
34 Ok(parents)
35 }
36
29 37 #[timed]
30 38 pub fn parse_dirstate(mut contents: &[u8]) -> Result<ParseResult, HgError> {
31 39 let mut copies = Vec::new();
@@ -214,6 +214,15 b' impl Repo {'
214 214 base: &self.working_directory,
215 215 }
216 216 }
217
218 pub fn dirstate_parents(
219 &self,
220 ) -> Result<crate::dirstate::DirstateParents, HgError> {
221 let dirstate = self.hg_vfs().mmap_open("dirstate")?;
222 let parents =
223 crate::dirstate::parsers::parse_dirstate_parents(&dirstate)?;
224 Ok(parents.clone())
225 }
217 226 }
218 227
219 228 impl Vfs<'_> {
@@ -188,6 +188,35 b" pub(crate) fn strip_suffix<'a>(s: &'a st"
188 188 }
189 189 }
190 190
191 #[cfg(unix)]
192 pub fn shell_quote(value: &[u8]) -> Vec<u8> {
193 // TODO: Use the `matches!` macro when we require Rust 1.42+
194 if value.iter().all(|&byte| match byte {
195 b'a'..=b'z'
196 | b'A'..=b'Z'
197 | b'0'..=b'9'
198 | b'.'
199 | b'_'
200 | b'/'
201 | b'+'
202 | b'-' => true,
203 _ => false,
204 }) {
205 value.to_owned()
206 } else {
207 let mut quoted = Vec::with_capacity(value.len() + 2);
208 quoted.push(b'\'');
209 for &byte in value {
210 if byte == b'\'' {
211 quoted.push(b'\\');
212 }
213 quoted.push(byte);
214 }
215 quoted.push(b'\'');
216 quoted
217 }
218 }
219
191 220 pub fn current_dir() -> Result<std::path::PathBuf, HgError> {
192 221 std::env::current_dir().map_err(|error| HgError::IoError {
193 222 error,
@@ -9,9 +9,11 b' edition = "2018"'
9 9
10 10 [dependencies]
11 11 hg-core = { path = "../hg-core"}
12 chrono = "0.4.19"
12 13 clap = "2.33.1"
13 14 derive_more = "0.99"
14 15 log = "0.4.11"
15 16 micro-timer = "0.3.1"
16 17 env_logger = "0.7.1"
17 18 format-bytes = "0.2.0"
19 users = "0.11.0"
@@ -9,6 +9,7 b' use hg::config::Config;'
9 9 use hg::repo::{Repo, RepoError};
10 10 use std::path::{Path, PathBuf};
11 11
12 mod blackbox;
12 13 mod error;
13 14 mod exitcode;
14 15 mod ui;
@@ -36,7 +37,10 b" fn add_global_args<'a, 'b>(app: App<'a, "
36 37 )
37 38 }
38 39
39 fn main_with_result(ui: &ui::Ui) -> Result<(), CommandError> {
40 fn main_with_result(
41 ui: &ui::Ui,
42 process_start_time: &blackbox::ProcessStartTime,
43 ) -> Result<(), CommandError> {
40 44 env_logger::init();
41 45 let app = App::new("rhg")
42 46 .setting(AppSettings::AllowInvalidUtf8)
@@ -83,35 +87,47 b' fn main_with_result(ui: &ui::Ui) -> Resu'
83 87 Err(error) => return Err(error.into()),
84 88 };
85 89
86 run(&CliInvocation {
90 let invocation = CliInvocation {
87 91 ui,
88 92 subcommand_args,
89 93 non_repo_config,
90 94 repo: repo.as_ref(),
91 })
95 };
96 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
97 blackbox.log_command_start();
98 let result = run(&invocation);
99 blackbox.log_command_end(exit_code(&result));
100 result
92 101 }
93 102
94 103 fn main() {
104 // Run this first, before we find out if the blackbox extension is even
105 // enabled, in order to include everything in-between in the duration
106 // measurements. Reading config files can be slow if they’re on NFS.
107 let process_start_time = blackbox::ProcessStartTime::now();
108
95 109 let ui = ui::Ui::new();
96 110
97 let exit_code = match main_with_result(&ui) {
111 let result = main_with_result(&ui, &process_start_time);
112 if let Err(CommandError::Abort { message }) = &result {
113 if !message.is_empty() {
114 // Ignore errors when writing to stderr, we’re already exiting
115 // with failure code so there’s not much more we can do.
116 let _ = ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
117 }
118 }
119 std::process::exit(exit_code(&result))
120 }
121
122 fn exit_code(result: &Result<(), CommandError>) -> i32 {
123 match result {
98 124 Ok(()) => exitcode::OK,
125 Err(CommandError::Abort { .. }) => exitcode::ABORT,
99 126
100 127 // Exit with a specific code and no error message to let a potential
101 128 // wrapper script fallback to Python-based Mercurial.
102 129 Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED,
103
104 Err(CommandError::Abort { message }) => {
105 if !message.is_empty() {
106 // Ignore errors when writing to stderr, we’re already exiting
107 // with failure code so there’s not much more we can do.
108 let _ =
109 ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
110 }
111 exitcode::ABORT
112 }
113 };
114 std::process::exit(exit_code)
130 }
115 131 }
116 132
117 133 macro_rules! subcommands {
@@ -261,3 +261,15 b' And check that basic rhg commands work w'
261 261 $ rhg cat -r 0 a
262 262 a
263 263
264 The blackbox extension is supported
265
266 $ echo "[extensions]" >> $HGRCPATH
267 $ echo "blackbox =" >> $HGRCPATH
268 $ echo "[blackbox]" >> $HGRCPATH
269 $ echo "maxsize = 1" >> $HGRCPATH
270 $ rhg files > /dev/null
271 $ cat .hg/blackbox.log
272 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files exited 0 after 0.??? seconds (glob)
273 $ cat .hg/blackbox.log.1
274 ????/??/?? ??:??:??.??? * @d3873e73d99ef67873dac33fbcc66268d5d2b6f4 (*)> (rust) files (glob)
275
General Comments 0
You need to be logged in to leave comments. Login now