aboutsummaryrefslogtreecommitdiff
path: root/src/commands.rs
diff options
context:
space:
mode:
authorDaniel Mueller <deso@posteo.net>2020-08-25 19:04:50 -0700
committerDaniel Mueller <deso@posteo.net>2020-10-11 17:07:07 -0700
commit8cf63c6790192c30c81294e7a940d470bf061cbf (patch)
tree169ca792304f74a7b1548b5bad798b39649cd4c5 /src/commands.rs
parent330142e68cac0116babbf2fd64fc9ff0fde132c0 (diff)
downloadnitrocli-8cf63c6790192c30c81294e7a940d470bf061cbf.tar.gz
nitrocli-8cf63c6790192c30c81294e7a940d470bf061cbf.tar.bz2
Introduce support for user-provided extensions
This change introduces support for discovering and executing user-provided extensions to the program. Extensions are useful for allowing users to provide additional functionality on top of the nitrocli proper. Implementation wise we stick to an approach similar to git or cargo subcommands in nature: we search the directories listed in the PATH environment variable for a file that starts with "nitrocli-", followed by the extension name. This file is then executed. It is assumed that the extension recognizes (or at least not prohibits) the following arguments: --nitrocli (providing the path to the nitrocli binary), --model (with the model passed to the main program), and --verbosity (the verbosity level).
Diffstat (limited to 'src/commands.rs')
-rw-r--r--src/commands.rs103
1 files changed, 103 insertions, 0 deletions
diff --git a/src/commands.rs b/src/commands.rs
index 092b006..549ebec 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -3,11 +3,17 @@
// Copyright (C) 2018-2020 The Nitrocli Developers
// SPDX-License-Identifier: GPL-3.0-or-later
+use std::borrow;
use std::convert::TryFrom as _;
+use std::env;
+use std::ffi;
use std::fmt;
+use std::io;
use std::mem;
use std::ops;
use std::ops::Deref as _;
+use std::path;
+use std::process;
use std::thread;
use std::time;
use std::u8;
@@ -1081,6 +1087,103 @@ pub fn pws_status(ctx: &mut Context<'_>, all: bool) -> anyhow::Result<()> {
})
}
+/// Resolve an extension provided by name to an actual path.
+///
+/// Extensions are (executable) files that have the "nitrocli-" prefix
+/// and are discoverable via the `PATH` environment variable.
+pub(crate) fn resolve_extension(
+ path_var: &ffi::OsStr,
+ ext_name: &ffi::OsStr,
+) -> anyhow::Result<path::PathBuf> {
+ let mut bin_name = ffi::OsString::from("nitrocli-");
+ bin_name.push(ext_name);
+
+ for dir in env::split_paths(path_var) {
+ let mut bin_path = dir.clone();
+ bin_path.push(&bin_name);
+ // Note that we deliberately do not check whether the file we found
+ // is executable. If it is not we will just fail later on with a
+ // permission denied error. The reasons for this behavior are two
+ // fold:
+ // 1) Checking whether a file is executable in Rust is painful (as
+ // of 1.37 there exists the PermissionsExt trait but it is
+ // available only for Unix based systems).
+ // 2) It is considered a better user experience to resolve to an
+ // extension even if it later turned out to be not usable over
+ // not showing it and silently doing nothing -- mostly because
+ // anything residing in PATH should be executable anyway and
+ // given that its name also starts with nitrocli- we are pretty
+ // sure that's a bug on the user's side.
+ if bin_path.is_file() {
+ return Ok(bin_path);
+ }
+ }
+
+ let err = if let Some(name) = bin_name.to_str() {
+ format!("Extension {} not found", name).into()
+ } else {
+ borrow::Cow::from("Extension not found")
+ };
+ Err(io::Error::new(io::ErrorKind::NotFound, err).into())
+}
+
+/// Run an extension.
+pub fn extension(ctx: &mut Context<'_>, args: Vec<ffi::OsString>) -> anyhow::Result<()> {
+ // Note that while `Command` would actually honor PATH by itself, we
+ // do not want that behavior because it would circumvent the execution
+ // context we use for testing. As such, we need to do our own search.
+ let mut args = args.into_iter();
+ let ext_name = args.next().context("No extension specified")?;
+ let path_var = ctx.path.as_ref().context("PATH variable not present")?;
+ let ext_path = resolve_extension(&path_var, &ext_name)?;
+
+ // Note that theoretically we could just exec the extension and be
+ // done. However, the problem with that approach is that it makes
+ // testing extension support much more nasty, because the test process
+ // would be overwritten in the process, requiring us to essentially
+ // fork & exec nitrocli beforehand -- which is much more involved from
+ // a cargo test context.
+ let mut cmd = process::Command::new(&ext_path);
+
+ if let Some(model) = ctx.config.model {
+ let _ = cmd.env(crate::NITROCLI_MODEL, model.to_string());
+ }
+
+ if let Some(usb_path) = &ctx.config.usb_path {
+ let _ = cmd.env(crate::NITROCLI_USB_PATH, usb_path);
+ }
+
+ // TODO: We may want to take this path from the command execution
+ // context.
+ let binary = env::current_exe().context("Failed to retrieve path to nitrocli binary")?;
+ let serial_numbers = ctx
+ .config
+ .serial_numbers
+ .iter()
+ .map(ToString::to_string)
+ .collect::<Vec<_>>()
+ .join(",");
+
+ let out = cmd
+ .env(crate::NITROCLI_BINARY, binary)
+ .env(crate::NITROCLI_VERBOSITY, ctx.config.verbosity.to_string())
+ .env(crate::NITROCLI_NO_CACHE, ctx.config.no_cache.to_string())
+ .env(crate::NITROCLI_SERIAL_NUMBERS, serial_numbers)
+ .args(args)
+ .output()
+ .with_context(|| format!("Failed to execute extension {}", ext_path.display()))?;
+ ctx.stdout.write_all(&out.stdout)?;
+ ctx.stderr.write_all(&out.stderr)?;
+
+ if out.status.success() {
+ Ok(())
+ } else if let Some(rc) = out.status.code() {
+ Err(anyhow::Error::new(crate::DirectExitError(rc)))
+ } else {
+ Err(anyhow::Error::new(crate::DirectExitError(1)))
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;