1pub mod abi;
24pub mod capabilities;
25pub mod dylib;
26pub mod sandbox;
27pub mod wasm;
28
29use async_trait::async_trait;
30use parking_lot::RwLock;
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33use std::path::Path;
34use std::sync::Arc;
35
36use crate::types::Layer4Result;
37pub use abi::StablePluginMeta;
38pub use capabilities::{Capability, CapabilitySet};
39pub use dylib::{DylibLoader, PluginCreateFn, PluginDestroyFn, PluginMetaFn};
40pub use sandbox::PluginSandbox;
41pub use wasm::WasmLoader;
42
43#[async_trait]
45pub trait Plugin: Send + Sync {
46 fn name(&self) -> &str;
48
49 fn version(&self) -> &str;
51
52 fn description(&self) -> &str {
54 ""
55 }
56
57 fn dependencies(&self) -> Vec<&str> {
59 Vec::new()
60 }
61
62 async fn initialize(&self, context: &PluginContext) -> Layer4Result<()>;
64
65 async fn execute(&self, input: &serde_json::Value) -> Layer4Result<serde_json::Value>;
67
68 async fn shutdown(&self) -> Layer4Result<()> {
70 Ok(())
71 }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
79#[repr(C)]
80#[allow(improper_ctypes_definitions)] pub struct PluginMeta {
82 pub name: String,
83 pub version: String,
84 pub author: String,
85 pub description: String,
86 pub dependencies: Vec<String>,
87 pub entry_point: String,
88}
89
90impl Default for PluginMeta {
91 fn default() -> Self {
92 Self {
93 name: "unknown".to_string(),
94 version: "0.1.0".to_string(),
95 author: "unknown".to_string(),
96 description: String::new(),
97 dependencies: Vec::new(),
98 entry_point: "main".to_string(),
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum PluginState {
106 Unloaded,
107 Loaded,
108 Initialized,
109 Running,
110 Error,
111 Shutdown,
112}
113
114#[derive(Debug, Clone)]
116pub struct PluginInfo {
117 pub meta: PluginMeta,
118 pub state: PluginState,
119 pub path: std::path::PathBuf,
120 pub loaded_at: Option<chrono::DateTime<chrono::Utc>>,
121}
122
123#[derive(Debug, Clone)]
125pub struct PluginContext {
126 pub plugin_name: String,
127 pub config: serde_json::Value,
128 pub data_dir: std::path::PathBuf,
129}
130
131impl PluginContext {
132 pub fn new(plugin_name: impl Into<String>, data_dir: impl Into<std::path::PathBuf>) -> Self {
133 Self {
134 plugin_name: plugin_name.into(),
135 config: serde_json::Value::Null,
136 data_dir: data_dir.into(),
137 }
138 }
139
140 pub fn with_config(mut self, config: serde_json::Value) -> Self {
141 self.config = config;
142 self
143 }
144}
145
146pub struct PluginRegistry {
148 plugins: RwLock<HashMap<String, PluginInfo>>,
149 instances: RwLock<HashMap<String, Box<dyn Plugin>>>,
150}
151
152impl PluginRegistry {
153 pub fn new() -> Self {
154 Self {
155 plugins: RwLock::new(HashMap::new()),
156 instances: RwLock::new(HashMap::new()),
157 }
158 }
159
160 pub fn register(&self, plugin: Box<dyn Plugin>, path: &Path) -> Layer4Result<()> {
162 let name = plugin.name().to_string();
163 let meta = PluginMeta {
164 name: name.clone(),
165 version: plugin.version().to_string(),
166 description: plugin.description().to_string(),
167 dependencies: plugin
168 .dependencies()
169 .iter()
170 .map(|s| s.to_string())
171 .collect(),
172 ..Default::default()
173 };
174
175 let info = PluginInfo {
176 meta,
177 state: PluginState::Loaded,
178 path: path.to_path_buf(),
179 loaded_at: Some(chrono::Utc::now()),
180 };
181
182 self.plugins.write().insert(name.clone(), info);
183 self.instances.write().insert(name, plugin);
184
185 Ok(())
186 }
187
188 pub fn unregister(&self, name: &str) -> Layer4Result<bool> {
190 self.plugins.write().remove(name);
191 Ok(self.instances.write().remove(name).is_some())
192 }
193
194 pub fn get_info(&self, name: &str) -> Option<PluginInfo> {
196 self.plugins.read().get(name).cloned()
197 }
198
199 pub fn get(&self, _name: &str) -> Option<Arc<dyn Plugin>> {
201 None
204 }
205
206 pub fn list(&self) -> Vec<PluginInfo> {
208 self.plugins.read().values().cloned().collect()
209 }
210
211 pub fn update_state(&self, name: &str, state: PluginState) {
213 if let Some(info) = self.plugins.write().get_mut(name) {
214 info.state = state;
215 }
216 }
217
218 pub fn count(&self) -> usize {
220 self.plugins.read().len()
221 }
222}
223
224impl Default for PluginRegistry {
225 fn default() -> Self {
226 Self::new()
227 }
228}
229
230pub struct PluginLoader {
232 registry: PluginRegistry,
233 dylib_loader: DylibLoader,
234 wasm_loader: WasmLoader,
235 plugin_dir: std::path::PathBuf,
236}
237
238impl PluginLoader {
239 pub fn new(plugin_dir: impl Into<std::path::PathBuf>) -> Self {
241 Self {
242 registry: PluginRegistry::new(),
243 dylib_loader: DylibLoader::new(),
244 wasm_loader: WasmLoader::new().expect("Failed to create WasmLoader"),
245 plugin_dir: plugin_dir.into(),
246 }
247 }
248
249 pub fn with_default_dir() -> Self {
251 Self::new("~/.continuum/plugins")
252 }
253
254 pub async fn load_dylib(&self, path: &Path) -> Layer4Result<String> {
260 let (name, meta) = self.dylib_loader.load_safe(path)?;
261
262 let info = PluginInfo {
263 meta,
264 state: PluginState::Loaded,
265 path: path.to_path_buf(),
266 loaded_at: Some(chrono::Utc::now()),
267 };
268
269 self.registry.plugins.write().insert(name.clone(), info);
270 self.registry.update_state(&name, PluginState::Loaded);
271
272 Ok(name)
273 }
274
275 pub async fn load(&self, path: &Path) -> Layer4Result<String> {
277 let ext = path.extension().and_then(|e| e.to_str());
279
280 match ext {
281 Some("so") | Some("dylib") | Some("dll") => self.load_dylib(path).await,
282 Some("wasm") => self.load_wasm(path).await,
283 _ => {
284 let ext_display = ext.unwrap_or("(no extension)");
285 Err(anyhow::anyhow!(
286 "Unsupported plugin extension '{}'. Supported formats: .so, .dylib, .dll (native), .wasm (WebAssembly)",
287 ext_display
288 ))
289 }
290 }
291 }
292
293 pub async fn load_wasm(&self, path: &Path) -> Layer4Result<String> {
295 let capabilities = CapabilitySet::sandboxed();
296 let name = self.wasm_loader.load(path, capabilities)?;
297 self.registry.update_state(&name, PluginState::Loaded);
298 Ok(name)
299 }
300
301 pub async fn load_dir(&self) -> Layer4Result<Vec<String>> {
303 let mut loaded = Vec::new();
304
305 if let Ok(entries) = std::fs::read_dir(&self.plugin_dir) {
306 for entry in entries.flatten() {
307 let path = entry.path();
308 let ext = path.extension().and_then(|e| e.to_str());
309
310 if matches!(ext, Some("so") | Some("dylib") | Some("dll") | Some("wasm")) {
312 if let Ok(name) = self.load(&path).await {
313 loaded.push(name);
314 }
315 }
316 }
317 }
318
319 Ok(loaded)
320 }
321
322 pub fn get(&self, name: &str) -> Option<PluginInfo> {
324 self.registry.get_info(name)
325 }
326
327 pub fn get_meta(&self, name: &str) -> Option<PluginMeta> {
329 self.dylib_loader.get_meta(name)
330 }
331
332 pub async fn initialize(&self, name: &str, context: &PluginContext) -> Layer4Result<()> {
334 let config_json = serde_json::to_string(&context.config).unwrap_or_default();
336 match self.dylib_loader.call_initialize(name, &config_json) {
337 Some(true) => {
338 tracing::debug!("Plugin {} initialized via FFI", name);
339 }
340 Some(false) => {
341 tracing::warn!("Plugin {} FFI initialize returned failure", name);
342 }
343 None => {
344 tracing::debug!("Plugin {} has no FFI initialize function", name);
346 }
347 }
348
349 self.registry.update_state(name, PluginState::Initialized);
350 Ok(())
351 }
352
353 pub async fn reload(&self, name: &str) -> Layer4Result<()> {
355 self.dylib_loader.unload(name)?;
357
358 let info = self.registry.get_info(name);
360 if let Some(info) = info {
361 self.load_dylib(&info.path).await?;
362 }
363
364 Ok(())
365 }
366
367 pub async fn unload(&self, name: &str) -> Layer4Result<()> {
369 self.registry.update_state(name, PluginState::Shutdown);
370 self.dylib_loader.unload(name)?;
371 self.registry.unregister(name)?;
372 Ok(())
373 }
374
375 pub fn list(&self) -> Vec<PluginInfo> {
377 self.registry.list()
378 }
379
380 pub fn count(&self) -> usize {
382 self.registry.count()
383 }
384
385 pub fn render_status(&self) -> String {
387 let plugins = self.registry.list();
388 let mut output = String::new();
389
390 output.push_str("Plugins:\n");
391
392 if plugins.is_empty() {
393 output.push_str(" No plugins loaded\n");
394 } else {
395 for info in plugins {
396 let status = match info.state {
397 PluginState::Unloaded => "⚪",
398 PluginState::Loaded => "🔵",
399 PluginState::Initialized => "🟢",
400 PluginState::Running => "🟡",
401 PluginState::Error => "🔴",
402 PluginState::Shutdown => "⚫",
403 };
404 output.push_str(&format!(
405 " {} {} v{}\n",
406 status, info.meta.name, info.meta.version
407 ));
408 }
409 }
410
411 output
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_plugin_registry_creation() {
421 let registry = PluginRegistry::new();
422 assert_eq!(registry.count(), 0);
423 }
424
425 #[test]
426 fn test_plugin_context_creation() {
427 let ctx = PluginContext::new("test-plugin", "/tmp/plugins");
428 assert_eq!(ctx.plugin_name, "test-plugin");
429 }
430
431 #[test]
432 fn test_plugin_loader_creation() {
433 let loader = PluginLoader::with_default_dir();
434 assert_eq!(loader.count(), 0);
435 }
436
437 #[test]
438 fn test_plugin_meta_default() {
439 let meta = PluginMeta::default();
440 assert_eq!(meta.name, "unknown");
441 assert_eq!(meta.version, "0.1.0");
442 }
443
444 #[tokio::test]
445 async fn test_unknown_plugin_extension_returns_error() {
446 let dir = tempfile::tempdir().unwrap();
447 let plugin_path = dir.path().join("plugin.txt");
448 std::fs::write(&plugin_path, b"not a plugin").unwrap();
449
450 let loader = PluginLoader::new(dir.path());
451 let err = loader.load(&plugin_path).await.unwrap_err();
452 let message = err.to_string();
453
454 assert!(message.contains("Unsupported plugin extension"));
455 assert!(message.contains(".wasm"));
456 }
457}