use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use lazy_static::lazy_static;
use spin::Mutex;
use super::error::PluginError;
use super::host::HostState;
use super::runtime::WasmPlugin;
use super::types::{HookType, PluginContext, PluginInfo, PluginLimits, PluginManifest};
lazy_static! {
static ref PLUGINS: Mutex<BTreeMap<String, WasmPlugin>> = Mutex::new(BTreeMap::new());
static ref DATASET_HOOKS: Mutex<BTreeMap<String, Vec<DatasetHook>>> = Mutex::new(BTreeMap::new());
static ref PLUGIN_CONFIG: Mutex<BTreeMap<String, BTreeMap<String, String>>> = Mutex::new(BTreeMap::new());
}
#[derive(Debug, Clone)]
struct DatasetHook {
plugin_name: String,
hook: HookType,
priority: i32,
}
pub struct PluginManager;
impl PluginManager {
pub fn load_plugin(name: &str, wasm_bytes: &[u8]) -> Result<PluginManifest, PluginError> {
Self::load_plugin_with_limits(name, wasm_bytes, PluginLimits::default())
}
pub fn load_plugin_with_limits(
name: &str,
wasm_bytes: &[u8],
limits: PluginLimits,
) -> Result<PluginManifest, PluginError> {
let mut plugins = PLUGINS.lock();
if plugins.contains_key(name) {
return Err(PluginError::AlreadyLoaded(name.into()));
}
let plugin = WasmPlugin::load_with_limits(name, wasm_bytes, limits)?;
let manifest = plugin.manifest().clone();
plugins.insert(name.into(), plugin);
Ok(manifest)
}
pub fn unload_plugin(name: &str) -> Result<(), PluginError> {
let mut plugins = PLUGINS.lock();
let mut plugin = plugins
.remove(name)
.ok_or_else(|| PluginError::NotFound(name.into()))?;
plugin.destroy()?;
let mut hooks = DATASET_HOOKS.lock();
for (_, dataset_hooks) in hooks.iter_mut() {
dataset_hooks.retain(|h| h.plugin_name != name);
}
Ok(())
}
pub fn list_plugins() -> Vec<PluginInfo> {
let plugins = PLUGINS.lock();
plugins
.values()
.map(|p| {
let mut info = PluginInfo::from(p.manifest());
info.invocation_count = p.invocation_count();
info
})
.collect()
}
pub fn get_plugin(name: &str) -> Option<PluginInfo> {
let plugins = PLUGINS.lock();
plugins.get(name).map(|p| {
let mut info = PluginInfo::from(p.manifest());
info.invocation_count = p.invocation_count();
info
})
}
pub fn attach(
dataset: &str,
plugin_name: &str,
hooks: &[HookType],
config: &BTreeMap<String, String>,
) -> Result<(), PluginError> {
Self::attach_with_priority(dataset, plugin_name, hooks, config, 0)
}
pub fn attach_with_priority(
dataset: &str,
plugin_name: &str,
hooks: &[HookType],
config: &BTreeMap<String, String>,
priority: i32,
) -> Result<(), PluginError> {
{
let plugins = PLUGINS.lock();
if !plugins.contains_key(plugin_name) {
return Err(PluginError::NotFound(plugin_name.into()));
}
}
if !config.is_empty() {
let mut configs = PLUGIN_CONFIG.lock();
let key = alloc::format!("{}:{}", dataset, plugin_name);
configs.insert(key, config.clone());
}
let mut dataset_hooks = DATASET_HOOKS.lock();
let entry = dataset_hooks.entry(dataset.into()).or_default();
for hook in hooks {
let already_attached = entry
.iter()
.any(|h| h.plugin_name == plugin_name && h.hook == *hook);
if !already_attached {
entry.push(DatasetHook {
plugin_name: plugin_name.into(),
hook: *hook,
priority,
});
}
}
entry.sort_by_key(|h| h.priority);
Ok(())
}
pub fn detach(dataset: &str, plugin_name: &str) -> Result<(), PluginError> {
let mut hooks = DATASET_HOOKS.lock();
if let Some(dataset_hooks) = hooks.get_mut(dataset) {
dataset_hooks.retain(|h| h.plugin_name != plugin_name);
}
let mut configs = PLUGIN_CONFIG.lock();
let key = alloc::format!("{}:{}", dataset, plugin_name);
configs.remove(&key);
Ok(())
}
pub fn list_dataset_hooks(dataset: &str) -> Vec<(String, HookType)> {
let hooks = DATASET_HOOKS.lock();
hooks
.get(dataset)
.map(|hs| hs.iter().map(|h| (h.plugin_name.clone(), h.hook)).collect())
.unwrap_or_default()
}
pub fn execute_hook(
dataset: &str,
hook: HookType,
ctx: &PluginContext,
data: &[u8],
) -> Result<Vec<u8>, PluginError> {
let hooks = DATASET_HOOKS.lock();
let dataset_hooks = match hooks.get(dataset) {
Some(h) => h.clone(),
None => return Ok(data.to_vec()),
};
drop(hooks);
let mut result = data.to_vec();
for dh in dataset_hooks.iter().filter(|h| h.hook == hook) {
let mut plugins = PLUGINS.lock();
if let Some(plugin) = plugins.get_mut(&dh.plugin_name) {
{
let configs = PLUGIN_CONFIG.lock();
let key = alloc::format!("{}:{}", dataset, dh.plugin_name);
if let Some(config) = configs.get(&key) {
let state = plugin.host_state_mut();
for (k, v) in config {
state.config.insert(k.clone(), v.clone());
}
}
}
result = plugin.process(ctx, &result)?;
}
}
Ok(result)
}
pub fn execute_notify_hook(
dataset: &str,
hook: HookType,
ctx: &PluginContext,
) -> Result<(), PluginError> {
let _ = Self::execute_hook(dataset, hook, ctx, &[])?;
Ok(())
}
pub fn has_hooks(dataset: &str, hook: HookType) -> bool {
let hooks = DATASET_HOOKS.lock();
hooks
.get(dataset)
.map(|hs| hs.iter().any(|h| h.hook == hook))
.unwrap_or(false)
}
#[cfg(test)]
pub fn clear_all() {
let mut plugins = PLUGINS.lock();
plugins.clear();
let mut hooks = DATASET_HOOKS.lock();
hooks.clear();
let mut configs = PLUGIN_CONFIG.lock();
configs.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
#[test]
fn test_dataset_hook_struct() {
let hook = DatasetHook {
plugin_name: "test".into(),
hook: HookType::PreWrite,
priority: 0,
};
assert_eq!(hook.plugin_name, "test");
assert_eq!(hook.hook, HookType::PreWrite);
}
#[test]
fn test_list_plugins_empty() {
let plugins = PluginManager::list_plugins();
let _ = plugins;
}
#[test]
fn test_attach_nonexistent_plugin() {
let result = PluginManager::attach(
"dataset",
"nonexistent",
&[HookType::PreWrite],
&BTreeMap::new(),
);
assert!(matches!(result, Err(PluginError::NotFound(_))));
}
#[test]
fn test_has_hooks_empty() {
assert!(!PluginManager::has_hooks(
"nonexistent_dataset",
HookType::PreWrite
));
}
#[test]
fn test_list_dataset_hooks_empty() {
let hooks = PluginManager::list_dataset_hooks("nonexistent");
assert!(hooks.is_empty());
}
#[test]
fn test_execute_hook_no_plugins() {
let ctx = PluginContext::new("/path", "dataset", "write");
let data = b"hello world";
let result = PluginManager::execute_hook("empty_dataset", HookType::PreWrite, &ctx, data);
assert!(result.is_ok());
assert_eq!(result.unwrap(), data);
}
}