gravityfile_plugin/
runtime.rs

1//! Plugin runtime trait and manager.
2//!
3//! This module defines the language-agnostic [`PluginRuntime`] trait that
4//! all scripting language implementations must satisfy.
5
6use 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/// A handle to a loaded plugin.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct PluginHandle(pub(crate) usize);
21
22impl PluginHandle {
23    /// Create a new plugin handle.
24    pub fn new(id: usize) -> Self {
25        Self(id)
26    }
27
28    /// Get the raw ID.
29    pub fn id(&self) -> usize {
30        self.0
31    }
32}
33
34/// Type alias for boxed futures returned by async plugin methods.
35pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
36
37/// Trait that all plugin runtime implementations must satisfy.
38///
39/// This trait abstracts over the specific scripting language (Lua, Rhai, WASM)
40/// allowing plugins to be written in any supported language.
41pub trait PluginRuntime: Send + Sync {
42    /// Get the name of this runtime (e.g., "lua", "rhai", "wasm").
43    fn name(&self) -> &'static str;
44
45    /// Get the file extensions this runtime handles (e.g., [".lua"]).
46    fn file_extensions(&self) -> &'static [&'static str];
47
48    /// Initialize the runtime with configuration.
49    fn init(&mut self, config: &PluginConfig) -> PluginResult<()>;
50
51    /// Load a plugin from a file path.
52    ///
53    /// Returns a handle that can be used to interact with the plugin.
54    fn load_plugin(&mut self, id: &str, source: &Path) -> PluginResult<PluginHandle>;
55
56    /// Unload a previously loaded plugin.
57    fn unload_plugin(&mut self, handle: PluginHandle) -> PluginResult<()>;
58
59    /// Get metadata about a loaded plugin.
60    fn get_metadata(&self, handle: PluginHandle) -> Option<&PluginMetadata>;
61
62    /// Check if a plugin implements a specific hook.
63    fn has_hook(&self, handle: PluginHandle, hook_name: &str) -> bool;
64
65    /// Call a hook on a plugin synchronously.
66    ///
67    /// Used for hooks that must complete immediately (e.g., render hooks).
68    fn call_hook_sync(
69        &self,
70        handle: PluginHandle,
71        hook: &Hook,
72        ctx: &HookContext,
73    ) -> PluginResult<HookResult>;
74
75    /// Call a hook on a plugin asynchronously.
76    ///
77    /// Used for hooks that may take time (e.g., scan complete hooks).
78    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    /// Call an arbitrary method on a plugin.
86    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    /// Create an isolated context for running async plugin code.
94    ///
95    /// Isolated contexts have limited API access and their own Lua/Rhai state,
96    /// making them safe to run in background tasks.
97    fn create_isolated_context(&self, sandbox: &SandboxConfig) -> PluginResult<Box<dyn IsolatedContext>>;
98
99    /// Get the list of loaded plugin handles.
100    fn loaded_plugins(&self) -> Vec<PluginHandle>;
101
102    /// Shutdown the runtime and cleanup resources.
103    fn shutdown(&mut self) -> PluginResult<()>;
104}
105
106/// An isolated execution context for running plugin code safely.
107///
108/// Isolated contexts are used for async plugins (previewers, analyzers) that
109/// run in background tasks. They have:
110/// - Their own script state (not shared with main runtime)
111/// - Limited API access (no UI modification)
112/// - Cancellation support
113/// - Resource limits
114pub trait IsolatedContext: Send {
115    /// Execute a chunk of code in this isolated context.
116    fn execute<'a>(
117        &'a self,
118        code: &'a [u8],
119        cancel: CancellationToken,
120    ) -> BoxFuture<'a, PluginResult<Value>>;
121
122    /// Execute a named function with arguments.
123    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    /// Set a global variable in this context.
131    fn set_global(&mut self, name: &str, value: Value) -> PluginResult<()>;
132
133    /// Get a global variable from this context.
134    fn get_global(&self, name: &str) -> PluginResult<Value>;
135}
136
137/// Information about a loaded plugin.
138#[derive(Debug, Clone)]
139pub struct LoadedPlugin {
140    /// Unique handle for this plugin.
141    pub handle: PluginHandle,
142
143    /// Plugin ID (usually directory name).
144    pub id: String,
145
146    /// Plugin metadata from plugin.toml.
147    pub metadata: PluginMetadata,
148
149    /// Path to the plugin directory.
150    pub path: std::path::PathBuf,
151
152    /// Hooks this plugin implements.
153    pub hooks: Vec<String>,
154}
155
156/// Manager for all plugin runtimes and loaded plugins.
157pub struct PluginManager {
158    /// Available runtimes keyed by name.
159    runtimes: HashMap<String, Box<dyn PluginRuntime>>,
160
161    /// Loaded plugins keyed by handle.
162    plugins: HashMap<PluginHandle, LoadedPlugin>,
163
164    /// Plugin config.
165    config: PluginConfig,
166
167    /// Next plugin handle ID (reserved for future use).
168    #[allow(dead_code)]
169    next_handle: usize,
170}
171
172impl PluginManager {
173    /// Create a new plugin manager with the given configuration.
174    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    /// Register a plugin runtime.
184    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    /// Get a runtime by name.
191    pub fn get_runtime(&self, name: &str) -> Option<&dyn PluginRuntime> {
192        self.runtimes.get(name).map(|r| r.as_ref())
193    }
194
195    /// Get a mutable runtime by name.
196    pub fn get_runtime_mut(&mut self, name: &str) -> Option<&mut Box<dyn PluginRuntime>> {
197        self.runtimes.get_mut(name)
198    }
199
200    /// Initialize all registered runtimes.
201    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    /// Discover and load plugins from the plugin directory.
209    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        // Read plugin directories
218        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            // Look for plugin.toml
227            let toml_path = path.join("plugin.toml");
228            if !toml_path.exists() {
229                continue;
230            }
231
232            // Parse metadata
233            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            // Find the appropriate runtime
238            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            // Find the entry point file
246            let entry_file = path.join(&metadata.entry);
247            if !entry_file.exists() {
248                continue;
249            }
250
251            // Load the plugin
252            let handle = runtime.load_plugin(&metadata.name, &entry_file)?;
253
254            // Collect hooks
255            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    /// Load a single plugin from a path.
288    pub fn load_plugin(&mut self, path: &Path) -> PluginResult<LoadedPlugin> {
289        // Parse metadata
290        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        // Find runtime
296        let runtime = self.runtimes.get_mut(&metadata.runtime).ok_or_else(|| {
297            PluginError::RuntimeNotAvailable {
298                runtime: metadata.runtime.clone(),
299            }
300        })?;
301
302        // Load plugin
303        let entry_file = path.join(&metadata.entry);
304        let handle = runtime.load_plugin(&metadata.name, &entry_file)?;
305
306        // Collect hooks
307        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    /// Unload a plugin.
332    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    /// Dispatch a hook to all plugins that implement it.
342    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                        // Log error but continue to other plugins
367                        eprintln!("Plugin {} hook error: {}", plugin.id, e);
368                    }
369                }
370            }
371        }
372
373        results
374    }
375
376    /// Get all loaded plugins.
377    pub fn plugins(&self) -> impl Iterator<Item = &LoadedPlugin> {
378        self.plugins.values()
379    }
380
381    /// Get a loaded plugin by handle.
382    pub fn get_plugin(&self, handle: PluginHandle) -> Option<&LoadedPlugin> {
383        self.plugins.get(&handle)
384    }
385
386    /// Get plugins of a specific kind.
387    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    /// Shutdown all runtimes.
392    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}