use std::{
collections::HashMap,
fmt::Display,
fs, io,
path::{Path, PathBuf},
};
use abi_stable::{
library::{lib_header_from_path, LibrarySuffix, RawLibrary},
reexports::SelfOps,
sabi_trait::TD_Opaque,
std_types::ROption::{RNone, RSome},
};
use anyhow::anyhow;
use phaneron_plugin::{
traits::{CreateNodeDescription, PluginNodeDescription},
types::Node,
types::NodeContext,
types::NodeHandle,
types::PhaneronPlugin,
PhaneronLoggingContext, PhaneronLoggingContext_TO, PhaneronPluginContext,
PhaneronPluginRootModuleRef,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize)]
pub struct DevPluginManifest {
plugins: Vec<String>,
}
#[derive(Default)]
pub struct PluginManager {
plugins: HashMap<PluginId, PhaneronPlugin>,
nodes_provided_by_plugins: HashMap<String, PluginId>,
node_descriptions: HashMap<String, PluginNodeDescription>,
}
pub enum PluginLoadType {
Development(DevPluginManifest),
Production { plugins_directory: String },
}
impl PluginManager {
pub fn initialize(&mut self, load_type: PluginLoadType) -> anyhow::Result<usize> {
let (plugins_to_load, plugins_directory) = match load_type {
PluginLoadType::Development(manifest) => (manifest.plugins, None),
PluginLoadType::Production { plugins_directory } => {
let mut plugins = vec![];
let paths = fs::read_dir(plugins_directory.clone()).unwrap();
for path in paths.flatten() {
if let Ok(metadata) = path.metadata() {
if metadata.is_file() {
if let Some(name) = path.file_name().to_str() {
plugins.push(name.to_string())
}
}
}
}
(plugins, Some(plugins_directory))
}
};
let mut loaded_plugins = 0;
for plugin in plugins_to_load {
self.load_plugin(&plugins_directory, &plugin)?;
loaded_plugins += 1;
}
Ok(loaded_plugins)
}
fn load_plugin(
&mut self,
plugins_dir: &Option<String>,
plugin_name: &str,
) -> anyhow::Result<()> {
let library_path: PathBuf = match compute_plugin_path(plugins_dir, plugin_name) {
Ok(x) => x,
Err(e) => return Err(anyhow!(e)),
};
let res = (|| {
let header = lib_header_from_path(&library_path)?;
header.init_root_module::<PhaneronPluginRootModuleRef>()
})();
let root_module = match res {
Ok(x) => x,
Err(e) => return Err(anyhow!(e)),
};
let logger = PluginLogger {
plugin_name: plugin_name.to_string(),
};
let logger = PhaneronLoggingContext_TO::from_value(logger, TD_Opaque);
let plugin_context = PhaneronPluginContext::new(logger);
let plugin = root_module.load()(plugin_context)
.map_err(|err| anyhow!(err.to_string()))
.into_result()?;
let nodes = plugin.get_available_node_types();
let plugin_id = PluginId::default();
for node_type in nodes {
self.nodes_provided_by_plugins
.insert(node_type.id.to_string(), plugin_id.clone());
self.node_descriptions
.insert(node_type.id.to_string(), node_type);
}
self.plugins.insert(plugin_id, plugin);
Ok(())
}
pub fn create_node_handle(
&self,
node_id: String,
node_type: String,
) -> Result<NodeHandle, String> {
let plugin_id = self.nodes_provided_by_plugins.get(&node_type).unwrap();
let plugin = self.plugins.get(plugin_id).unwrap();
plugin
.create_node(CreateNodeDescription {
node_type: node_type.into(),
node_id: node_id.into(),
})
.map_err(|err| err.into())
.into()
}
pub fn initialize_node(
&self,
context: NodeContext,
node_handle: NodeHandle,
configuration: Option<String>,
) -> Option<Node> {
let configuration = match configuration {
Some(config) => RSome(config.into()),
None => RNone,
};
Some(node_handle.initialize(context, configuration))
}
}
fn compute_plugin_path(plugins_dir: &Option<String>, base_name: &str) -> io::Result<PathBuf> {
if let Some(plugins_dir) = plugins_dir {
let plugins_dir = plugins_dir.as_ref_::<Path>().into_::<PathBuf>();
let plugins_path =
RawLibrary::path_in_directory(&plugins_dir, base_name, LibrarySuffix::NoSuffix);
return Ok(plugins_path);
}
let debug_dir = "target/debug/".as_ref_::<Path>().into_::<PathBuf>();
let release_dir = "target/release/".as_ref_::<Path>().into_::<PathBuf>();
let debug_path = RawLibrary::path_in_directory(&debug_dir, base_name, LibrarySuffix::NoSuffix);
let release_path =
RawLibrary::path_in_directory(&release_dir, base_name, LibrarySuffix::NoSuffix);
match (debug_path.exists(), release_path.exists()) {
(false, false) => debug_path,
(true, false) => debug_path,
(false, true) => release_path,
(true, true) => {
if debug_path.metadata()?.modified()? < release_path.metadata()?.modified()? {
release_path
} else {
debug_path
}
}
}
.piped(Ok)
}
#[derive(Debug, Clone)]
struct PluginLogger {
plugin_name: String,
}
impl PhaneronLoggingContext for PluginLogger {
fn log(&self, level: phaneron_plugin::LogLevel, message: abi_stable::std_types::RString) {
match level {
phaneron_plugin::LogLevel::Error => {
tracing::error!("PLUGIN {}: {}", self.plugin_name, message.to_string())
}
phaneron_plugin::LogLevel::Warn => {
tracing::warn!("PLUGIN {}: {}", self.plugin_name, message.to_string())
}
phaneron_plugin::LogLevel::Info => {
tracing::info!("PLUGIN {}: {}", self.plugin_name, message.to_string())
}
phaneron_plugin::LogLevel::Debug => {
tracing::debug!("PLUGIN {}: {}", self.plugin_name, message.to_string())
}
phaneron_plugin::LogLevel::Trace => {
tracing::trace!("PLUGIN {}: {}", self.plugin_name, message.to_string())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PluginId(String);
impl PluginId {
pub fn new_from(id: String) -> Self {
Self(id)
}
}
impl Default for PluginId {
fn default() -> Self {
Self(uuid::Uuid::new_v4().to_string())
}
}
impl Display for PluginId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}