comparison rust/rhg/src/main.rs @ 46667:93e9f448273c

rhg: Add support for automatic fallback to Python `rhg` is a command-line application that can do a small subset of what `hg` can. It is written entirely in Rust, which avoids the cost of starting a Python interpreter and importing many Python modules. In a script that runs many `hg` commands, this cost can add up. However making users decide when to use `rhg` instead of `hg` is not practical as we want the subset of supported functionality to grow over time. Instead we introduce "fallback" behavior where, when `rhg` encounters something (a sub-command, a repository format, …) that is not implemented in Rust-only, it does nothing but silently start a subprocess of Python-based `hg` running the same command. That way `rhg` becomes a drop-in replacement for `hg` that sometimes goes faster. Whether Python is used should be an implementation detail not apparent to users (other than through speed). A new `fallback` value is added to the previously introduced `rhg.on-unsupported` configuration key. When in this mode, the new `rhg.fallback-executable` config is determine what command to use to run a Python-based `hg`. The previous `rhg.on-unsupported = abort-silent` configuration was designed to let a wrapper script call `rhg` and then fall back to `hg` based on the exit code. This is still available, but having fallback behavior built-in in rhg might be easier for users instead of leaving that script "as an exercise for the reader". Using a subprocess like this is not idea, especially when `rhg` is to be installed in `$PATH` as `hg`, since the other `hg.py` executable needs to still be available… somewhere. Eventually this could be replaced by using PyOxidizer to a have a single executable that embeds a Python interpreter, but only starts it when needed. Differential Revision: https://phab.mercurial-scm.org/D10093
author Simon Sapin <simon.sapin@octobus.net>
date Mon, 01 Mar 2021 20:36:06 +0100
parents 33f2d56acc73
children 6cd9f53aaed8
comparison
equal deleted inserted replaced
46666:33f2d56acc73 46667:93e9f448273c
9 use hg::repo::{Repo, RepoError}; 9 use hg::repo::{Repo, RepoError};
10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes}; 10 use hg::utils::files::{get_bytes_from_os_str, get_path_from_bytes};
11 use hg::utils::SliceExt; 11 use hg::utils::SliceExt;
12 use std::ffi::OsString; 12 use std::ffi::OsString;
13 use std::path::PathBuf; 13 use std::path::PathBuf;
14 use std::process::Command;
14 15
15 mod blackbox; 16 mod blackbox;
16 mod error; 17 mod error;
17 mod exitcode; 18 mod exitcode;
18 mod ui; 19 mod ui;
136 } 137 }
137 } 138 }
138 139
139 fn exit( 140 fn exit(
140 ui: &Ui, 141 ui: &Ui,
141 on_unsupported: OnUnsupported, 142 mut on_unsupported: OnUnsupported,
142 result: Result<(), CommandError>, 143 result: Result<(), CommandError>,
143 ) -> ! { 144 ) -> ! {
145 if let (
146 OnUnsupported::Fallback { executable },
147 Err(CommandError::UnsupportedFeature { .. }),
148 ) = (&on_unsupported, &result)
149 {
150 let mut args = std::env::args_os();
151 let executable_path = get_path_from_bytes(&executable);
152 let this_executable = args.next().expect("exepcted argv[0] to exist");
153 if executable_path == &PathBuf::from(this_executable) {
154 // Avoid spawning infinitely many processes until resource
155 // exhaustion.
156 let _ = ui.write_stderr(&format_bytes!(
157 b"Blocking recursive fallback. The 'rhg.fallback-executable = {}' config \
158 points to `rhg` itself.\n",
159 executable
160 ));
161 on_unsupported = OnUnsupported::Abort
162 } else {
163 // `args` is now `argv[1..]` since we’ve already consumed `argv[0]`
164 let result = Command::new(executable_path).args(args).status();
165 match result {
166 Ok(status) => std::process::exit(
167 status.code().unwrap_or(exitcode::ABORT),
168 ),
169 Err(error) => {
170 let _ = ui.write_stderr(&format_bytes!(
171 b"tried to fall back to a '{}' sub-process but got error {}\n",
172 executable, format_bytes::Utf8(error)
173 ));
174 on_unsupported = OnUnsupported::Abort
175 }
176 }
177 }
178 }
144 match &result { 179 match &result {
145 Ok(_) => {} 180 Ok(_) => {}
146 Err(CommandError::Abort { message }) => { 181 Err(CommandError::Abort { message }) => {
147 if !message.is_empty() { 182 if !message.is_empty() {
148 // Ignore errors when writing to stderr, we’re already exiting 183 // Ignore errors when writing to stderr, we’re already exiting
158 b"unsupported feature: {}\n", 193 b"unsupported feature: {}\n",
159 message 194 message
160 )); 195 ));
161 } 196 }
162 OnUnsupported::AbortSilent => {} 197 OnUnsupported::AbortSilent => {}
198 OnUnsupported::Fallback { .. } => unreachable!(),
163 } 199 }
164 } 200 }
165 } 201 }
166 std::process::exit(exit_code(&result)) 202 std::process::exit(exit_code(&result))
167 } 203 }
266 /// Print an error message describing what feature is not supported, 302 /// Print an error message describing what feature is not supported,
267 /// and exit with code 252. 303 /// and exit with code 252.
268 Abort, 304 Abort,
269 /// Silently exit with code 252. 305 /// Silently exit with code 252.
270 AbortSilent, 306 AbortSilent,
307 /// Try running a Python implementation
308 Fallback { executable: Vec<u8> },
271 } 309 }
272 310
273 impl OnUnsupported { 311 impl OnUnsupported {
312 const DEFAULT: Self = OnUnsupported::Abort;
313 const DEFAULT_FALLBACK_EXECUTABLE: &'static [u8] = b"hg";
314
274 fn from_config(config: &Config) -> Self { 315 fn from_config(config: &Config) -> Self {
275 let default = OnUnsupported::Abort; 316 match config
276 match config.get(b"rhg", b"on-unsupported") { 317 .get(b"rhg", b"on-unsupported")
318 .map(|value| value.to_ascii_lowercase())
319 .as_deref()
320 {
277 Some(b"abort") => OnUnsupported::Abort, 321 Some(b"abort") => OnUnsupported::Abort,
278 Some(b"abort-silent") => OnUnsupported::AbortSilent, 322 Some(b"abort-silent") => OnUnsupported::AbortSilent,
279 None => default, 323 Some(b"fallback") => OnUnsupported::Fallback {
324 executable: config
325 .get(b"rhg", b"fallback-executable")
326 .unwrap_or(Self::DEFAULT_FALLBACK_EXECUTABLE)
327 .to_owned(),
328 },
329 None => Self::DEFAULT,
280 Some(_) => { 330 Some(_) => {
281 // TODO: warn about unknown config value 331 // TODO: warn about unknown config value
282 default 332 Self::DEFAULT
283 } 333 }
284 } 334 }
285 } 335 }
286 } 336 }