pub mod abi;
pub mod capabilities;
pub mod dylib;
pub mod sandbox;
pub mod wasm;
use async_trait::async_trait;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use crate::types::Layer4Result;
pub use abi::StablePluginMeta;
pub use capabilities::{Capability, CapabilitySet};
pub use dylib::{DylibLoader, PluginCreateFn, PluginDestroyFn, PluginMetaFn};
pub use sandbox::PluginSandbox;
pub use wasm::WasmLoader;
#[async_trait]
pub trait Plugin: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn description(&self) -> &str {
""
}
fn dependencies(&self) -> Vec<&str> {
Vec::new()
}
async fn initialize(&self, context: &PluginContext) -> Layer4Result<()>;
async fn execute(&self, input: &serde_json::Value) -> Layer4Result<serde_json::Value>;
async fn shutdown(&self) -> Layer4Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[repr(C)]
#[allow(improper_ctypes_definitions)] pub struct PluginMeta {
pub name: String,
pub version: String,
pub author: String,
pub description: String,
pub dependencies: Vec<String>,
pub entry_point: String,
}
impl Default for PluginMeta {
fn default() -> Self {
Self {
name: "unknown".to_string(),
version: "0.1.0".to_string(),
author: "unknown".to_string(),
description: String::new(),
dependencies: Vec::new(),
entry_point: "main".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginState {
Unloaded,
Loaded,
Initialized,
Running,
Error,
Shutdown,
}
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub meta: PluginMeta,
pub state: PluginState,
pub path: std::path::PathBuf,
pub loaded_at: Option<chrono::DateTime<chrono::Utc>>,
}
#[derive(Debug, Clone)]
pub struct PluginContext {
pub plugin_name: String,
pub config: serde_json::Value,
pub data_dir: std::path::PathBuf,
}
impl PluginContext {
pub fn new(plugin_name: impl Into<String>, data_dir: impl Into<std::path::PathBuf>) -> Self {
Self {
plugin_name: plugin_name.into(),
config: serde_json::Value::Null,
data_dir: data_dir.into(),
}
}
pub fn with_config(mut self, config: serde_json::Value) -> Self {
self.config = config;
self
}
}
pub struct PluginRegistry {
plugins: RwLock<HashMap<String, PluginInfo>>,
instances: RwLock<HashMap<String, Box<dyn Plugin>>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: RwLock::new(HashMap::new()),
instances: RwLock::new(HashMap::new()),
}
}
pub fn register(&self, plugin: Box<dyn Plugin>, path: &Path) -> Layer4Result<()> {
let name = plugin.name().to_string();
let meta = PluginMeta {
name: name.clone(),
version: plugin.version().to_string(),
description: plugin.description().to_string(),
dependencies: plugin
.dependencies()
.iter()
.map(|s| s.to_string())
.collect(),
..Default::default()
};
let info = PluginInfo {
meta,
state: PluginState::Loaded,
path: path.to_path_buf(),
loaded_at: Some(chrono::Utc::now()),
};
self.plugins.write().insert(name.clone(), info);
self.instances.write().insert(name, plugin);
Ok(())
}
pub fn unregister(&self, name: &str) -> Layer4Result<bool> {
self.plugins.write().remove(name);
Ok(self.instances.write().remove(name).is_some())
}
pub fn get_info(&self, name: &str) -> Option<PluginInfo> {
self.plugins.read().get(name).cloned()
}
pub fn get(&self, _name: &str) -> Option<Arc<dyn Plugin>> {
None
}
pub fn list(&self) -> Vec<PluginInfo> {
self.plugins.read().values().cloned().collect()
}
pub fn update_state(&self, name: &str, state: PluginState) {
if let Some(info) = self.plugins.write().get_mut(name) {
info.state = state;
}
}
pub fn count(&self) -> usize {
self.plugins.read().len()
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
pub struct PluginLoader {
registry: PluginRegistry,
dylib_loader: DylibLoader,
wasm_loader: WasmLoader,
plugin_dir: std::path::PathBuf,
}
impl PluginLoader {
pub fn new(plugin_dir: impl Into<std::path::PathBuf>) -> Self {
Self {
registry: PluginRegistry::new(),
dylib_loader: DylibLoader::new(),
wasm_loader: WasmLoader::new().expect("Failed to create WasmLoader"),
plugin_dir: plugin_dir.into(),
}
}
pub fn with_default_dir() -> Self {
Self::new("~/.continuum/plugins")
}
pub async fn load_dylib(&self, path: &Path) -> Layer4Result<String> {
let (name, meta) = self.dylib_loader.load_safe(path)?;
let info = PluginInfo {
meta,
state: PluginState::Loaded,
path: path.to_path_buf(),
loaded_at: Some(chrono::Utc::now()),
};
self.registry.plugins.write().insert(name.clone(), info);
self.registry.update_state(&name, PluginState::Loaded);
Ok(name)
}
pub async fn load(&self, path: &Path) -> Layer4Result<String> {
let ext = path.extension().and_then(|e| e.to_str());
match ext {
Some("so") | Some("dylib") | Some("dll") => self.load_dylib(path).await,
Some("wasm") => self.load_wasm(path).await,
_ => {
let ext_display = ext.unwrap_or("(no extension)");
Err(anyhow::anyhow!(
"Unsupported plugin extension '{}'. Supported formats: .so, .dylib, .dll (native), .wasm (WebAssembly)",
ext_display
))
}
}
}
pub async fn load_wasm(&self, path: &Path) -> Layer4Result<String> {
let capabilities = CapabilitySet::sandboxed();
let name = self.wasm_loader.load(path, capabilities)?;
self.registry.update_state(&name, PluginState::Loaded);
Ok(name)
}
pub async fn load_dir(&self) -> Layer4Result<Vec<String>> {
let mut loaded = Vec::new();
if let Ok(entries) = std::fs::read_dir(&self.plugin_dir) {
for entry in entries.flatten() {
let path = entry.path();
let ext = path.extension().and_then(|e| e.to_str());
if matches!(ext, Some("so") | Some("dylib") | Some("dll") | Some("wasm")) {
if let Ok(name) = self.load(&path).await {
loaded.push(name);
}
}
}
}
Ok(loaded)
}
pub fn get(&self, name: &str) -> Option<PluginInfo> {
self.registry.get_info(name)
}
pub fn get_meta(&self, name: &str) -> Option<PluginMeta> {
self.dylib_loader.get_meta(name)
}
pub async fn initialize(&self, name: &str, context: &PluginContext) -> Layer4Result<()> {
let config_json = serde_json::to_string(&context.config).unwrap_or_default();
match self.dylib_loader.call_initialize(name, &config_json) {
Some(true) => {
tracing::debug!("Plugin {} initialized via FFI", name);
}
Some(false) => {
tracing::warn!("Plugin {} FFI initialize returned failure", name);
}
None => {
tracing::debug!("Plugin {} has no FFI initialize function", name);
}
}
self.registry.update_state(name, PluginState::Initialized);
Ok(())
}
pub async fn reload(&self, name: &str) -> Layer4Result<()> {
self.dylib_loader.unload(name)?;
let info = self.registry.get_info(name);
if let Some(info) = info {
self.load_dylib(&info.path).await?;
}
Ok(())
}
pub async fn unload(&self, name: &str) -> Layer4Result<()> {
self.registry.update_state(name, PluginState::Shutdown);
self.dylib_loader.unload(name)?;
self.registry.unregister(name)?;
Ok(())
}
pub fn list(&self) -> Vec<PluginInfo> {
self.registry.list()
}
pub fn count(&self) -> usize {
self.registry.count()
}
pub fn render_status(&self) -> String {
let plugins = self.registry.list();
let mut output = String::new();
output.push_str("Plugins:\n");
if plugins.is_empty() {
output.push_str(" No plugins loaded\n");
} else {
for info in plugins {
let status = match info.state {
PluginState::Unloaded => "⚪",
PluginState::Loaded => "🔵",
PluginState::Initialized => "🟢",
PluginState::Running => "🟡",
PluginState::Error => "🔴",
PluginState::Shutdown => "⚫",
};
output.push_str(&format!(
" {} {} v{}\n",
status, info.meta.name, info.meta.version
));
}
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_registry_creation() {
let registry = PluginRegistry::new();
assert_eq!(registry.count(), 0);
}
#[test]
fn test_plugin_context_creation() {
let ctx = PluginContext::new("test-plugin", "/tmp/plugins");
assert_eq!(ctx.plugin_name, "test-plugin");
}
#[test]
fn test_plugin_loader_creation() {
let loader = PluginLoader::with_default_dir();
assert_eq!(loader.count(), 0);
}
#[test]
fn test_plugin_meta_default() {
let meta = PluginMeta::default();
assert_eq!(meta.name, "unknown");
assert_eq!(meta.version, "0.1.0");
}
#[tokio::test]
async fn test_unknown_plugin_extension_returns_error() {
let dir = tempfile::tempdir().unwrap();
let plugin_path = dir.path().join("plugin.txt");
std::fs::write(&plugin_path, b"not a plugin").unwrap();
let loader = PluginLoader::new(dir.path());
let err = loader.load(&plugin_path).await.unwrap_err();
let message = err.to_string();
assert!(message.contains("Unsupported plugin extension"));
assert!(message.contains(".wasm"));
}
}