##// END OF EJS Templates
rhg: Make configuration available as early as possible in main()...
Simon Sapin -
r47423:7284b524 default
parent child Browse files
Show More
@@ -1,161 +1,163 b''
1 1 //! Logging for repository events, including commands run in the repository.
2 2
3 3 use crate::CliInvocation;
4 4 use format_bytes::format_bytes;
5 5 use hg::errors::HgError;
6 6 use hg::repo::Repo;
7 7 use hg::utils::{files::get_bytes_from_os_str, shell_quote};
8 8
9 9 const ONE_MEBIBYTE: u64 = 1 << 20;
10 10
11 11 // TODO: somehow keep defaults in sync with `configitem` in `hgext/blackbox.py`
12 12 const DEFAULT_MAX_SIZE: u64 = ONE_MEBIBYTE;
13 13 const DEFAULT_MAX_FILES: u32 = 7;
14 14
15 15 // Python does not support %.3f, only %f
16 16 const DEFAULT_DATE_FORMAT: &str = "%Y/%m/%d %H:%M:%S%.3f";
17 17
18 18 type DateTime = chrono::DateTime<chrono::Local>;
19 19
20 20 pub struct ProcessStartTime {
21 21 /// For measuring duration
22 22 monotonic_clock: std::time::Instant,
23 23 /// For formatting with year, month, day, etc.
24 24 calendar_based: DateTime,
25 25 }
26 26
27 27 impl ProcessStartTime {
28 28 pub fn now() -> Self {
29 29 Self {
30 30 monotonic_clock: std::time::Instant::now(),
31 31 calendar_based: chrono::Local::now(),
32 32 }
33 33 }
34 34 }
35 35
36 36 pub struct Blackbox<'a> {
37 37 process_start_time: &'a ProcessStartTime,
38 38 /// Do nothing if this is `None`
39 39 configured: Option<ConfiguredBlackbox<'a>>,
40 40 }
41 41
42 42 struct ConfiguredBlackbox<'a> {
43 43 repo: &'a Repo,
44 44 max_size: u64,
45 45 max_files: u32,
46 46 date_format: &'a str,
47 47 }
48 48
49 49 impl<'a> Blackbox<'a> {
50 50 pub fn new(
51 51 invocation: &'a CliInvocation<'a>,
52 52 process_start_time: &'a ProcessStartTime,
53 53 ) -> Result<Self, HgError> {
54 54 let configured = if let Ok(repo) = invocation.repo {
55 let config = invocation.config();
56 if config.get(b"extensions", b"blackbox").is_none() {
55 if invocation.config.get(b"extensions", b"blackbox").is_none() {
57 56 // The extension is not enabled
58 57 None
59 58 } else {
60 59 Some(ConfiguredBlackbox {
61 60 repo,
62 max_size: config
61 max_size: invocation
62 .config
63 63 .get_byte_size(b"blackbox", b"maxsize")?
64 64 .unwrap_or(DEFAULT_MAX_SIZE),
65 max_files: config
65 max_files: invocation
66 .config
66 67 .get_u32(b"blackbox", b"maxfiles")?
67 68 .unwrap_or(DEFAULT_MAX_FILES),
68 date_format: config
69 date_format: invocation
70 .config
69 71 .get_str(b"blackbox", b"date-format")?
70 72 .unwrap_or(DEFAULT_DATE_FORMAT),
71 73 })
72 74 }
73 75 } else {
74 76 // Without a local repository there’s no `.hg/blackbox.log` to
75 77 // write to.
76 78 None
77 79 };
78 80 Ok(Self {
79 81 process_start_time,
80 82 configured,
81 83 })
82 84 }
83 85
84 86 pub fn log_command_start(&self) {
85 87 if let Some(configured) = &self.configured {
86 88 let message = format_bytes!(b"(rust) {}", format_cli_args());
87 89 configured.log(&self.process_start_time.calendar_based, &message);
88 90 }
89 91 }
90 92
91 93 pub fn log_command_end(&self, exit_code: i32) {
92 94 if let Some(configured) = &self.configured {
93 95 let now = chrono::Local::now();
94 96 let duration = self
95 97 .process_start_time
96 98 .monotonic_clock
97 99 .elapsed()
98 100 .as_secs_f64();
99 101 let message = format_bytes!(
100 102 b"(rust) {} exited {} after {} seconds",
101 103 format_cli_args(),
102 104 exit_code,
103 105 format_bytes::Utf8(format_args!("{:.03}", duration))
104 106 );
105 107 configured.log(&now, &message);
106 108 }
107 109 }
108 110 }
109 111
110 112 impl ConfiguredBlackbox<'_> {
111 113 fn log(&self, date_time: &DateTime, message: &[u8]) {
112 114 let date = format_bytes::Utf8(date_time.format(self.date_format));
113 115 let user = users::get_current_username().map(get_bytes_from_os_str);
114 116 let user = user.as_deref().unwrap_or(b"???");
115 117 let rev = format_bytes::Utf8(match self.repo.dirstate_parents() {
116 118 Ok(parents) if parents.p2 == hg::revlog::node::NULL_NODE => {
117 119 format!("{:x}", parents.p1)
118 120 }
119 121 Ok(parents) => format!("{:x}+{:x}", parents.p1, parents.p2),
120 122 Err(_dirstate_corruption_error) => {
121 123 // TODO: log a non-fatal warning to stderr
122 124 "???".to_owned()
123 125 }
124 126 });
125 127 let pid = std::process::id();
126 128 let line = format_bytes!(
127 129 b"{} {} @{} ({})> {}\n",
128 130 date,
129 131 user,
130 132 rev,
131 133 pid,
132 134 message
133 135 );
134 136 let result =
135 137 hg::logging::LogFile::new(self.repo.hg_vfs(), "blackbox.log")
136 138 .max_size(Some(self.max_size))
137 139 .max_files(self.max_files)
138 140 .write(&line);
139 141 match result {
140 142 Ok(()) => {}
141 143 Err(_io_error) => {
142 144 // TODO: log a non-fatal warning to stderr
143 145 }
144 146 }
145 147 }
146 148 }
147 149
148 150 fn format_cli_args() -> Vec<u8> {
149 151 let mut args = std::env::args_os();
150 152 let _ = args.next(); // Skip the first (or zeroth) arg, the name of the `rhg` executable
151 153 let mut args = args.map(|arg| shell_quote(&get_bytes_from_os_str(arg)));
152 154 let mut formatted = Vec::new();
153 155 if let Some(arg) = args.next() {
154 156 formatted.extend(arg)
155 157 }
156 158 for arg in args {
157 159 formatted.push(b' ');
158 160 formatted.extend(arg)
159 161 }
160 162 formatted
161 163 }
@@ -1,36 +1,36 b''
1 1 use crate::error::CommandError;
2 2 use clap::Arg;
3 3 use format_bytes::format_bytes;
4 4 use hg::errors::HgError;
5 5 use hg::utils::SliceExt;
6 6
7 7 pub const HELP_TEXT: &str = "
8 8 With one argument of the form section.name, print just the value of that config item.
9 9 ";
10 10
11 11 pub fn args() -> clap::App<'static, 'static> {
12 12 clap::SubCommand::with_name("config")
13 13 .arg(
14 14 Arg::with_name("name")
15 15 .help("the section.name to print")
16 16 .value_name("NAME")
17 17 .required(true)
18 18 .takes_value(true),
19 19 )
20 20 .about(HELP_TEXT)
21 21 }
22 22
23 23 pub fn run(invocation: &crate::CliInvocation) -> Result<(), CommandError> {
24 24 let (section, name) = invocation
25 25 .subcommand_args
26 26 .value_of("name")
27 27 .expect("missing required CLI argument")
28 28 .as_bytes()
29 29 .split_2(b'.')
30 30 .ok_or_else(|| HgError::abort(""))?;
31 31
32 let value = invocation.config().get(section, name).unwrap_or(b"");
32 let value = invocation.config.get(section, name).unwrap_or(b"");
33 33
34 34 invocation.ui.write_stdout(&format_bytes!(b"{}\n", value))?;
35 35 Ok(())
36 36 }
@@ -1,179 +1,228 b''
1 1 extern crate log;
2 2 use crate::ui::Ui;
3 3 use clap::App;
4 4 use clap::AppSettings;
5 5 use clap::Arg;
6 6 use clap::ArgMatches;
7 7 use format_bytes::format_bytes;
8 8 use hg::config::Config;
9 9 use hg::repo::{Repo, RepoError};
10 use std::path::{Path, PathBuf};
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 use hg::utils::SliceExt;
12 use std::ffi::OsString;
13 use std::path::PathBuf;
11 14
12 15 mod blackbox;
13 16 mod error;
14 17 mod exitcode;
15 18 mod ui;
16 19 use error::CommandError;
17 20
18 21 fn main_with_result(
22 process_start_time: &blackbox::ProcessStartTime,
19 23 ui: &ui::Ui,
20 process_start_time: &blackbox::ProcessStartTime,
24 repo: Result<&Repo, &NoRepoInCwdError>,
25 config: &Config,
21 26 ) -> Result<(), CommandError> {
22 env_logger::init();
23 27 let app = App::new("rhg")
24 28 .global_setting(AppSettings::AllowInvalidUtf8)
25 29 .setting(AppSettings::SubcommandRequired)
26 30 .setting(AppSettings::VersionlessSubcommands)
27 31 .arg(
28 32 Arg::with_name("repository")
29 33 .help("repository root directory")
30 34 .short("-R")
31 35 .long("--repository")
32 36 .value_name("REPO")
33 37 .takes_value(true)
34 38 // Both ok: `hg -R ./foo log` or `hg log -R ./foo`
35 39 .global(true),
36 40 )
37 41 .arg(
38 42 Arg::with_name("config")
39 43 .help("set/override config option (use 'section.name=value')")
40 44 .long("--config")
41 45 .value_name("CONFIG")
42 46 .takes_value(true)
43 47 .global(true)
44 48 // Ok: `--config section.key1=val --config section.key2=val2`
45 49 .multiple(true)
46 50 // Not ok: `--config section.key1=val section.key2=val2`
47 51 .number_of_values(1),
48 52 )
49 53 .version("0.0.1");
50 54 let app = add_subcommand_args(app);
51 55
52 56 let matches = app.clone().get_matches_safe()?;
53 57
54 58 let (subcommand_name, subcommand_matches) = matches.subcommand();
55 59 let run = subcommand_run_fn(subcommand_name)
56 60 .expect("unknown subcommand name from clap despite AppSettings::SubcommandRequired");
57 61 let subcommand_args = subcommand_matches
58 62 .expect("no subcommand arguments from clap despite AppSettings::SubcommandRequired");
59 63
60 let config_args = matches
61 .values_of_os("config")
62 // Turn `Option::None` into an empty iterator:
63 .into_iter()
64 .flatten()
65 .map(hg::utils::files::get_bytes_from_os_str);
66 let non_repo_config = &hg::config::Config::load(config_args)?;
67
68 let repo_path = matches.value_of_os("repository").map(Path::new);
69 let repo = match Repo::find(non_repo_config, repo_path) {
70 Ok(repo) => Ok(repo),
71 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
72 // Not finding a repo is not fatal yet, if `-R` was not given
73 Err(NoRepoInCwdError { cwd: at })
74 }
75 Err(error) => return Err(error.into()),
76 };
77
78 64 let invocation = CliInvocation {
79 65 ui,
80 66 subcommand_args,
81 non_repo_config,
82 repo: repo.as_ref(),
67 config,
68 repo,
83 69 };
84 70 let blackbox = blackbox::Blackbox::new(&invocation, process_start_time)?;
85 71 blackbox.log_command_start();
86 72 let result = run(&invocation);
87 73 blackbox.log_command_end(exit_code(&result));
88 74 result
89 75 }
90 76
91 77 fn main() {
92 78 // Run this first, before we find out if the blackbox extension is even
93 79 // enabled, in order to include everything in-between in the duration
94 80 // measurements. Reading config files can be slow if they’re on NFS.
95 81 let process_start_time = blackbox::ProcessStartTime::now();
96 82
83 env_logger::init();
97 84 let ui = ui::Ui::new();
98 85
99 let result = main_with_result(&ui, &process_start_time);
100 if let Err(CommandError::Abort { message }) = &result {
101 if !message.is_empty() {
102 // Ignore errors when writing to stderr, we’re already exiting
103 // with failure code so there’s not much more we can do.
104 let _ = ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
86 let early_args = EarlyArgs::parse(std::env::args_os());
87 let non_repo_config = Config::load(early_args.config)
88 .unwrap_or_else(|error| exit(&ui, Err(error.into())));
89
90 let repo_path = early_args.repo.as_deref().map(get_path_from_bytes);
91 let repo_result = match Repo::find(&non_repo_config, repo_path) {
92 Ok(repo) => Ok(repo),
93 Err(RepoError::NotFound { at }) if repo_path.is_none() => {
94 // Not finding a repo is not fatal yet, if `-R` was not given
95 Err(NoRepoInCwdError { cwd: at })
105 96 }
106 }
107 std::process::exit(exit_code(&result))
97 Err(error) => exit(&ui, Err(error.into())),
98 };
99
100 let config = if let Ok(repo) = &repo_result {
101 repo.config()
102 } else {
103 &non_repo_config
104 };
105
106 let result = main_with_result(
107 &process_start_time,
108 &ui,
109 repo_result.as_ref(),
110 config,
111 );
112 exit(&ui, result)
108 113 }
109 114
110 115 fn exit_code(result: &Result<(), CommandError>) -> i32 {
111 116 match result {
112 117 Ok(()) => exitcode::OK,
113 118 Err(CommandError::Abort { .. }) => exitcode::ABORT,
114 119
115 120 // Exit with a specific code and no error message to let a potential
116 121 // wrapper script fallback to Python-based Mercurial.
117 122 Err(CommandError::Unimplemented) => exitcode::UNIMPLEMENTED,
118 123 }
119 124 }
120 125
126 fn exit(ui: &Ui, result: Result<(), CommandError>) -> ! {
127 if let Err(CommandError::Abort { message }) = &result {
128 if !message.is_empty() {
129 // Ignore errors when writing to stderr, we’re already exiting
130 // with failure code so there’s not much more we can do.
131 let _ = ui.write_stderr(&format_bytes!(b"abort: {}\n", message));
132 }
133 }
134 std::process::exit(exit_code(&result))
135 }
136
121 137 macro_rules! subcommands {
122 138 ($( $command: ident )+) => {
123 139 mod commands {
124 140 $(
125 141 pub mod $command;
126 142 )+
127 143 }
128 144
129 145 fn add_subcommand_args<'a, 'b>(app: App<'a, 'b>) -> App<'a, 'b> {
130 146 app
131 147 $(
132 148 .subcommand(commands::$command::args())
133 149 )+
134 150 }
135 151
136 152 pub type RunFn = fn(&CliInvocation) -> Result<(), CommandError>;
137 153
138 154 fn subcommand_run_fn(name: &str) -> Option<RunFn> {
139 155 match name {
140 156 $(
141 157 stringify!($command) => Some(commands::$command::run),
142 158 )+
143 159 _ => None,
144 160 }
145 161 }
146 162 };
147 163 }
148 164
149 165 subcommands! {
150 166 cat
151 167 debugdata
152 168 debugrequirements
153 169 files
154 170 root
155 171 config
156 172 }
157 173 pub struct CliInvocation<'a> {
158 174 ui: &'a Ui,
159 175 subcommand_args: &'a ArgMatches<'a>,
160 non_repo_config: &'a Config,
176 config: &'a Config,
161 177 /// References inside `Result` is a bit peculiar but allow
162 178 /// `invocation.repo?` to work out with `&CliInvocation` since this
163 179 /// `Result` type is `Copy`.
164 180 repo: Result<&'a Repo, &'a NoRepoInCwdError>,
165 181 }
166 182
167 183 struct NoRepoInCwdError {
168 184 cwd: PathBuf,
169 185 }
170 186
171 impl CliInvocation<'_> {
172 fn config(&self) -> &Config {
173 if let Ok(repo) = self.repo {
174 repo.config()
175 } else {
176 self.non_repo_config
187 /// CLI arguments to be parsed "early" in order to be able to read
188 /// configuration before using Clap. Ideally we would also use Clap for this,
189 /// see <https://github.com/clap-rs/clap/discussions/2366>.
190 ///
191 /// These arguments are still declared when we do use Clap later, so that Clap
192 /// does not return an error for their presence.
193 struct EarlyArgs {
194 /// Values of all `--config` arguments. (Possibly none)
195 config: Vec<Vec<u8>>,
196 /// Value of the `-R` or `--repository` argument, if any.
197 repo: Option<Vec<u8>>,
198 }
199
200 impl EarlyArgs {
201 fn parse(args: impl IntoIterator<Item = OsString>) -> Self {
202 let mut args = args.into_iter().map(get_bytes_from_os_str);
203 let mut config = Vec::new();
204 let mut repo = None;
205 // Use `while let` instead of `for` so that we can also call
206 // `args.next()` inside the loop.
207 while let Some(arg) = args.next() {
208 if arg == b"--config" {
209 if let Some(value) = args.next() {
210 config.push(value)
211 }
212 } else if let Some(value) = arg.drop_prefix(b"--config=") {
213 config.push(value.to_owned())
214 }
215
216 if arg == b"--repository" || arg == b"-R" {
217 if let Some(value) = args.next() {
218 repo = Some(value)
219 }
220 } else if let Some(value) = arg.drop_prefix(b"--repository=") {
221 repo = Some(value.to_owned())
222 } else if let Some(value) = arg.drop_prefix(b"-R") {
223 repo = Some(value.to_owned())
224 }
177 225 }
226 Self { config, repo }
178 227 }
179 228 }
General Comments 0
You need to be logged in to leave comments. Login now