fusabi_plugin_runtime/
plugin.rs

1//! Plugin representation and execution.
2
3use std::path::PathBuf;
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6use std::time::Instant;
7
8use parking_lot::RwLock;
9
10use fusabi_host::{Engine, EngineConfig, Value};
11
12use crate::error::{Error, Result};
13use crate::lifecycle::LifecycleState;
14use crate::manifest::Manifest;
15
16static NEXT_PLUGIN_ID: AtomicU64 = AtomicU64::new(1);
17
18/// Information about a loaded plugin.
19#[derive(Debug, Clone)]
20pub struct PluginInfo {
21    /// Unique plugin ID.
22    pub id: u64,
23    /// Plugin name from manifest.
24    pub name: String,
25    /// Plugin version from manifest.
26    pub version: String,
27    /// Path to the manifest file.
28    pub manifest_path: Option<PathBuf>,
29    /// Path to the source/bytecode file.
30    pub entry_path: Option<PathBuf>,
31    /// When the plugin was loaded.
32    pub loaded_at: Instant,
33    /// When the plugin was last reloaded.
34    pub last_reload: Option<Instant>,
35    /// Total reload count.
36    pub reload_count: u64,
37    /// Total invocation count.
38    pub invocation_count: u64,
39    /// Current lifecycle state.
40    pub state: LifecycleState,
41}
42
43impl PluginInfo {
44    /// Create new plugin info.
45    fn new(id: u64, manifest: &Manifest) -> Self {
46        Self {
47            id,
48            name: manifest.name.clone(),
49            version: manifest.version.clone(),
50            manifest_path: None,
51            entry_path: None,
52            loaded_at: Instant::now(),
53            last_reload: None,
54            reload_count: 0,
55            invocation_count: 0,
56            state: LifecycleState::Created,
57        }
58    }
59}
60
61/// Internal plugin state.
62struct PluginInner {
63    manifest: Manifest,
64    info: PluginInfo,
65    engine: Option<Engine>,
66    bytecode: Option<Vec<u8>>,
67}
68
69/// A loaded Fusabi plugin.
70pub struct Plugin {
71    inner: RwLock<PluginInner>,
72}
73
74impl Plugin {
75    /// Create a new plugin from a manifest.
76    pub fn new(manifest: Manifest) -> Self {
77        let id = NEXT_PLUGIN_ID.fetch_add(1, Ordering::Relaxed);
78        let info = PluginInfo::new(id, &manifest);
79
80        Self {
81            inner: RwLock::new(PluginInner {
82                manifest,
83                info,
84                engine: None,
85                bytecode: None,
86            }),
87        }
88    }
89
90    /// Get the plugin ID.
91    pub fn id(&self) -> u64 {
92        self.inner.read().info.id
93    }
94
95    /// Get the plugin name.
96    pub fn name(&self) -> String {
97        self.inner.read().manifest.name.clone()
98    }
99
100    /// Get the plugin version.
101    pub fn version(&self) -> String {
102        self.inner.read().manifest.version.clone()
103    }
104
105    /// Get the plugin manifest.
106    pub fn manifest(&self) -> Manifest {
107        self.inner.read().manifest.clone()
108    }
109
110    /// Get plugin information.
111    pub fn info(&self) -> PluginInfo {
112        self.inner.read().info.clone()
113    }
114
115    /// Get the current lifecycle state.
116    pub fn state(&self) -> LifecycleState {
117        self.inner.read().info.state
118    }
119
120    /// Set the lifecycle state.
121    pub fn set_state(&self, state: LifecycleState) {
122        self.inner.write().info.state = state;
123    }
124
125    /// Initialize the plugin with an engine.
126    pub fn initialize(&self, engine_config: EngineConfig) -> Result<()> {
127        let mut inner = self.inner.write();
128
129        // Check state
130        if inner.info.state != LifecycleState::Created
131            && inner.info.state != LifecycleState::Stopped
132        {
133            return Err(Error::invalid_state(
134                "Created or Stopped",
135                format!("{:?}", inner.info.state),
136            ));
137        }
138
139        // Verify capabilities
140        let caps = &engine_config.capabilities;
141        for required_cap in &inner.manifest.capabilities {
142            let cap = fusabi_host::Capability::from_name(required_cap)
143                .ok_or_else(|| Error::invalid_manifest(format!("unknown capability: {}", required_cap)))?;
144
145            if !caps.has(cap) {
146                return Err(Error::MissingCapability(required_cap.clone()));
147            }
148        }
149
150        // Create engine
151        let engine = Engine::new(engine_config)
152            .map_err(|e| Error::init_failed(e.to_string()))?;
153
154        inner.engine = Some(engine);
155        inner.info.state = LifecycleState::Initialized;
156
157        Ok(())
158    }
159
160    /// Start the plugin (call init function if exists).
161    pub fn start(&self) -> Result<()> {
162        let mut inner = self.inner.write();
163
164        if inner.info.state != LifecycleState::Initialized {
165            return Err(Error::invalid_state(
166                "Initialized",
167                format!("{:?}", inner.info.state),
168            ));
169        }
170
171        // Call init function if declared
172        if inner.manifest.exports.contains(&"init".to_string()) {
173            if let Some(ref engine) = inner.engine {
174                engine
175                    .execute("init()")
176                    .map_err(|e| Error::init_failed(e.to_string()))?;
177            }
178        }
179
180        inner.info.state = LifecycleState::Running;
181        Ok(())
182    }
183
184    /// Stop the plugin (call cleanup function if exists).
185    pub fn stop(&self) -> Result<()> {
186        let mut inner = self.inner.write();
187
188        if inner.info.state != LifecycleState::Running {
189            return Err(Error::invalid_state(
190                "Running",
191                format!("{:?}", inner.info.state),
192            ));
193        }
194
195        // Call cleanup function if declared
196        if inner.manifest.exports.contains(&"cleanup".to_string()) {
197            if let Some(ref engine) = inner.engine {
198                let _ = engine.execute("cleanup()");
199            }
200        }
201
202        inner.info.state = LifecycleState::Stopped;
203        Ok(())
204    }
205
206    /// Unload the plugin.
207    pub fn unload(&self) -> Result<()> {
208        let mut inner = self.inner.write();
209
210        // Try to stop if running
211        if inner.info.state == LifecycleState::Running {
212            if inner.manifest.exports.contains(&"cleanup".to_string()) {
213                if let Some(ref engine) = inner.engine {
214                    let _ = engine.execute("cleanup()");
215                }
216            }
217        }
218
219        inner.engine = None;
220        inner.bytecode = None;
221        inner.info.state = LifecycleState::Unloaded;
222
223        Ok(())
224    }
225
226    /// Call a function exported by the plugin.
227    pub fn call(&self, function: &str, args: &[Value]) -> Result<Value> {
228        let mut inner = self.inner.write();
229
230        // Check state
231        if inner.info.state != LifecycleState::Running {
232            return Err(Error::invalid_state(
233                "Running",
234                format!("{:?}", inner.info.state),
235            ));
236        }
237
238        // Check function is exported
239        if !inner.manifest.exports.contains(&function.to_string())
240            && function != "main"
241        {
242            return Err(Error::FunctionNotFound(function.to_string()));
243        }
244
245        // Build call expression
246        let call_expr = if args.is_empty() {
247            format!("{}()", function)
248        } else {
249            // Format args - simplified for simulation
250            let args_str: Vec<String> = args.iter().map(|a| a.to_string()).collect();
251            format!("{}({})", function, args_str.join(", "))
252        };
253
254        // Increment invocation count before borrowing engine
255        inner.info.invocation_count += 1;
256
257        // Execute
258        let engine = inner
259            .engine
260            .as_ref()
261            .ok_or_else(|| Error::invalid_state("engine initialized", "no engine"))?;
262
263        engine
264            .execute(&call_expr)
265            .map_err(|e| Error::execution_failed(e.to_string()))
266    }
267
268    /// Reload the plugin from source.
269    pub fn reload(&self) -> Result<()> {
270        let mut inner = self.inner.write();
271
272        // Must be in a reloadable state
273        if inner.info.state == LifecycleState::Unloaded {
274            return Err(Error::PluginUnloaded);
275        }
276
277        let was_running = inner.info.state == LifecycleState::Running;
278
279        // Stop if running
280        if was_running {
281            if inner.manifest.exports.contains(&"cleanup".to_string()) {
282                if let Some(ref engine) = inner.engine {
283                    let _ = engine.execute("cleanup()");
284                }
285            }
286        }
287
288        // Reset state
289        inner.info.state = LifecycleState::Initialized;
290        inner.info.last_reload = Some(Instant::now());
291        inner.info.reload_count += 1;
292
293        // Restart if was running
294        if was_running {
295            inner.info.state = LifecycleState::Running;
296            if inner.manifest.exports.contains(&"init".to_string()) {
297                if let Some(ref engine) = inner.engine {
298                    engine
299                        .execute("init()")
300                        .map_err(|e| Error::ReloadFailed(e.to_string()))?;
301                }
302            }
303        }
304
305        Ok(())
306    }
307
308    /// Check if the plugin exports a function.
309    pub fn has_export(&self, name: &str) -> bool {
310        self.inner.read().manifest.exports.contains(&name.to_string())
311    }
312
313    /// Get all exported function names.
314    pub fn exports(&self) -> Vec<String> {
315        self.inner.read().manifest.exports.clone()
316    }
317
318    /// Check if the plugin requires a capability.
319    pub fn requires_capability(&self, cap: &str) -> bool {
320        self.inner.read().manifest.requires_capability(cap)
321    }
322
323    /// Set the compiled bytecode.
324    pub fn set_bytecode(&self, bytecode: Vec<u8>) {
325        self.inner.write().bytecode = Some(bytecode);
326    }
327
328    /// Get the compiled bytecode if available.
329    pub fn bytecode(&self) -> Option<Vec<u8>> {
330        self.inner.read().bytecode.clone()
331    }
332}
333
334impl std::fmt::Debug for Plugin {
335    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
336        let inner = self.inner.read();
337        f.debug_struct("Plugin")
338            .field("id", &inner.info.id)
339            .field("name", &inner.manifest.name)
340            .field("version", &inner.manifest.version)
341            .field("state", &inner.info.state)
342            .finish()
343    }
344}
345
346/// Handle to a loaded plugin for safe concurrent access.
347#[derive(Clone)]
348pub struct PluginHandle {
349    plugin: Arc<Plugin>,
350}
351
352impl PluginHandle {
353    /// Create a new plugin handle.
354    pub fn new(plugin: Plugin) -> Self {
355        Self {
356            plugin: Arc::new(plugin),
357        }
358    }
359
360    /// Get the plugin ID.
361    pub fn id(&self) -> u64 {
362        self.plugin.id()
363    }
364
365    /// Get the plugin name.
366    pub fn name(&self) -> String {
367        self.plugin.name()
368    }
369
370    /// Get the plugin state.
371    pub fn state(&self) -> LifecycleState {
372        self.plugin.state()
373    }
374
375    /// Call a function on the plugin.
376    pub fn call(&self, function: &str, args: &[Value]) -> Result<Value> {
377        self.plugin.call(function, args)
378    }
379
380    /// Get plugin info.
381    pub fn info(&self) -> PluginInfo {
382        self.plugin.info()
383    }
384
385    /// Check if the plugin exports a function.
386    pub fn has_export(&self, name: &str) -> bool {
387        self.plugin.has_export(name)
388    }
389
390    /// Get the underlying plugin.
391    pub fn inner(&self) -> &Plugin {
392        &self.plugin
393    }
394}
395
396impl std::fmt::Debug for PluginHandle {
397    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
398        f.debug_struct("PluginHandle")
399            .field("id", &self.id())
400            .field("name", &self.name())
401            .field("state", &self.state())
402            .finish()
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409    use crate::manifest::ManifestBuilder;
410
411    fn create_test_manifest() -> Manifest {
412        ManifestBuilder::new("test-plugin", "1.0.0")
413            .source("test.fsx")
414            .export("main")
415            .export("init")
416            .build_unchecked()
417    }
418
419    #[test]
420    fn test_plugin_creation() {
421        let manifest = create_test_manifest();
422        let plugin = Plugin::new(manifest);
423
424        assert!(plugin.id() > 0);
425        assert_eq!(plugin.name(), "test-plugin");
426        assert_eq!(plugin.version(), "1.0.0");
427        assert_eq!(plugin.state(), LifecycleState::Created);
428    }
429
430    #[test]
431    fn test_plugin_lifecycle() {
432        let manifest = create_test_manifest();
433        let plugin = Plugin::new(manifest);
434
435        // Initialize
436        plugin
437            .initialize(EngineConfig::default())
438            .unwrap();
439        assert_eq!(plugin.state(), LifecycleState::Initialized);
440
441        // Start
442        plugin.start().unwrap();
443        assert_eq!(plugin.state(), LifecycleState::Running);
444
445        // Stop
446        plugin.stop().unwrap();
447        assert_eq!(plugin.state(), LifecycleState::Stopped);
448
449        // Unload
450        plugin.unload().unwrap();
451        assert_eq!(plugin.state(), LifecycleState::Unloaded);
452    }
453
454    #[test]
455    fn test_plugin_invalid_state_transitions() {
456        let manifest = create_test_manifest();
457        let plugin = Plugin::new(manifest);
458
459        // Can't start before initialize
460        assert!(plugin.start().is_err());
461
462        // Can't stop before start
463        assert!(plugin.stop().is_err());
464
465        // Initialize first
466        plugin.initialize(EngineConfig::default()).unwrap();
467
468        // Can't stop before start
469        assert!(plugin.stop().is_err());
470    }
471
472    #[test]
473    fn test_plugin_capabilities() {
474        let manifest = ManifestBuilder::new("test", "1.0.0")
475            .source("test.fsx")
476            .capability("fs:read")
477            .build_unchecked();
478
479        let plugin = Plugin::new(manifest);
480
481        // Missing capability should fail
482        let config = EngineConfig::default()
483            .with_capabilities(fusabi_host::Capabilities::none());
484
485        assert!(plugin.initialize(config).is_err());
486
487        // With capability should succeed
488        let config = EngineConfig::default().with_capabilities(
489            fusabi_host::Capabilities::none().with(fusabi_host::Capability::FsRead),
490        );
491
492        assert!(plugin.initialize(config).is_ok());
493    }
494
495    #[test]
496    fn test_plugin_handle() {
497        let manifest = create_test_manifest();
498        let plugin = Plugin::new(manifest);
499        let handle = PluginHandle::new(plugin);
500
501        assert!(handle.id() > 0);
502        assert_eq!(handle.name(), "test-plugin");
503        assert!(handle.has_export("main"));
504
505        // Clone and verify
506        let handle2 = handle.clone();
507        assert_eq!(handle.id(), handle2.id());
508    }
509}