mockforge_plugin_registry/
runtime.rs

1//! Multi-language plugin runtime support
2
3use crate::{RegistryError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::process::{Child, Command, Stdio};
8
9/// Plugin runtime language
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
11#[serde(rename_all = "lowercase")]
12pub enum PluginLanguage {
13    Rust,
14    Python,
15    JavaScript,
16    TypeScript,
17    Go,
18    Ruby,
19    Other(String),
20}
21
22impl PluginLanguage {
23    /// Get runtime executor for this language
24    pub fn executor(&self) -> Box<dyn RuntimeExecutor> {
25        match self {
26            PluginLanguage::Rust => Box::new(RustExecutor),
27            PluginLanguage::Python => Box::new(PythonExecutor::default()),
28            PluginLanguage::JavaScript | PluginLanguage::TypeScript => {
29                Box::new(JavaScriptExecutor::default())
30            }
31            PluginLanguage::Go => Box::new(GoExecutor),
32            PluginLanguage::Ruby => Box::new(RubyExecutor),
33            PluginLanguage::Other(_) => Box::new(GenericExecutor),
34        }
35    }
36}
37
38/// Runtime executor trait
39pub trait RuntimeExecutor: Send + Sync {
40    /// Start the plugin process
41    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>>;
42
43    /// Check if runtime is available
44    fn is_available(&self) -> bool;
45
46    /// Get runtime version
47    fn version(&self) -> Result<String>;
48
49    /// Install plugin dependencies
50    fn install_dependencies(&self, plugin_path: &Path) -> Result<()>;
51}
52
53/// Running plugin process
54pub trait RuntimeProcess: Send + Sync {
55    /// Check if process is running
56    fn is_running(&mut self) -> bool;
57
58    /// Stop the process
59    fn stop(&mut self) -> Result<()>;
60
61    /// Get process ID
62    fn pid(&self) -> Option<u32>;
63
64    /// Send message to plugin
65    fn send_message(&mut self, message: &[u8]) -> Result<()>;
66
67    /// Receive message from plugin
68    fn receive_message(&mut self) -> Result<Vec<u8>>;
69}
70
71/// Runtime configuration
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RuntimeConfig {
74    /// Environment variables
75    pub env_vars: HashMap<String, String>,
76
77    /// Working directory
78    pub working_dir: Option<PathBuf>,
79
80    /// Arguments to pass to plugin
81    pub args: Vec<String>,
82
83    /// Timeout for operations (seconds)
84    pub timeout: u64,
85
86    /// Memory limit (MB)
87    pub memory_limit: Option<u64>,
88
89    /// CPU limit (cores)
90    pub cpu_limit: Option<f32>,
91}
92
93impl Default for RuntimeConfig {
94    fn default() -> Self {
95        Self {
96            env_vars: HashMap::new(),
97            working_dir: None,
98            args: vec![],
99            timeout: 30,
100            memory_limit: Some(512), // 512MB default
101            cpu_limit: None,
102        }
103    }
104}
105
106// ===== Rust Executor =====
107
108struct RustExecutor;
109
110impl RuntimeExecutor for RustExecutor {
111    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>> {
112        let mut cmd = Command::new(plugin_path);
113
114        cmd.args(&config.args)
115            .envs(&config.env_vars)
116            .stdin(Stdio::piped())
117            .stdout(Stdio::piped())
118            .stderr(Stdio::piped());
119
120        if let Some(dir) = &config.working_dir {
121            cmd.current_dir(dir);
122        }
123
124        let child = cmd
125            .spawn()
126            .map_err(|e| RegistryError::Storage(format!("Failed to start Rust plugin: {}", e)))?;
127
128        Ok(Box::new(ProcessWrapper::new(child)))
129    }
130
131    fn is_available(&self) -> bool {
132        Command::new("rustc").arg("--version").output().is_ok()
133    }
134
135    fn version(&self) -> Result<String> {
136        let output = Command::new("rustc")
137            .arg("--version")
138            .output()
139            .map_err(|e| RegistryError::Storage(format!("Failed to get rustc version: {}", e)))?;
140
141        Ok(String::from_utf8_lossy(&output.stdout).to_string())
142    }
143
144    fn install_dependencies(&self, plugin_path: &Path) -> Result<()> {
145        let output = Command::new("cargo")
146            .args(["build", "--release"])
147            .current_dir(plugin_path)
148            .output()
149            .map_err(|e| RegistryError::Storage(format!("Failed to build Rust plugin: {}", e)))?;
150
151        if !output.status.success() {
152            return Err(RegistryError::Storage(format!(
153                "Rust plugin build failed: {}",
154                String::from_utf8_lossy(&output.stderr)
155            )));
156        }
157
158        Ok(())
159    }
160}
161
162// ===== Python Executor =====
163
164#[derive(Default)]
165struct PythonExecutor {
166    python_cmd: String,
167}
168
169impl PythonExecutor {
170    #[allow(dead_code)]
171    fn new(python_cmd: String) -> Self {
172        Self { python_cmd }
173    }
174}
175
176impl RuntimeExecutor for PythonExecutor {
177    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>> {
178        let python_cmd = if self.python_cmd.is_empty() {
179            "python3"
180        } else {
181            &self.python_cmd
182        };
183
184        let mut cmd = Command::new(python_cmd);
185
186        cmd.arg(plugin_path)
187            .args(&config.args)
188            .envs(&config.env_vars)
189            .stdin(Stdio::piped())
190            .stdout(Stdio::piped())
191            .stderr(Stdio::piped());
192
193        if let Some(dir) = &config.working_dir {
194            cmd.current_dir(dir);
195        }
196
197        let child = cmd
198            .spawn()
199            .map_err(|e| RegistryError::Storage(format!("Failed to start Python plugin: {}", e)))?;
200
201        Ok(Box::new(ProcessWrapper::new(child)))
202    }
203
204    fn is_available(&self) -> bool {
205        Command::new("python3").arg("--version").output().is_ok()
206    }
207
208    fn version(&self) -> Result<String> {
209        let output = Command::new("python3")
210            .arg("--version")
211            .output()
212            .map_err(|e| RegistryError::Storage(format!("Failed to get Python version: {}", e)))?;
213
214        Ok(String::from_utf8_lossy(&output.stdout).to_string())
215    }
216
217    fn install_dependencies(&self, plugin_path: &Path) -> Result<()> {
218        let requirements = plugin_path.join("requirements.txt");
219
220        if requirements.exists() {
221            let output = Command::new("pip3")
222                .args(["install", "-r"])
223                .arg(&requirements)
224                .output()
225                .map_err(|e| {
226                RegistryError::Storage(format!("Failed to install Python dependencies: {}", e))
227            })?;
228
229            if !output.status.success() {
230                return Err(RegistryError::Storage(format!(
231                    "Python dependency installation failed: {}",
232                    String::from_utf8_lossy(&output.stderr)
233                )));
234            }
235        }
236
237        Ok(())
238    }
239}
240
241// ===== JavaScript/TypeScript Executor =====
242
243#[derive(Default)]
244struct JavaScriptExecutor {
245    runtime: String, // "node" or "deno" or "bun"
246}
247
248impl JavaScriptExecutor {
249    #[allow(dead_code)]
250    fn new(runtime: String) -> Self {
251        Self { runtime }
252    }
253}
254
255impl RuntimeExecutor for JavaScriptExecutor {
256    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>> {
257        let runtime = if self.runtime.is_empty() {
258            "node"
259        } else {
260            &self.runtime
261        };
262
263        let mut cmd = Command::new(runtime);
264
265        cmd.arg(plugin_path)
266            .args(&config.args)
267            .envs(&config.env_vars)
268            .stdin(Stdio::piped())
269            .stdout(Stdio::piped())
270            .stderr(Stdio::piped());
271
272        if let Some(dir) = &config.working_dir {
273            cmd.current_dir(dir);
274        }
275
276        let child = cmd.spawn().map_err(|e| {
277            RegistryError::Storage(format!("Failed to start JavaScript plugin: {}", e))
278        })?;
279
280        Ok(Box::new(ProcessWrapper::new(child)))
281    }
282
283    fn is_available(&self) -> bool {
284        Command::new("node").arg("--version").output().is_ok()
285    }
286
287    fn version(&self) -> Result<String> {
288        let output = Command::new("node")
289            .arg("--version")
290            .output()
291            .map_err(|e| RegistryError::Storage(format!("Failed to get Node.js version: {}", e)))?;
292
293        Ok(String::from_utf8_lossy(&output.stdout).to_string())
294    }
295
296    fn install_dependencies(&self, plugin_path: &Path) -> Result<()> {
297        let package_json = plugin_path.join("package.json");
298
299        if package_json.exists() {
300            let output =
301                Command::new("npm").arg("install").current_dir(plugin_path).output().map_err(
302                    |e| {
303                        RegistryError::Storage(format!("Failed to install npm dependencies: {}", e))
304                    },
305                )?;
306
307            if !output.status.success() {
308                return Err(RegistryError::Storage(format!(
309                    "npm install failed: {}",
310                    String::from_utf8_lossy(&output.stderr)
311                )));
312            }
313        }
314
315        Ok(())
316    }
317}
318
319// ===== Go Executor =====
320
321struct GoExecutor;
322
323impl RuntimeExecutor for GoExecutor {
324    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>> {
325        let mut cmd = Command::new(plugin_path);
326
327        cmd.args(&config.args)
328            .envs(&config.env_vars)
329            .stdin(Stdio::piped())
330            .stdout(Stdio::piped())
331            .stderr(Stdio::piped());
332
333        if let Some(dir) = &config.working_dir {
334            cmd.current_dir(dir);
335        }
336
337        let child = cmd
338            .spawn()
339            .map_err(|e| RegistryError::Storage(format!("Failed to start Go plugin: {}", e)))?;
340
341        Ok(Box::new(ProcessWrapper::new(child)))
342    }
343
344    fn is_available(&self) -> bool {
345        Command::new("go").arg("version").output().is_ok()
346    }
347
348    fn version(&self) -> Result<String> {
349        let output = Command::new("go")
350            .arg("version")
351            .output()
352            .map_err(|e| RegistryError::Storage(format!("Failed to get Go version: {}", e)))?;
353
354        Ok(String::from_utf8_lossy(&output.stdout).to_string())
355    }
356
357    fn install_dependencies(&self, plugin_path: &Path) -> Result<()> {
358        let output = Command::new("go")
359            .args(["build", "-o", "plugin"])
360            .current_dir(plugin_path)
361            .output()
362            .map_err(|e| RegistryError::Storage(format!("Failed to build Go plugin: {}", e)))?;
363
364        if !output.status.success() {
365            return Err(RegistryError::Storage(format!(
366                "Go plugin build failed: {}",
367                String::from_utf8_lossy(&output.stderr)
368            )));
369        }
370
371        Ok(())
372    }
373}
374
375// ===== Ruby Executor =====
376
377struct RubyExecutor;
378
379impl RuntimeExecutor for RubyExecutor {
380    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>> {
381        let mut cmd = Command::new("ruby");
382
383        cmd.arg(plugin_path)
384            .args(&config.args)
385            .envs(&config.env_vars)
386            .stdin(Stdio::piped())
387            .stdout(Stdio::piped())
388            .stderr(Stdio::piped());
389
390        if let Some(dir) = &config.working_dir {
391            cmd.current_dir(dir);
392        }
393
394        let child = cmd
395            .spawn()
396            .map_err(|e| RegistryError::Storage(format!("Failed to start Ruby plugin: {}", e)))?;
397
398        Ok(Box::new(ProcessWrapper::new(child)))
399    }
400
401    fn is_available(&self) -> bool {
402        Command::new("ruby").arg("--version").output().is_ok()
403    }
404
405    fn version(&self) -> Result<String> {
406        let output = Command::new("ruby")
407            .arg("--version")
408            .output()
409            .map_err(|e| RegistryError::Storage(format!("Failed to get Ruby version: {}", e)))?;
410
411        Ok(String::from_utf8_lossy(&output.stdout).to_string())
412    }
413
414    fn install_dependencies(&self, plugin_path: &Path) -> Result<()> {
415        let gemfile = plugin_path.join("Gemfile");
416
417        if gemfile.exists() {
418            let output = Command::new("bundle")
419                .arg("install")
420                .current_dir(plugin_path)
421                .output()
422                .map_err(|e| {
423                    RegistryError::Storage(format!("Failed to install Ruby gems: {}", e))
424                })?;
425
426            if !output.status.success() {
427                return Err(RegistryError::Storage(format!(
428                    "bundle install failed: {}",
429                    String::from_utf8_lossy(&output.stderr)
430                )));
431            }
432        }
433
434        Ok(())
435    }
436}
437
438// ===== Generic Executor =====
439
440struct GenericExecutor;
441
442impl RuntimeExecutor for GenericExecutor {
443    fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>> {
444        let mut cmd = Command::new(plugin_path);
445
446        cmd.args(&config.args)
447            .envs(&config.env_vars)
448            .stdin(Stdio::piped())
449            .stdout(Stdio::piped())
450            .stderr(Stdio::piped());
451
452        if let Some(dir) = &config.working_dir {
453            cmd.current_dir(dir);
454        }
455
456        let child = cmd.spawn().map_err(|e| {
457            RegistryError::Storage(format!("Failed to start generic plugin: {}", e))
458        })?;
459
460        Ok(Box::new(ProcessWrapper::new(child)))
461    }
462
463    fn is_available(&self) -> bool {
464        true
465    }
466
467    fn version(&self) -> Result<String> {
468        Ok("unknown".to_string())
469    }
470
471    fn install_dependencies(&self, _plugin_path: &Path) -> Result<()> {
472        Ok(())
473    }
474}
475
476// ===== Process Wrapper =====
477
478struct ProcessWrapper {
479    child: Child,
480}
481
482impl ProcessWrapper {
483    fn new(child: Child) -> Self {
484        Self { child }
485    }
486}
487
488impl RuntimeProcess for ProcessWrapper {
489    fn is_running(&mut self) -> bool {
490        matches!(self.child.try_wait(), Ok(None))
491    }
492
493    fn stop(&mut self) -> Result<()> {
494        self.child
495            .kill()
496            .map_err(|e| RegistryError::Storage(format!("Failed to kill process: {}", e)))
497    }
498
499    fn pid(&self) -> Option<u32> {
500        Some(self.child.id())
501    }
502
503    fn send_message(&mut self, message: &[u8]) -> Result<()> {
504        use std::io::Write;
505
506        if let Some(stdin) = self.child.stdin.as_mut() {
507            stdin
508                .write_all(message)
509                .map_err(|e| RegistryError::Network(format!("Failed to send message: {}", e)))?;
510            stdin
511                .flush()
512                .map_err(|e| RegistryError::Network(format!("Failed to flush stdin: {}", e)))?;
513        }
514
515        Ok(())
516    }
517
518    fn receive_message(&mut self) -> Result<Vec<u8>> {
519        use std::io::Read;
520
521        if let Some(stdout) = self.child.stdout.as_mut() {
522            let mut buffer = Vec::new();
523            stdout
524                .read_to_end(&mut buffer)
525                .map_err(|e| RegistryError::Network(format!("Failed to read message: {}", e)))?;
526            return Ok(buffer);
527        }
528
529        Ok(vec![])
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use super::*;
536
537    #[test]
538    fn test_rust_executor_available() {
539        let executor = RustExecutor;
540        // This may fail in environments without Rust
541        let _ = executor.is_available();
542    }
543
544    #[test]
545    fn test_runtime_config_default() {
546        let config = RuntimeConfig::default();
547        assert_eq!(config.timeout, 30);
548        assert_eq!(config.memory_limit, Some(512));
549    }
550}