1use 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
18use mockforge_plugin_core::{PluginHealth, PluginId, PluginInstance, PluginManifest};
20
21pub struct PluginLoader {
23 registry: Arc<RwLock<PluginRegistry>>,
25 validator: PluginValidator,
27 sandbox: PluginSandbox,
29 config: PluginLoaderConfig,
31 stats: RwLock<PluginLoadStats>,
33}
34
35unsafe impl Send for PluginLoader {}
39unsafe impl Sync for PluginLoader {}
40
41impl PluginLoader {
42 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 pub async fn load_all_plugins(&self) -> LoaderResult<PluginLoadStats> {
55 let mut stats = self.stats.write().await;
56 stats.start_loading();
57
58 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 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 pub async fn load_plugin(&self, plugin_id: &PluginId) -> LoaderResult<()> {
93 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 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 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 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 pub async fn list_plugins(&self) -> Vec<PluginId> {
140 let registry = self.registry.read().await;
141 registry.list_plugins()
142 }
143
144 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 pub async fn get_load_stats(&self) -> PluginLoadStats {
152 self.stats.read().await.clone()
153 }
154
155 pub async fn validate_plugin(&self, plugin_path: &Path) -> LoaderResult<PluginManifest> {
157 self.validator.validate_plugin_file(plugin_path).await
158 }
159
160 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 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 if !path.is_dir() {
199 continue;
200 }
201
202 let manifest_path = path.join("plugin.yaml");
204 if !manifest_path.exists() {
205 continue;
206 }
207
208 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 async fn discover_single_plugin(&self, manifest_path: &Path) -> LoaderResult<PluginDiscovery> {
222 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 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 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 async fn load_plugin_from_discovery(&self, discovery: &PluginDiscovery) -> LoaderResult<()> {
277 let plugin_id = &discovery.plugin_id;
278
279 {
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 self.validator.validate_capabilities(&discovery.manifest.capabilities)?;
289
290 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 self.validator.validate_wasm_file(&plugin_path).await?;
297
298 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 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 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 async fn load_plugin_instance(
327 &self,
328 context: &PluginLoadContext,
329 ) -> LoaderResult<PluginInstance> {
330 self.sandbox.create_plugin_instance(context).await
332 }
333
334 async fn find_plugin_wasm_file(&self, plugin_dir: &str) -> LoaderResult<Option<PathBuf>> {
336 let plugin_path = PathBuf::from(plugin_dir);
337
338 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 pub async fn reload_all_plugins(&self) -> LoaderResult<PluginLoadStats> {
364 let loaded_plugins = self.list_plugins().await;
366
367 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 self.load_all_plugins().await
376 }
377
378 pub async fn reload_plugin(&self, plugin_id: &PluginId) -> LoaderResult<()> {
380 self.unload_plugin(plugin_id).await?;
382
383 self.load_plugin(plugin_id).await
385 }
386
387 pub fn registry(&self) -> Arc<RwLock<PluginRegistry>> {
389 Arc::clone(&self.registry)
390 }
391
392 pub fn validator(&self) -> &PluginValidator {
394 &self.validator
395 }
396
397 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}