diff options
author | Lukasz Woznicki <75632179+makr11st@users.noreply.github.com> | 2021-11-24 20:54:56 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-11-24 20:54:56 +0000 |
commit | a4ffeccf60090e4456755bc53a6e3b8c8038e855 (patch) | |
tree | 9583f187114913a92866571920dd3bb205bd50a3 /crates/core/plugin_sm | |
parent | 8217e80670e76dbf9168780f5e0545355a39f8f3 (diff) |
Restructure directories of the workspace (#559)
* Restructure directories of the workspace
* Rename c8y_translator_lib to c8y_translator
* Update comment on how to get dummy plugin path
Signed-off-by: Lukasz Woznicki <lukasz.woznicki@softwareag.com>
Diffstat (limited to 'crates/core/plugin_sm')
-rw-r--r-- | crates/core/plugin_sm/Cargo.toml | 24 | ||||
-rw-r--r-- | crates/core/plugin_sm/src/lib.rs | 4 | ||||
-rw-r--r-- | crates/core/plugin_sm/src/log_file.rs | 25 | ||||
-rw-r--r-- | crates/core/plugin_sm/src/logged_command.rs | 231 | ||||
-rw-r--r-- | crates/core/plugin_sm/src/plugin.rs | 468 | ||||
-rw-r--r-- | crates/core/plugin_sm/src/plugin_manager.rs | 273 | ||||
-rw-r--r-- | crates/core/plugin_sm/tests/fixtures/plugin/plugin_get_command_list.0 | 3 | ||||
-rw-r--r-- | crates/core/plugin_sm/tests/plugin.rs | 329 | ||||
-rw-r--r-- | crates/core/plugin_sm/tests/plugin_manager.rs | 203 |
9 files changed, 1560 insertions, 0 deletions
diff --git a/crates/core/plugin_sm/Cargo.toml b/crates/core/plugin_sm/Cargo.toml new file mode 100644 index 00000000..1e67dbf3 --- /dev/null +++ b/crates/core/plugin_sm/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "plugin_sm" +version = "0.4.3" +authors = ["thin-edge.io team <info@thin-edge.io>"] +edition = "2018" + +[dependencies] +async-trait = "0.1" +csv = "1.1" +download = { path = "../../common/download" } +json_sm = { path = "../json_sm" } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tedge_utils = { path = "../../common/tedge_utils" } +thiserror = "1.0" +tokio = { version = "1.8", features = ["process", "rt"] } +tracing = { version = "0.1", features = ["attributes", "log"] } +url = "2.2" + +[dev-dependencies] +anyhow = "1.0" +assert_matches = "1.5" +structopt = "0.3" +tempfile = "3.2" diff --git a/crates/core/plugin_sm/src/lib.rs b/crates/core/plugin_sm/src/lib.rs new file mode 100644 index 00000000..79800999 --- /dev/null +++ b/crates/core/plugin_sm/src/lib.rs @@ -0,0 +1,4 @@ +pub mod log_file; +pub mod logged_command; +pub mod plugin; +pub mod plugin_manager; diff --git a/crates/core/plugin_sm/src/log_file.rs b/crates/core/plugin_sm/src/log_file.rs new file mode 100644 index 00000000..86d50a7c --- /dev/null +++ b/crates/core/plugin_sm/src/log_file.rs @@ -0,0 +1,25 @@ +use std::path::PathBuf; +use tokio::fs::File; +use tokio::io::BufWriter; + +pub struct LogFile { + path: PathBuf, + buffer: BufWriter<File>, +} + +impl LogFile { + pub async fn try_new(path: PathBuf) -> Result<LogFile, std::io::Error> { + let file = File::create(path.clone()).await?; + let buffer = BufWriter::new(file); + + Ok(LogFile { path, buffer }) + } + + pub fn path(&self) -> &str { + &self.path.to_str().unwrap_or("/var/log/tedge/agent") + } + + pub fn buffer(&mut self) -> &mut BufWriter<File> { + &mut self.buffer + } +} diff --git a/crates/core/plugin_sm/src/logged_command.rs b/crates/core/plugin_sm/src/logged_command.rs new file mode 100644 index 00000000..11739ce0 --- /dev/null +++ b/crates/core/plugin_sm/src/logged_command.rs @@ -0,0 +1,231 @@ +use std::ffi::OsStr; +use std::process::{Output, Stdio}; +use tokio::fs::File; +use tokio::io::{AsyncWriteExt, BufWriter}; +use tokio::process::{Child, Command}; + +pub struct LoggingChild { + command_line: String, + pub inner_child: Child, +} + +impl LoggingChild { + pub async fn wait_with_output( + self, + logger: &mut BufWriter<File>, + ) -> Result<Output, std::io::Error> { + let outcome = self.inner_child.wait_with_output().await; + if let Err(err) = LoggedCommand::log_outcome(&self.command_line, &outcome, logger).await { + tracing::log::error!("Fail to log the command execution: {}", err); + } + + outcome + } +} + +/// A command which execution is logged. +/// +/// This struct wraps the main command with a nice representation of that command. +/// This `command_line` field is only required because the +/// [`Command::get_program()`](https://doc.rust-lang.org/std/process/struct.Command.html#method.get_program) +/// and +/// [`Command::get_args()`](https://doc.rust-lang.org/std/process/struct.Command.html#method.get_args) +/// are nightly-only experimental APIs. +pub struct LoggedCommand { + command_line: String, + command: Command, +} + +impl std::fmt::Display for LoggedCommand { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.command_line.fmt(f) + } +} + +impl LoggedCommand { + pub fn new(program: impl AsRef<OsStr>) -> LoggedCommand { + let command_line = match program.as_ref().to_str() { + None => format!("{:?}", program.as_ref()), + Some(cmd) => cmd.to_string(), + }; + + let mut command = Command::new(program); + command + .current_dir("/tmp") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + LoggedCommand { + command_line, + command, + } + } + + pub fn arg(&mut self, arg: impl AsRef<OsStr>) -> &mut LoggedCommand { + // The arguments are displayed as debug, to be properly quoted and distinguished from each other. + self.command_line.push_str(&format!(" {:?}", arg.as_ref())); + self.command.arg(arg); + self + } + + /// Execute the command and log its exit status, stdout and stderr + /// + /// If the command has been executed the outcome is returned (successful or not). + /// If the command fails to execute (say not found or not executable) an `std::io::Error` is returned. + /// + /// If the function fails to log the execution of the command, + /// this is logged with `log::error!` without changing the return value. + pub async fn execute(mut self, logger: &mut BufWriter<File>) -> Result<Output, std::io::Error> { + let outcome = self.command.output().await; + + if let Err(err) = LoggedCommand::log_outcome(&self.command_line, &outcome, logger).await { + tracing::log::error!("Fail to log the command execution: {}", err); + } + + outcome + } + + pub fn spawn(&mut self) -> Result<LoggingChild, std::io::Error> { + let child = self.command.spawn()?; + Ok(LoggingChild { + command_line: self.command_line.clone(), + inner_child: child, + }) + } + + async fn log_outcome( + command_line: &str, + result: &Result<Output, std::io::Error>, + logger: &mut BufWriter<File>, + ) -> Result<(), std::io::Error> { + logger + .write_all(format!("----- $ {}\n", command_line).as_bytes()) + .await?; + + match result.as_ref() { + Ok(output) => { + match &output.status.code() { + None => logger.write_all(b"exit status: unknown\n\n").await?, + Some(code) => { + logger + .write_all(format!("exit status: {}\n\n", code).as_bytes()) + .await? + } + }; + logger.write_all(b"stdout <<EOF\n").await?; + logger.write_all(&output.stdout).await?; + logger.write_all(b"EOF\n\n").await?; + logger.write_all(b"stderr <<EOF\n").await?; + logger.write_all(&output.stderr).await?; + logger.write_all(b"EOF\n").await?; + } + Err(err) => { + logger + .write_all(format!("error: {}\n", &err).as_bytes()) + .await?; + } + } + + logger.flush().await?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::*; + use tokio::fs::File; + + #[tokio::test] + async fn on_execute_are_logged_command_line_exit_status_stdout_and_stderr( + ) -> Result<(), anyhow::Error> { + // Prepare a log file + let tmp_dir = TempDir::new()?; + let log_file_path = tmp_dir.path().join("operation.log"); + let log_file = File::create(log_file_path.clone()).await?; + let mut logger = BufWriter::new(log_file); + + // Prepare a command + let mut command = LoggedCommand::new("echo"); + command.arg("Hello").arg("World!"); + + // Execute the command with logging + let _ = command.execute(&mut logger).await; + + let log_content = String::from_utf8(std::fs::read(&log_file_path)?)?; + assert_eq!( + log_content, + r#"----- $ echo "Hello" "World!" +exit status: 0 + +stdout <<EOF +Hello World! +EOF + +stderr <<EOF +EOF +"# + ); + Ok(()) + } + + #[tokio::test] + async fn on_execute_with_error_stderr_is_logged() -> Result<(), anyhow::Error> { + // Prepare a log file + let tmp_dir = TempDir::new()?; + let log_file_path = tmp_dir.path().join("operation.log"); + let log_file = File::create(log_file_path.clone()).await?; + let mut logger = BufWriter::new(log_file); + + // Prepare a command that triggers some content on stderr + let mut command = LoggedCommand::new("ls"); + command.arg("dummy-file"); + + // Execute the command with logging + let _ = command.execute(&mut logger).await; + + // On expect the errors to be logged + let log_content = String::from_utf8(std::fs::read(&log_file_path)?)?; + assert_eq!( + log_content, + r#"----- $ ls "dummy-file" +exit status: 2 + +stdout <<EOF +EOF + +stderr <<EOF +ls: cannot access 'dummy-file': No such file or directory +EOF +"# + ); + Ok(()) + } + + #[tokio::test] + async fn on_execution_error_are_logged_command_line_and_error() -> Result<(), anyhow::Error> { + // Prepare a log file + let tmp_dir = TempDir::new()?; + let log_file_path = tmp_dir.path().join("operation.log"); + let log_file = File::create(log_file_path.clone()).await?; + let mut logger = BufWriter::new(log_file); + + // Prepare a command that cannot be executed + let command = LoggedCommand::new("dummy-command"); + + // Execute the command with logging + let _ = command.execute(&mut logger).await; + + // The fact that the command cannot be executed must be logged + let log_content = String::from_utf8(std::fs::read(&log_file_path)?)?; + assert_eq!( + log_content, + r#"----- $ dummy-command +error: No such file or directory (os error 2) +"# + ); + Ok(()) + } +} diff --git a/crates/core/plugin_sm/src/plugin.rs b/crates/core/plugin_sm/src/plugin.rs new file mode 100644 index 00000000..977ed88b --- /dev/null +++ b/crates/core/plugin_sm/src/plugin.rs @@ -0,0 +1,468 @@ +use crate::logged_command::LoggedCommand; +use async_trait::async_trait; +use csv::ReaderBuilder; +use download::Downloader; +use json_sm::*; +use std::{path::PathBuf, process::Output}; +use tokio::io::BufWriter; +use tokio::{fs::File, io::AsyncWriteExt}; +use tracing::error; + +#[async_trait] +pub trait Plugin { + async fn prepare(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError>; + + async fn install( + &self, + module: &SoftwareModule, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError>; + + async fn remove( + &self, + module: &SoftwareModule, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError>; + + async fn update_list( + &self, + modules: &Vec<SoftwareModuleUpdate>, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError>; + + async fn finalize(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError>; + + async fn list( + &self, + logger: &mut BufWriter<File>, + ) -> Result<Vec<SoftwareModule>, SoftwareError>; + + async fn version( + &self, + module: &SoftwareModule, + logger: &mut BufWriter<File>, + ) -> Result<Option<String>, SoftwareError>; + + async fn apply( + &self, + update: &SoftwareModuleUpdate, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError> { + match update.clone() { + SoftwareModuleUpdate::Install { mut module } => { + let module_url = module.url.clone(); + match module_url { + Some(url) => self.install_from_url(&mut module, &url, logger).await?, + None => self.install(&module, logger).await?, + } + + Ok(()) + } + SoftwareModuleUpdate::Remove { module } => self.remove(&module, logger).await, + } + } + + async fn apply_all( + &self, + mut updates: Vec<SoftwareModuleUpdate>, + logger: &mut BufWriter<File>, + ) -> Vec<SoftwareError> { + let mut failed_updates = Vec::new(); + + // Prepare the updates + if let Err(prepare_error) = self.prepare(logger).await { + failed_updates.push(prepare_error); + return failed_updates; + } + + // Download all modules for which a download URL is provided + let mut downloaders = Vec::new(); + for update in updates.iter_mut() { + let module = match update { + SoftwareModuleUpdate::Remove { module } => module, + SoftwareModuleUpdate::Install { module } => module, + }; + let module_url = module.url.clone(); + if let Some(url) = module_url { + match Self::download_from_url(module, &url, logger).await { + Err(prepare_error) => { + failed_updates.push(prepare_error); + break; + } + Ok(downloader) => downloaders.push(downloader), + } + } + } + + // Execute the updates + if failed_updates.is_empty() { + let outcome = self.update_list(&updates, logger).await; + if let Err(SoftwareError::UpdateListNotSupported(_)) = outcome { + for update in updates.iter() { + if let Err(error) = self.apply(update, logger).await { + failed_updates.push(error); + }; + } + } else if let Err(update_list_error) = outcome { + failed_updates.push(update_list_error); + } + } + + // Finalize the updates + if let Err(finalize_error) = self.finalize(logger).await { + failed_updates.push(finalize_error); + } + + // Cleanup all the downloaded modules + for downloader in downloaders { + if let Err(cleanup_error) = Self::cleanup_downloaded_artefacts(downloader, logger).await + { + failed_updates.push(cleanup_error); + } + } + + failed_updates + } + + async fn install_from_url( + &self, + module: &mut SoftwareModule, + url: &DownloadInfo, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError> { + let downloader = Self::download_from_url(module, url, logger).await?; + let result = self.install(module, logger).await; + Self::cleanup_downloaded_artefacts(downloader, logger).await?; + + result + } + + async fn download_from_url( + module: &mut SoftwareModule, + url: &DownloadInfo, + logger: &mut BufWriter<File>, + ) -> Result<Downloader, SoftwareError> { + let downloader = Downloader::new(&module.name, &module.version, "/tmp"); + + logger + .write_all( + format!( + "----- $ Downloading: {} to {} \n", + &url.url(), + &downloader.filename().to_string_lossy().to_string() + ) + .as_bytes(), + ) + .await?; + + if let Err(err) = + downloader + .download(url) + .await + .map_err(|err| SoftwareError::DownloadError { + reason: err.to_string(), + url: url.url().to_string(), + }) + { + error!("Download error: {}", &err); + logger + .write_all(format!("error: {}\n", &err).as_bytes()) + .await?; + return Err(err); + } + + module.file_path = Some(downloader.filename().to_owned()); + + Ok(downloader) + } + + async fn cleanup_downloaded_artefacts( + downloader: Downloader, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError> { + if let Err(err) = downloader + .cleanup() + .await + .map_err(|err| SoftwareError::IoError { + reason: err.to_string(), + }) + { + logger + .write_all(format!("warn: {}\n", &err).as_bytes()) + .await?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct ExternalPluginCommand { + pub name: SoftwareType, + pub path: PathBuf, + pub sudo: Option<PathBuf>, +} + +impl ExternalPluginCommand { + pub fn new(name: impl Into<SoftwareType>, path: impl Into<PathBuf>) -> ExternalPluginCommand { + ExternalPluginCommand { + name: name.into(), + path: path.into(), + sudo: Some("sudo".into()), + } + } + + pub fn command( + &self, + action: &str, + maybe_module: Option<&SoftwareModule>, + ) -> Result<LoggedCommand, SoftwareError> { + let mut command = if let Some(sudo) = &self.sudo { + let mut command = LoggedCommand::new(sudo); + command.arg(&self.path); + command + } else { + LoggedCommand::new(&self.path) + }; + command.arg(action); + + if let Some(module) = maybe_module { + self.check_module_type(module)?; + command.arg(&module.name); + if let Some(ref version) = module.version { + command.arg("--module-version"); + command.arg(version); + } + + if let Some(ref path) = module.file_path { + command.arg("--file"); + command.arg(path); + } + } + + Ok(command) + } + + pub async fn execute( + &self, + command: LoggedCommand, + logger: &mut BufWriter<File>, + ) -> Result<Output, SoftwareError> { + let output = command + .execute(logger) + .await + .map_err(|err| self.plugin_error(err))?; + Ok(output) + } + + pub fn content(&self, bytes: Vec<u8>) -> Result<String, SoftwareError> { + String::from_utf8(bytes).map_err(|err| self.plugin_error(err)) + } + + pub fn plugin_error(&self, err: impl std::fmt::Display) -> SoftwareError { + SoftwareError::Plugin { + software_type: self.name.clone(), + reason: format!("{}", err), + } + } + + /// This test validates if an incoming module can be handled by it, by matching the module type with the plugin type + pub fn check_module_type(&self, module: &SoftwareModule) -> Result<(), SoftwareError> { + match &module.module_type { + Some(name) if name == &self.name.clone() => Ok(()), + Some(name) if name == DEFAULT => Ok(()), + Some(name) => Err(SoftwareError::WrongModuleType { + actual: self.name.clone(), + expected: name.clone(), + }), + None => Ok(()), // A software module without a type can be handled by any plugin that's configured as default plugin + } + } +} + +const PREPARE: &str = "prepare"; +const INSTALL: &str = "install"; +const REMOVE: &str = "remove"; +const UPDATE_LIST: &str = "update-list"; +const FINALIZE: &str = "finalize"; +pub const LIST: &str = "list"; +const VERSION: &str = "version"; + +#[async_trait] +impl Plugin for ExternalPluginCommand { + async fn prepare(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError> { + let command = self.command(PREPARE, None)?; + let output = self.execute(command, logger).await?; + + if output.status.success() { + Ok(()) + } else { + Err(SoftwareError::Prepare { + software_type: self.name.clone(), + reason: self.content(output.stderr)?, + }) + } + } + + async fn install( + &self, + module: &SoftwareModule, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError> { + let command = self.command(INSTALL, Some(module))?; + let output = self.execute(command, logger).await?; + + if output.status.success() { + Ok(()) + } else { + Err(SoftwareError::Install { + module: module.clone(), + reason: self.content(output.stderr)?, + }) + } + } + + async fn remove( + &self, + module: &SoftwareModule, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError> { + let command = self.command(REMOVE, Some(module))?; + let output = self.execute(command, logger).await?; + + if output.status.success() { + Ok(()) + } else { + Err(SoftwareError::Remove { + module: module.clone(), + reason: self.content(output.stderr)?, + }) + } + } + + async fn update_list( + &self, + updates: &Vec<SoftwareModuleUpdate>, + logger: &mut BufWriter<File>, + ) -> Result<(), SoftwareError> { + let mut command = self.command(UPDATE_LIST, None)?; + + let mut child = command.spawn()?; + let child_stdin = + child + .inner_child + .stdin + .as_mut() + .ok_or_else(|| SoftwareError::IoError { + reason: "Plugin stdin unavailable".into(), + })?; + + for update in updates { + let action = match update { + SoftwareModuleUpdate::Install { module } => { + format!( + "install\t{}\t{}\t{}\n", + module.name, + module.version.clone().map_or("".into(), |v| v), + module.file_path.clone().map_or("".into(), |v| v + .to_str() + .map_or("".into(), |u| u.to_string())) + ) + } + + SoftwareModuleUpdate::Remove { module } => { + format!( + "remove\t{}\t{}\t\n", + module.name, + module.version.clone().map_or("".into(), |v| v), + ) + } + }; + + child_stdin.write_all(action.as_bytes()).await? + } + + let output = child.wait_with_output(logger).await?; + match output.status.code() { + Some(0) => Ok(()), + Some(1) => Err(SoftwareError::UpdateListNotSupported(self.name.clone())), + Some(_) => Err(SoftwareError::UpdateList { + software_type: self.name.clone(), + reason: self.content(output.stderr)?, + }), + None => Err(SoftwareError::UpdateList { + software_type: self.name.clone(), + reason: "Interrupted".into(), + }), + } + } + + async fn finalize(&self, logger: &mut BufWriter<File>) -> Result<(), SoftwareError> { + let command = self.command(FINALIZE, None)?; + let output = self.execute(command, logger).await?; + + if output.status.success() { + Ok(()) + } else { + Err(SoftwareError::Finalize { + software_type: self.name.clone(), + reason: self.content(output.stderr)?, + }) + } + } + + async fn list( + &self, + logger: &mut BufWriter<File>, + ) -> Result<Vec<SoftwareModule>, SoftwareError> { + let command = self.command(LIST, None)?; + let output = self.execute(command, logger).await?; + if output.status.success() { + let mut software_list = Vec::new(); + let mut rdr = ReaderBuilder::new() + .has_headers(false) + .delimiter(b'\t') + .from_reader(output.stdout.as_slice()); + + for module in rdr.deserialize() { + let (name, version): (String, Option<String>) = module?; + software_list.push(SoftwareModule { + name, + version, + module_type: Some(self.name.clone()), + file_path: None, + url: None, + }); + } + + Ok(software_list) + } else { + Err(SoftwareError::Plugin { + software_type: self.name.clone(), + reason: self.content(output.stderr)?, + }) + } + } + + async fn version( + &self, + module: &SoftwareModule, + logger: &mut BufWriter<File>, + ) -> Result<Option<String>, SoftwareError> { + let command = self.command(VERSION, Some(module))?; + let output = self.execute(command, logger).await?; + + if output.status.success() { + let version = String::from(self.content(output.stdout)?.trim()); + if version.is_empty() { + Ok(None) + } else { + Ok(Some(version)) + } + } else { + Err(SoftwareError::Plugin { + software_type: self.name.clone(), + reason: self.content(output.stderr)?, + }) + } + } +} diff --git a/crates/core/plugin_sm/src/plugin_manager.rs b/crates/core/plugin_sm/src/plugin_manager.rs new file mode 100644 index 00000000..44e7b1bc --- /dev/null +++ b/crates/core/plugin_sm/src/plugin_manager.rs @@ -0,0 +1,273 @@ +use crate::plugin::{Plugin, LIST}; +use crate::{log_file::LogFile, plugin::ExternalPluginCommand}; +use json_sm::{ + SoftwareError, SoftwareListRequest, SoftwareListResponse, SoftwareType, SoftwareUpdateRequest, + SoftwareUpdateResponse, DEFAULT, +}; +use std::{ + collections::HashMap, + fs, + io::{self, ErrorKind}, + path::PathBuf, + process::{Command, Stdio}, +}; +use tedge_utils::paths::pathbuf_to_string; +use tracing::{error, info, warn}; + +/// The main responsibility of a `Plugins` implementation is to retrieve the appropriate plugin for a given software module. +pub trait Plugins { + type Plugin; + + /// Return the plugin to be used by default when installing a software module, if any. + fn default(&self) -> Option<&Self::Plugin>; + + /// Return the plugin declared with the given name, if any. + fn by_software_type(&self, software_type: &str) -> Option<&Self::Plugin>; + + /// Return the plugin associated with the file extension of the module name, if any. + fn by_file_extension(&self, module_name: &str) -> Option<&Self::Plugin>; + + fn plugin(&self, software_type: &str) -> Result<&Self::Plugin, SoftwareError> { + let module_plugin = self.by_software_type(software_type).ok_or_else(|| { + SoftwareError::UnknownSoftwareType { + software_type: software_type.into(), + } + })?; + + Ok(module_plugin) + } + + fn update_default(&mut self, new_default: &Option<SoftwareType>) -> Result<(), SoftwareError>; +} + +#[derive(Debug)] +pub struct ExternalPlugins { + plugin_dir: PathBuf, + plugin_map: HashMap<SoftwareType, ExternalPluginCommand>, + default_plugin_type: Option<SoftwareType>, + sudo: Option<PathBuf>, +} + +impl Plugins for ExternalPlugins { + type Plugin = ExternalPluginCommand; + + fn default(&self) -> Option<&Self::Plugin> { + if let Some(default_plugin_type) = &self.default_plugin_type { + self.by_software_type(default_plugin_type.as_str()) + } else if self.plugin_map.len() == 1 { + Some(self.plugin_map.iter().next().unwrap().1) //Unwrap is safe here as one entry is guaranteed + } else { + None + } + } + + fn update_default(&mut self, new_default: &Option<SoftwareType>) -> Result<(), SoftwareError> { + self.default_plugin_type = new_default.to_owned(); + Ok(()) + } + + fn by_software_type(&self, software_type: &str) -> Option<&Self::Plugin> { + if software_type.eq(DEFAULT) { + self.default() + } else { + self.plugin_map.get(software_type) |