Skip to main content

mockforge_plugin_loader/
loader.rs

1//! Plugin loader implementation
2//!
3//! This module provides the main PluginLoader that handles:
4//! - Plugin discovery and validation
5//! - Secure plugin loading with sandboxing
6//! - Plugin lifecycle management
7//! - Resource monitoring and cleanup
8
9use super::*;
10use crate::registry::PluginRegistry;
11use crate::sandbox::PluginSandbox;
12use crate::validator::PluginValidator;
13use std::path::PathBuf;
14use std::sync::Arc;
15use tokio::sync::RwLock;
16use tokio::time::{timeout, Duration};
17
18// Import types from plugin core
19use mockforge_plugin_core::{PluginHealth, PluginId, PluginInstance, PluginManifest};
20
21/// Main plugin loader
22pub struct PluginLoader {
23    /// Plugin registry
24    registry: Arc<RwLock<PluginRegistry>>,
25    /// Plugin validator
26    validator: PluginValidator,
27    /// Plugin sandbox
28    sandbox: PluginSandbox,
29    /// Loader configuration
30    config: PluginLoaderConfig,
31    /// Loading statistics
32    stats: RwLock<PluginLoadStats>,
33}
34
35// SAFETY: All PluginLoader fields are individually Send + Sync (Arc, RwLock wrappers)
36// except for PluginRegistry which requires manual Send + Sync due to wasmtime types.
37// The registry is wrapped in Arc<RwLock<...>> ensuring synchronized access.
38unsafe impl Send for PluginLoader {}
39unsafe impl Sync for PluginLoader {}
40
41impl PluginLoader {
42    /// Create a new plugin loader
43    pub fn new(config: PluginLoaderConfig) -> Self {
44        Self {
45            registry: Arc::new(RwLock::new(PluginRegistry::new())),
46            validator: PluginValidator::new(config.clone()),
47            sandbox: PluginSandbox::new(config.clone()),
48            config,
49            stats: RwLock::new(PluginLoadStats::default()),
50        }
51    }
52
53    /// Load all plugins from configured directories
54    pub async fn load_all_plugins(&self) -> LoaderResult<PluginLoadStats> {
55        let mut stats = self.stats.write().await;
56        stats.start_loading();
57
58        // Discover plugins from all configured directories
59        let mut all_discoveries = Vec::new();
60        for dir in &self.config.plugin_dirs {
61            let discoveries = self.discover_plugins_in_directory(dir).await?;
62            all_discoveries.extend(discoveries);
63        }
64
65        stats.discovered = all_discoveries.len();
66
67        // Load valid plugins
68        for discovery in all_discoveries {
69            if discovery.is_valid {
70                match self.load_plugin_from_discovery(&discovery).await {
71                    Ok(_) => stats.record_success(),
72                    Err(e) => {
73                        tracing::warn!("Failed to load plugin {}: {}", discovery.plugin_id, e);
74                        stats.record_failure();
75                    }
76                }
77            } else {
78                tracing::debug!(
79                    "Skipping invalid plugin {}: {}",
80                    discovery.plugin_id,
81                    discovery.first_error().unwrap_or("unknown error")
82                );
83                stats.record_skipped();
84            }
85        }
86
87        stats.finish_loading();
88        Ok(stats.clone())
89    }
90
91    /// Load a specific plugin by ID
92    pub async fn load_plugin(&self, plugin_id: &PluginId) -> LoaderResult<()> {
93        // Find plugin in discovery paths
94        let discovery = self
95            .discover_plugin_by_id(plugin_id)
96            .await?
97            .ok_or_else(|| PluginLoaderError::not_found(plugin_id.clone()))?;
98
99        if !discovery.is_valid {
100            return Err(PluginLoaderError::validation(
101                discovery.first_error().unwrap_or("Plugin validation failed").to_string(),
102            ));
103        }
104
105        self.load_plugin_from_discovery(&discovery).await
106    }
107
108    /// Load the exact directory selected by an installer, without rediscovering
109    /// a possibly different plugin with the same ID in configured search paths.
110    /// Callers must enforce their signature policy before calling this method.
111    pub async fn load_plugin_from_path(&self, plugin_path: &Path) -> LoaderResult<PluginId> {
112        let manifest = self.validate_plugin(plugin_path).await?;
113        let plugin_id = manifest.info.id.clone();
114        let discovery = PluginDiscovery::success(
115            plugin_id.clone(),
116            manifest,
117            plugin_path.display().to_string(),
118        );
119        self.load_plugin_from_discovery(&discovery).await?;
120        Ok(plugin_id)
121    }
122
123    /// Unload a plugin
124    pub async fn unload_plugin(&self, plugin_id: &PluginId) -> LoaderResult<()> {
125        let mut registry = self.registry.write().await;
126        registry.remove_plugin(plugin_id)?;
127
128        tracing::info!("Unloaded plugin: {}", plugin_id);
129        Ok(())
130    }
131
132    /// Get loaded plugin by ID
133    pub async fn get_plugin(&self, plugin_id: &PluginId) -> Option<PluginInstance> {
134        let registry = self.registry.read().await;
135        registry.get_plugin(plugin_id).cloned()
136    }
137
138    /// List all loaded plugins
139    pub async fn list_plugins(&self) -> Vec<PluginId> {
140        let registry = self.registry.read().await;
141        registry.list_plugins()
142    }
143
144    /// Get plugin health status
145    pub async fn get_plugin_health(&self, plugin_id: &PluginId) -> LoaderResult<PluginHealth> {
146        let registry = self.registry.read().await;
147        registry.get_plugin_health(plugin_id)
148    }
149
150    /// Get loading statistics
151    pub async fn get_load_stats(&self) -> PluginLoadStats {
152        self.stats.read().await.clone()
153    }
154
155    /// Validate plugin without loading
156    pub async fn validate_plugin(&self, plugin_path: &Path) -> LoaderResult<PluginManifest> {
157        self.validator.validate_plugin_file(plugin_path).await
158    }
159
160    /// Discover plugins in a directory
161    async fn discover_plugins_in_directory(
162        &self,
163        dir_path: &str,
164    ) -> LoaderResult<Vec<PluginDiscovery>> {
165        let expanded_path = shellexpand::tilde(dir_path);
166        let dir_path = PathBuf::from(expanded_path.as_ref());
167
168        if !dir_path.exists() {
169            tracing::debug!("Plugin directory does not exist: {}", dir_path.display());
170            return Ok(Vec::new());
171        }
172
173        if !dir_path.is_dir() {
174            return Err(PluginLoaderError::fs(format!(
175                "Plugin path is not a directory: {}",
176                dir_path.display()
177            )));
178        }
179
180        let mut discoveries = Vec::new();
181
182        // Read directory entries
183        let mut entries = match tokio::fs::read_dir(&dir_path).await {
184            Ok(entries) => entries,
185            Err(e) => {
186                return Err(PluginLoaderError::fs(format!(
187                    "Failed to read plugin directory {}: {}",
188                    dir_path.display(),
189                    e
190                )));
191            }
192        };
193
194        while let Ok(Some(entry)) = entries.next_entry().await {
195            let path = entry.path();
196
197            // Skip non-directories
198            if !path.is_dir() {
199                continue;
200            }
201
202            // Look for plugin.yaml in the directory
203            let manifest_path = path.join("plugin.yaml");
204            if !manifest_path.exists() {
205                continue;
206            }
207
208            // Try to discover plugin
209            match self.discover_single_plugin(&manifest_path).await {
210                Ok(discovery) => discoveries.push(discovery),
211                Err(e) => {
212                    tracing::warn!("Failed to discover plugin at {}: {}", path.display(), e);
213                }
214            }
215        }
216
217        Ok(discoveries)
218    }
219
220    /// Discover a single plugin from manifest file
221    async fn discover_single_plugin(&self, manifest_path: &Path) -> LoaderResult<PluginDiscovery> {
222        // Load and validate manifest
223        let manifest = match PluginManifest::from_file(manifest_path) {
224            Ok(manifest) => manifest,
225            Err(e) => {
226                let plugin_id = PluginId::new("unknown".to_string());
227                let errors = vec![format!("Failed to load manifest: {}", e)];
228                return Ok(PluginDiscovery::failure(
229                    plugin_id,
230                    manifest_path.display().to_string(),
231                    errors,
232                ));
233            }
234        };
235
236        let plugin_id = manifest.id().clone();
237
238        // Validate manifest
239        let validation_result = self.validator.validate_manifest(&manifest).await;
240
241        match validation_result {
242            Ok(_) => {
243                let discovery = PluginDiscovery::success(
244                    plugin_id,
245                    manifest,
246                    manifest_path.parent().unwrap().display().to_string(),
247                );
248                Ok(discovery)
249            }
250            Err(errors) => {
251                let errors_str: Vec<String> = vec![errors.to_string()];
252                Ok(PluginDiscovery::failure(
253                    plugin_id,
254                    manifest_path.display().to_string(),
255                    errors_str,
256                ))
257            }
258        }
259    }
260
261    /// Discover plugin by ID
262    async fn discover_plugin_by_id(
263        &self,
264        plugin_id: &PluginId,
265    ) -> LoaderResult<Option<PluginDiscovery>> {
266        for dir in &self.config.plugin_dirs {
267            let discoveries = self.discover_plugins_in_directory(dir).await?;
268            if let Some(discovery) = discoveries.into_iter().find(|d| &d.plugin_id == plugin_id) {
269                return Ok(Some(discovery));
270            }
271        }
272        Ok(None)
273    }
274
275    /// Load plugin from discovery result
276    async fn load_plugin_from_discovery(&self, discovery: &PluginDiscovery) -> LoaderResult<()> {
277        let plugin_id = &discovery.plugin_id;
278
279        // Check if already loaded
280        {
281            let registry = self.registry.read().await;
282            if registry.has_plugin(plugin_id) {
283                return Err(PluginLoaderError::already_loaded(plugin_id.clone()));
284            }
285        }
286
287        // Validate capabilities
288        self.validator.validate_capabilities(&discovery.manifest.capabilities)?;
289
290        // Find WASM file
291        let plugin_path = self.find_plugin_wasm_file(&discovery.path).await?.ok_or_else(|| {
292            PluginLoaderError::load(format!("No WebAssembly file found for plugin {}", plugin_id))
293        })?;
294
295        // Validate WASM file
296        self.validator.validate_wasm_file(&plugin_path).await?;
297
298        // Create load context
299        let load_context = PluginLoadContext::new(
300            plugin_id.clone(),
301            discovery.manifest.clone(),
302            plugin_path.display().to_string(),
303            self.config.clone(),
304        );
305
306        // Load plugin with timeout
307        let load_timeout = Duration::from_secs(self.config.load_timeout_secs);
308        let plugin_instance = timeout(load_timeout, self.load_plugin_instance(&load_context))
309            .await
310            .map_err(|_| {
311                PluginLoaderError::load(format!(
312                    "Plugin loading timed out after {} seconds",
313                    self.config.load_timeout_secs
314                ))
315            })??;
316
317        // Register plugin
318        let mut registry = self.registry.write().await;
319        registry.add_plugin(plugin_instance)?;
320
321        tracing::info!("Successfully loaded plugin: {}", plugin_id);
322        Ok(())
323    }
324
325    /// Load plugin instance
326    async fn load_plugin_instance(
327        &self,
328        context: &PluginLoadContext,
329    ) -> LoaderResult<PluginInstance> {
330        // Create plugin instance through sandbox
331        self.sandbox.create_plugin_instance(context).await
332    }
333
334    /// Find plugin WASM file in plugin directory
335    async fn find_plugin_wasm_file(&self, plugin_dir: &str) -> LoaderResult<Option<PathBuf>> {
336        let plugin_path = PathBuf::from(plugin_dir);
337
338        // Look for .wasm files in the plugin directory
339        let mut entries = match tokio::fs::read_dir(&plugin_path).await {
340            Ok(entries) => entries,
341            Err(e) => {
342                return Err(PluginLoaderError::fs(format!(
343                    "Failed to read plugin directory {}: {}",
344                    plugin_path.display(),
345                    e
346                )));
347            }
348        };
349
350        while let Ok(Some(entry)) = entries.next_entry().await {
351            let path = entry.path();
352            if let Some(extension) = path.extension() {
353                if extension == "wasm" {
354                    return Ok(Some(path));
355                }
356            }
357        }
358
359        Ok(None)
360    }
361
362    /// Reload all plugins
363    pub async fn reload_all_plugins(&self) -> LoaderResult<PluginLoadStats> {
364        // Get currently loaded plugins
365        let loaded_plugins = self.list_plugins().await;
366
367        // Unload all plugins
368        for plugin_id in &loaded_plugins {
369            if let Err(e) = self.unload_plugin(plugin_id).await {
370                tracing::warn!("Failed to unload plugin {} during reload: {}", plugin_id, e);
371            }
372        }
373
374        // Load all plugins again
375        self.load_all_plugins().await
376    }
377
378    /// Reload specific plugin
379    pub async fn reload_plugin(&self, plugin_id: &PluginId) -> LoaderResult<()> {
380        // Unload plugin
381        self.unload_plugin(plugin_id).await?;
382
383        // Load plugin again
384        self.load_plugin(plugin_id).await
385    }
386
387    /// Get registry reference (for advanced operations)
388    pub fn registry(&self) -> Arc<RwLock<PluginRegistry>> {
389        Arc::clone(&self.registry)
390    }
391
392    /// Get validator reference (for advanced operations)
393    pub fn validator(&self) -> &PluginValidator {
394        &self.validator
395    }
396
397    /// Get sandbox reference (for advanced operations)
398    pub fn sandbox(&self) -> &PluginSandbox {
399        &self.sandbox
400    }
401}
402
403impl Default for PluginLoader {
404    fn default() -> Self {
405        Self::new(PluginLoaderConfig::default())
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[tokio::test]
414    async fn test_plugin_loader_creation() {
415        let config = PluginLoaderConfig::default();
416        let loader = PluginLoader::new(config);
417
418        let stats = loader.get_load_stats().await;
419        assert_eq!(stats.discovered, 0);
420        assert_eq!(stats.loaded, 0);
421    }
422
423    #[tokio::test]
424    async fn test_load_stats() {
425        let mut stats = PluginLoadStats::default();
426
427        stats.start_loading();
428        assert!(stats.start_time.is_some());
429
430        stats.record_success();
431        stats.record_failure();
432        stats.record_skipped();
433
434        stats.finish_loading();
435        assert!(stats.end_time.is_some());
436
437        assert_eq!(stats.loaded, 1);
438        assert_eq!(stats.failed, 1);
439        assert_eq!(stats.skipped, 1);
440        assert_eq!(stats.discovered, 3);
441        assert_eq!(stats.success_rate(), 33.33333333333333);
442    }
443
444    #[tokio::test]
445    async fn test_plugin_discovery_success() {
446        let plugin_id = PluginId::new("test-plugin");
447        let manifest = PluginManifest::new(PluginInfo::new(
448            plugin_id.clone(),
449            PluginVersion::new(1, 0, 0),
450            "Test Plugin",
451            "A test plugin",
452            PluginAuthor::new("Test Author"),
453        ));
454
455        let discovery = PluginDiscovery::success(plugin_id, manifest, "/tmp/test".to_string());
456        assert!(discovery.is_success());
457        assert!(discovery.errors.is_empty());
458    }
459
460    #[tokio::test]
461    async fn test_plugin_discovery_failure() {
462        let plugin_id = PluginId::new("test-plugin");
463        let errors = vec!["Validation failed".to_string()];
464
465        let discovery =
466            PluginDiscovery::failure(plugin_id, "/tmp/test".to_string(), errors.clone());
467        assert!(!discovery.is_success());
468        assert_eq!(discovery.errors, errors);
469        assert_eq!(discovery.first_error(), Some("Validation failed"));
470    }
471}