1use std::collections::HashMap;
7use std::future::Future;
8use std::path::Path;
9use std::pin::Pin;
10
11use tokio_util::sync::CancellationToken;
12
13use crate::config::{PluginConfig, PluginMetadata};
14use crate::hooks::{Hook, HookContext, HookResult};
15use crate::sandbox::SandboxConfig;
16use crate::types::{PluginError, PluginKind, PluginResult, Value};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct PluginHandle(pub(crate) usize);
21
22impl PluginHandle {
23 pub fn new(id: usize) -> Self {
25 Self(id)
26 }
27
28 pub fn id(&self) -> usize {
30 self.0
31 }
32}
33
34pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
36
37pub trait PluginRuntime: Send + Sync {
42 fn name(&self) -> &'static str;
44
45 fn file_extensions(&self) -> &'static [&'static str];
47
48 fn init(&mut self, config: &PluginConfig) -> PluginResult<()>;
50
51 fn load_plugin(&mut self, id: &str, source: &Path) -> PluginResult<PluginHandle>;
55
56 fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()>;
58
59 fn get_metadata(&self, handle: PluginHandle) -> Option<&PluginMetadata>;
61
62 fn has_hook(&self, handle: PluginHandle, hook_name: &str) -> bool;
64
65 fn call_hook_sync(
69 &self,
70 handle: PluginHandle,
71 hook: &Hook,
72 ctx: &HookContext,
73 ) -> PluginResult<HookResult>;
74
75 fn call_hook_async<'a>(
79 &'a self,
80 handle: PluginHandle,
81 hook: &'a Hook,
82 ctx: &'a HookContext,
83 ) -> BoxFuture<'a, PluginResult<HookResult>>;
84
85 fn call_method<'a>(
87 &'a self,
88 handle: PluginHandle,
89 method: &'a str,
90 args: Vec<Value>,
91 ) -> BoxFuture<'a, PluginResult<Value>>;
92
93 fn create_isolated_context(&self, sandbox: &SandboxConfig) -> PluginResult<Box<dyn IsolatedContext>>;
98
99 fn loaded_plugins(&self) -> Vec<PluginHandle>;
101
102 fn shutdown(&mut self) -> PluginResult<()>;
104}
105
106pub trait IsolatedContext: Send {
115 fn execute<'a>(
117 &'a self,
118 code: &'a [u8],
119 cancel: CancellationToken,
120 ) -> BoxFuture<'a, PluginResult<Value>>;
121
122 fn call_function<'a>(
124 &'a self,
125 name: &'a str,
126 args: Vec<Value>,
127 cancel: CancellationToken,
128 ) -> BoxFuture<'a, PluginResult<Value>>;
129
130 fn set_global(&mut self, name: &str, value: Value) -> PluginResult<()>;
132
133 fn get_global(&self, name: &str) -> PluginResult<Value>;
135}
136
137#[derive(Debug, Clone)]
139pub struct LoadedPlugin {
140 pub handle: PluginHandle,
142
143 pub id: String,
145
146 pub metadata: PluginMetadata,
148
149 pub path: std::path::PathBuf,
151
152 pub hooks: Vec<String>,
154}
155
156pub struct PluginManager {
158 runtimes: HashMap<String, Box<dyn PluginRuntime>>,
160
161 plugins: HashMap<PluginHandle, LoadedPlugin>,
163
164 config: PluginConfig,
166
167 #[allow(dead_code)]
169 next_handle: usize,
170}
171
172impl PluginManager {
173 pub fn new(config: PluginConfig) -> Self {
175 Self {
176 runtimes: HashMap::new(),
177 plugins: HashMap::new(),
178 config,
179 next_handle: 0,
180 }
181 }
182
183 pub fn register_runtime(&mut self, runtime: Box<dyn PluginRuntime>) -> PluginResult<()> {
185 let name = runtime.name().to_string();
186 self.runtimes.insert(name, runtime);
187 Ok(())
188 }
189
190 pub fn get_runtime(&self, name: &str) -> Option<&dyn PluginRuntime> {
192 self.runtimes.get(name).map(|r| r.as_ref())
193 }
194
195 pub fn get_runtime_mut(&mut self, name: &str) -> Option<&mut Box<dyn PluginRuntime>> {
197 self.runtimes.get_mut(name)
198 }
199
200 pub fn init_runtimes(&mut self) -> PluginResult<()> {
202 for runtime in self.runtimes.values_mut() {
203 runtime.init(&self.config)?;
204 }
205 Ok(())
206 }
207
208 pub async fn discover_plugins(&mut self) -> PluginResult<Vec<LoadedPlugin>> {
210 let plugin_dir = self.config.plugin_dir.clone();
211 if !plugin_dir.exists() {
212 return Ok(vec![]);
213 }
214
215 let mut loaded = vec![];
216
217 let entries = std::fs::read_dir(&plugin_dir).map_err(|e| PluginError::Io(e))?;
219
220 for entry in entries.flatten() {
221 let path = entry.path();
222 if !path.is_dir() {
223 continue;
224 }
225
226 let toml_path = path.join("plugin.toml");
228 if !toml_path.exists() {
229 continue;
230 }
231
232 let toml_content = std::fs::read_to_string(&toml_path)?;
234 let metadata: PluginMetadata = toml::from_str(&toml_content)
235 .map_err(|e| PluginError::ConfigError { message: e.to_string() })?;
236
237 let runtime_name = &metadata.runtime;
239 let runtime = self.runtimes.get_mut(runtime_name).ok_or_else(|| {
240 PluginError::RuntimeNotAvailable {
241 runtime: runtime_name.clone(),
242 }
243 })?;
244
245 let entry_file = path.join(&metadata.entry);
247 if !entry_file.exists() {
248 continue;
249 }
250
251 let handle = runtime.load_plugin(&metadata.name, &entry_file)?;
253
254 let hooks: Vec<String> = [
256 "on_navigate",
257 "on_drill_down",
258 "on_scan_start",
259 "on_scan_complete",
260 "on_delete_start",
261 "on_delete_complete",
262 "on_render",
263 "on_action",
264 "on_startup",
265 "on_shutdown",
266 ]
267 .iter()
268 .filter(|h| runtime.has_hook(handle, h))
269 .map(|s| s.to_string())
270 .collect();
271
272 let loaded_plugin = LoadedPlugin {
273 handle,
274 id: metadata.name.clone(),
275 metadata,
276 path: path.clone(),
277 hooks,
278 };
279
280 self.plugins.insert(handle, loaded_plugin.clone());
281 loaded.push(loaded_plugin);
282 }
283
284 Ok(loaded)
285 }
286
287 pub fn load_plugin(&mut self, path: &Path) -> PluginResult<LoadedPlugin> {
289 let toml_path = path.join("plugin.toml");
291 let toml_content = std::fs::read_to_string(&toml_path)?;
292 let metadata: PluginMetadata = toml::from_str(&toml_content)
293 .map_err(|e| PluginError::ConfigError { message: e.to_string() })?;
294
295 let runtime = self.runtimes.get_mut(&metadata.runtime).ok_or_else(|| {
297 PluginError::RuntimeNotAvailable {
298 runtime: metadata.runtime.clone(),
299 }
300 })?;
301
302 let entry_file = path.join(&metadata.entry);
304 let handle = runtime.load_plugin(&metadata.name, &entry_file)?;
305
306 let hooks: Vec<String> = [
308 "on_navigate",
309 "on_drill_down",
310 "on_scan_start",
311 "on_scan_complete",
312 "on_render",
313 ]
314 .iter()
315 .filter(|h| runtime.has_hook(handle, h))
316 .map(|s| s.to_string())
317 .collect();
318
319 let loaded_plugin = LoadedPlugin {
320 handle,
321 id: metadata.name.clone(),
322 metadata,
323 path: path.to_path_buf(),
324 hooks,
325 };
326
327 self.plugins.insert(handle, loaded_plugin.clone());
328 Ok(loaded_plugin)
329 }
330
331 pub fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()> {
333 if let Some(plugin) = self.plugins.remove(&handle) {
334 if let Some(runtime) = self.runtimes.get_mut(&plugin.metadata.runtime) {
335 runtime.unload_plugin(handle)?;
336 }
337 }
338 Ok(())
339 }
340
341 pub async fn dispatch_hook(&self, hook: &Hook, ctx: &HookContext) -> Vec<HookResult> {
343 let hook_name = hook.name();
344 let mut results = vec![];
345
346 for (handle, plugin) in &self.plugins {
347 if !plugin.hooks.contains(&hook_name.to_string()) {
348 continue;
349 }
350
351 if let Some(runtime) = self.runtimes.get(&plugin.metadata.runtime) {
352 let result = if hook.is_sync() {
353 runtime.call_hook_sync(*handle, hook, ctx)
354 } else {
355 runtime.call_hook_async(*handle, hook, ctx).await
356 };
357
358 match result {
359 Ok(r) => {
360 results.push(r.clone());
361 if r.stop_propagation {
362 break;
363 }
364 }
365 Err(e) => {
366 eprintln!("Plugin {} hook error: {}", plugin.id, e);
368 }
369 }
370 }
371 }
372
373 results
374 }
375
376 pub fn plugins(&self) -> impl Iterator<Item = &LoadedPlugin> {
378 self.plugins.values()
379 }
380
381 pub fn get_plugin(&self, handle: PluginHandle) -> Option<&LoadedPlugin> {
383 self.plugins.get(&handle)
384 }
385
386 pub fn plugins_of_kind(&self, kind: PluginKind) -> impl Iterator<Item = &LoadedPlugin> {
388 self.plugins.values().filter(move |p| p.metadata.kind == kind)
389 }
390
391 pub fn shutdown(&mut self) -> PluginResult<()> {
393 for runtime in self.runtimes.values_mut() {
394 runtime.shutdown()?;
395 }
396 self.plugins.clear();
397 Ok(())
398 }
399}
400
401impl Default for PluginManager {
402 fn default() -> Self {
403 Self::new(PluginConfig::default())
404 }
405}