summaryrefslogtreecommitdiffstats
path: root/crates/core/plugin_sm/src/plugin_manager.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/core/plugin_sm/src/plugin_manager.rs')
-rw-r--r--crates/core/plugin_sm/src/plugin_manager.rs273
1 files changed, 273 insertions, 0 deletions
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)
+ }
+ }
+
+ fn by_file_extension(&self, module_name: &str) -> Option<&Self::Plugin> {
+ if let Some(dot) = module_name.rfind('.') {
+ let (_, extension) = module_name.split_at(dot + 1);
+ self.by_software_type(extension)
+ } else {
+ self.default()
+ }
+ }
+}
+
+impl ExternalPlugins {
+ pub fn open(
+ plugin_dir: impl Into<PathBuf>,
+ default_plugin_type: Option<String>,
+ sudo: Option<PathBuf>,
+ ) -> Result<ExternalPlugins, SoftwareError> {
+ let mut plugins = ExternalPlugins {
+ plugin_dir: plugin_dir.into(),
+ plugin_map: HashMap::new(),
+ default_plugin_type: default_plugin_type.clone(),
+ sudo,
+ };
+ let () = plugins.load()?;
+
+ match default_plugin_type {
+ Some(default_plugin_type) => {
+ if plugins
+ .by_software_type(default_plugin_type.as_str())
+ .is_none()
+ {
+ warn!(
+ "The configured default plugin: {} not found",
+ default_plugin_type
+ );
+ }
+ info!("Default plugin type: {}", default_plugin_type)
+ }
+ None => {
+ info!("Default plugin type: Not configured")
+ }
+ }
+
+ Ok(plugins)
+ }
+
+ pub fn load(&mut self) -> io::Result<()> {
+ self.plugin_map.clear();
+ for maybe_entry in fs::read_dir(&self.plugin_dir)? {
+ let entry = maybe_entry?;
+ let path = entry.path();
+ if path.is_file() {
+ let mut command = if let Some(sudo) = &self.sudo {
+ let mut command = Command::new(sudo);
+ command.arg(&path);
+ command
+ } else {
+ Command::new(&path)
+ };
+
+ match command
+ .arg(LIST)
+ .stdout(Stdio::null())
+ .stderr(Stdio::null())
+ .status()
+ {
+ Ok(code) if code.success() => {
+ info!(
+ "Plugin activated: {}",
+ pathbuf_to_string(path.clone()).unwrap()
+ );
+ }
+
+ // If the file is not executable or returned non 0 status code we assume it is not a valid and skip further processing.
+ Ok(_) => {
+ error!(
+ "File {} in plugin directory does not support list operation and may not be a valid plugin, skipping.",
+ pathbuf_to_string(path.clone()).unwrap()
+ );
+ continue;
+ }
+
+ Err(err) if err.kind() == ErrorKind::PermissionDenied => {
+ error!(
+ "File {} Permission Denied, is the file an executable?\n
+ The file will not be registered as a plugin.",
+ pathbuf_to_string(path.clone()).unwrap()
+ );
+ continue;
+ }
+
+ Err(err) => {
+ error!(
+ "An error occurred while trying to run: {}: {}\n
+ The file will not be registered as a plugin.",
+ pathbuf_to_string(path.clone()).unwrap(),
+ err
+ );
+ continue;
+ }
+ }
+
+ if let Some(file_name) = path.file_name() {
+ if let Some(plugin_name) = file_name.to_str() {
+ let plugin = ExternalPluginCommand::new(plugin_name, &path);
+ self.plugin_map.insert(plugin_name.into(), plugin);
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn empty(&self) -> bool {
+ self.plugin_map.is_empty()
+ }
+
+ pub async fn list(
+ &self,
+ request: &SoftwareListRequest,
+ mut log_file: LogFile,
+ ) -> SoftwareListResponse {
+ let mut response = SoftwareListResponse::new(request);
+ let mut logger = log_file.buffer();
+ let mut error_count = 0;
+
+ for (software_type, plugin) in self.plugin_map.iter() {
+ match plugin.list(&mut logger).await {
+ Ok(software_list) => response.add_modules(&software_type, software_list),
+ Err(_) => {
+ error_count += 1;
+ }
+ }
+ }
+
+ if let Some(reason) = ExternalPlugins::error_message(log_file.path(), error_count) {
+ response.set_error(&reason);
+ }
+
+ response
+ }
+
+ pub async fn process(
+ &self,
+ request: &SoftwareUpdateRequest,
+ mut log_file: LogFile,
+ ) -> SoftwareUpdateResponse {
+ let mut response = SoftwareUpdateResponse::new(request);
+ let mut logger = log_file.buffer();
+ let mut error_count = 0;
+
+ for software_type in request.modules_types() {
+ let errors = if let Some(plugin) = self.by_software_type(&software_type) {
+ let updates = request.updates_for(&software_type);
+ plugin.apply_all(updates, &mut logger).await
+ } else {
+ vec![SoftwareError::UnknownSoftwareType {
+ software_type: software_type.clone(),
+ }]
+ };
+
+ if !errors.is_empty() {
+ error_count += 1;
+ response.add_errors(&software_type, errors);
+ }
+ }
+
+ for (software_type, plugin) in self.plugin_map.iter() {
+ match plugin.list(&mut logger).await {
+ Ok(software_list) => response.add_modules(software_type, software_list),
+ Err(err) => {
+ error_count += 1;
+ response.add_errors(software_type, vec![err])
+ }
+ }
+ }
+
+ if let Some(reason) = ExternalPlugins::error_message(log_file.path(), error_count) {
+ response.set_error(&reason);
+ }
+
+ response
+ }
+
+ fn error_message(log_file: &str, error_count: i32) -> Option<String> {
+ if error_count > 0 {
+ let reason = if error_count == 1 {
+ format!("1 error, see device log file {}", log_file)
+ } else {
+ format!("{} errors, see device log file {}", error_count, log_file)
+ };
+ Some(reason)
+ } else {
+ None
+ }
+ }
+}