diff options
Diffstat (limited to 'src/commands.rs')
-rw-r--r-- | src/commands.rs | 94 |
1 files changed, 94 insertions, 0 deletions
diff --git a/src/commands.rs b/src/commands.rs index a2b6004..5e2af91 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -17,8 +17,14 @@ // * along with this program. If not, see <http://www.gnu.org/licenses/>. * // ************************************************************************* +use std::borrow; +use std::env; +use std::ffi; use std::fmt; +use std::io; use std::mem; +use std::path; +use std::process; use std::result; use std::thread; use std::time; @@ -980,6 +986,94 @@ pub fn pws_status(ctx: &mut ExecCtx<'_>, all: bool) -> 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, +) -> Result<path::PathBuf> { + let mut bin_name = ffi::OsString::from("nitrocli-"); + bin_name.push(ext_name); + + // The std::env module has several references to the PATH environment + // variable, indicating that this name is considered platform + // independent from their perspective. We do the same. + 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 show an extension + // that we found (we list them in the help text) 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 ExecCtx<'_>, args: Vec<ffi::OsString>) -> 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() + .ok_or_else(|| Error::from("no extension specified"))?; + let path_var = ctx + .path + .as_ref() + .ok_or_else(|| Error::from("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.model { + let _ = cmd.args(&["--model", model.as_ref()]); + }; + + let out = cmd + // TODO: We may want to take this path from the command execution + // context. + .args(&["--nitrocli", &env::current_exe()?.to_string_lossy()]) + .args(&["--verbosity", &ctx.verbosity.to_string()]) + .args(args) + .output()?; + ctx.stdout.write_all(&out.stdout)?; + ctx.stderr.write_all(&out.stderr)?; + + if out.status.success() { + Ok(()) + } else { + Err(Error::ExtensionFailed(ext_path, out.status.code())) + } +} + #[cfg(test)] mod tests { use super::*; |