mockforge_plugin_registry/
runtime.rs1use crate::{RegistryError, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use std::process::{Child, Command, Stdio};
8
9#[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 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
38pub trait RuntimeExecutor: Send + Sync {
40 fn start(&self, plugin_path: &Path, config: &RuntimeConfig) -> Result<Box<dyn RuntimeProcess>>;
42
43 fn is_available(&self) -> bool;
45
46 fn version(&self) -> Result<String>;
48
49 fn install_dependencies(&self, plugin_path: &Path) -> Result<()>;
51}
52
53pub trait RuntimeProcess: Send + Sync {
55 fn is_running(&mut self) -> bool;
57
58 fn stop(&mut self) -> Result<()>;
60
61 fn pid(&self) -> Option<u32>;
63
64 fn send_message(&mut self, message: &[u8]) -> Result<()>;
66
67 fn receive_message(&mut self) -> Result<Vec<u8>>;
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct RuntimeConfig {
74 pub env_vars: HashMap<String, String>,
76
77 pub working_dir: Option<PathBuf>,
79
80 pub args: Vec<String>,
82
83 pub timeout: u64,
85
86 pub memory_limit: Option<u64>,
88
89 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), cpu_limit: None,
102 }
103 }
104}
105
106struct 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#[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#[derive(Default)]
244struct JavaScriptExecutor {
245 runtime: String, }
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
319struct 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
375struct 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
438struct 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
476struct 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 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}