1use std::path::{Path, PathBuf};
12use std::process::Stdio;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum RuntimePlatform {
17 Native,
19 Docker,
21 Wasm,
23 Serverless,
25 Embedded,
27 Browser,
29}
30
31impl std::fmt::Display for RuntimePlatform {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 RuntimePlatform::Native => write!(f, "native"),
35 RuntimePlatform::Docker => write!(f, "docker"),
36 RuntimePlatform::Wasm => write!(f, "wasm"),
37 RuntimePlatform::Serverless => write!(f, "serverless"),
38 RuntimePlatform::Embedded => write!(f, "embedded"),
39 RuntimePlatform::Browser => write!(f, "browser"),
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, Default)]
48pub struct RuntimeCapabilities {
49 pub has_shell_access: bool,
51 pub has_filesystem_access: bool,
53 pub has_network_access: bool,
55 pub supports_long_running: bool,
57 pub supports_multithreading: bool,
59 pub supports_dynamic_loading: bool,
61 pub max_file_size: u64,
63 pub max_memory: u64,
65}
66
67impl RuntimeCapabilities {
68 pub fn full() -> Self {
70 Self {
71 has_shell_access: true,
72 has_filesystem_access: true,
73 has_network_access: true,
74 supports_long_running: true,
75 supports_multithreading: true,
76 supports_dynamic_loading: true,
77 max_file_size: 0,
78 max_memory: 0,
79 }
80 }
81
82 pub fn restricted() -> Self {
84 Self {
85 has_shell_access: false,
86 has_filesystem_access: false,
87 has_network_access: false,
88 supports_long_running: false,
89 supports_multithreading: false,
90 supports_dynamic_loading: false,
91 max_file_size: 10 * 1024 * 1024, max_memory: 128 * 1024 * 1024, }
94 }
95
96 pub fn container() -> Self {
98 Self {
99 has_shell_access: true,
100 has_filesystem_access: true,
101 has_network_access: true,
102 supports_long_running: true,
103 supports_multithreading: true,
104 supports_dynamic_loading: false, max_file_size: 100 * 1024 * 1024, max_memory: 512 * 1024 * 1024, }
108 }
109}
110
111#[async_trait::async_trait]
115pub trait RuntimeAdapter: Send + Sync {
116 fn name(&self) -> &str;
118
119 fn platform(&self) -> RuntimePlatform;
121
122 fn capabilities(&self) -> RuntimeCapabilities;
124
125 fn storage_path(&self) -> PathBuf;
129
130 fn temp_path(&self) -> PathBuf;
132
133 fn memory_budget(&self) -> u64 {
135 self.capabilities().max_memory
136 }
137
138 fn has_shell_access(&self) -> bool {
140 self.capabilities().has_shell_access
141 }
142
143 fn has_filesystem_access(&self) -> bool {
145 self.capabilities().has_filesystem_access
146 }
147
148 fn has_network_access(&self) -> bool {
150 self.capabilities().has_network_access
151 }
152
153 fn supports_long_running(&self) -> bool {
155 self.capabilities().supports_long_running
156 }
157
158 fn build_shell_command(
166 &self,
167 command: &str,
168 working_dir: Option<&Path>,
169 ) -> Result<tokio::process::Command, RuntimeError>;
170
171 async fn read_file(&self, path: &Path) -> Result<String, RuntimeError>;
175
176 async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError>;
180
181 async fn file_exists(&self, path: &Path) -> bool;
183
184 async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError>;
186
187 async fn list_directory(&self, path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError>;
189
190 async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError>;
192
193 async fn execute_shell(
197 &self,
198 command: &str,
199 working_dir: Option<&Path>,
200 timeout_secs: Option<u64>,
201 ) -> Result<ShellResult, RuntimeError>;
202
203 fn get_env(&self, key: &str) -> Option<String>;
205
206 fn set_env(&self, key: &str, value: &str) -> Result<(), RuntimeError>;
212
213 fn log(&self, level: LogLevel, message: &str);
215
216 fn current_timestamp(&self) -> u64 {
218 std::time::SystemTime::now()
219 .duration_since(std::time::UNIX_EPOCH)
220 .map_or(0, |d| d.as_secs())
221 }
222}
223
224#[derive(Debug, Clone)]
226pub enum RuntimeError {
227 NotSupported(String),
229 PermissionDenied(String),
231 ResourceLimit(String),
233 IoError(String),
235 Timeout(String),
237 Other(String),
239}
240
241impl std::fmt::Display for RuntimeError {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match self {
244 RuntimeError::NotSupported(msg) => write!(f, "操作不被支持: {msg}"),
245 RuntimeError::PermissionDenied(msg) => write!(f, "权限不足: {msg}"),
246 RuntimeError::ResourceLimit(msg) => write!(f, "资源限制: {msg}"),
247 RuntimeError::IoError(msg) => write!(f, "IO 错误: {msg}"),
248 RuntimeError::Timeout(msg) => write!(f, "超时: {msg}"),
249 RuntimeError::Other(msg) => write!(f, "错误: {msg}"),
250 }
251 }
252}
253
254impl std::error::Error for RuntimeError {}
255
256#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
258pub enum LogLevel {
259 Trace,
260 Debug,
261 Info,
262 Warn,
263 Error,
264}
265
266#[derive(Debug, Clone)]
268pub struct ShellResult {
269 pub exit_code: i32,
271 pub stdout: String,
273 pub stderr: String,
275 pub duration_ms: u64,
277}
278
279pub struct NativeRuntimeAdapter {
281 name: String,
282 storage_path: PathBuf,
283 capabilities: RuntimeCapabilities,
284}
285
286impl NativeRuntimeAdapter {
287 pub fn new() -> Self {
289 let storage_path = PathBuf::from("./.rucora");
291
292 Self {
293 name: "native".to_string(),
294 storage_path,
295 capabilities: RuntimeCapabilities::full(),
296 }
297 }
298
299 pub fn with_storage_path(mut self, path: impl AsRef<Path>) -> Self {
301 self.storage_path = path.as_ref().to_path_buf();
302 self
303 }
304
305 pub fn with_capabilities(mut self, capabilities: RuntimeCapabilities) -> Self {
307 self.capabilities = capabilities;
308 self
309 }
310}
311
312impl Default for NativeRuntimeAdapter {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318#[async_trait::async_trait]
319impl RuntimeAdapter for NativeRuntimeAdapter {
320 fn name(&self) -> &str {
321 &self.name
322 }
323
324 fn platform(&self) -> RuntimePlatform {
325 RuntimePlatform::Native
326 }
327
328 fn capabilities(&self) -> RuntimeCapabilities {
329 self.capabilities
330 }
331
332 fn storage_path(&self) -> PathBuf {
333 self.storage_path.clone()
334 }
335
336 fn temp_path(&self) -> PathBuf {
337 std::env::temp_dir().join("rucora")
338 }
339
340 fn build_shell_command(
341 &self,
342 command: &str,
343 working_dir: Option<&Path>,
344 ) -> Result<tokio::process::Command, RuntimeError> {
345 if !self.has_shell_access() {
346 return Err(RuntimeError::NotSupported(
347 "当前运行时不支持 shell 访问".to_string(),
348 ));
349 }
350
351 #[cfg(target_os = "windows")]
352 let mut cmd = {
353 let mut c = tokio::process::Command::new("cmd");
354 c.arg("/C").arg(command);
355 c
356 };
357
358 #[cfg(not(target_os = "windows"))]
359 let mut cmd = {
360 let mut c = tokio::process::Command::new("sh");
361 c.arg("-c").arg(command);
362 c
363 };
364
365 if let Some(dir) = working_dir {
366 cmd.current_dir(dir);
367 }
368
369 Ok(cmd)
370 }
371
372 async fn read_file(&self, path: &Path) -> Result<String, RuntimeError> {
373 if !self.has_filesystem_access() {
374 return Err(RuntimeError::NotSupported(
375 "当前运行时不支持文件系统访问".to_string(),
376 ));
377 }
378
379 if let Ok(metadata) = tokio::fs::metadata(path).await {
381 let size = metadata.len();
382 let max_size = self.capabilities.max_file_size;
383 if max_size > 0 && size > max_size {
384 return Err(RuntimeError::ResourceLimit(format!(
385 "文件大小 {size} 超过限制 {max_size}"
386 )));
387 }
388 }
389
390 tokio::fs::read_to_string(path)
391 .await
392 .map_err(|e| RuntimeError::IoError(e.to_string()))
393 }
394
395 async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError> {
396 if !self.has_filesystem_access() {
397 return Err(RuntimeError::NotSupported(
398 "当前运行时不支持文件系统访问".to_string(),
399 ));
400 }
401
402 let content_size = content.len() as u64;
404 let max_size = self.capabilities.max_file_size;
405 if max_size > 0 && content_size > max_size {
406 return Err(RuntimeError::ResourceLimit(format!(
407 "内容大小 {content_size} 超过限制 {max_size}"
408 )));
409 }
410
411 if let Some(parent) = path.parent() {
413 tokio::fs::create_dir_all(parent)
414 .await
415 .map_err(|e| RuntimeError::IoError(e.to_string()))?;
416 }
417
418 tokio::fs::write(path, content)
419 .await
420 .map_err(|e| RuntimeError::IoError(e.to_string()))
421 }
422
423 async fn file_exists(&self, path: &Path) -> bool {
424 if !self.has_filesystem_access() {
425 return false;
426 }
427 tokio::fs::metadata(path).await.is_ok()
428 }
429
430 async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError> {
431 if !self.has_filesystem_access() {
432 return Err(RuntimeError::NotSupported(
433 "当前运行时不支持文件系统访问".to_string(),
434 ));
435 }
436
437 tokio::fs::metadata(path)
438 .await
439 .map(|m| m.len())
440 .map_err(|e| RuntimeError::IoError(e.to_string()))
441 }
442
443 async fn list_directory(&self, path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError> {
444 if !self.has_filesystem_access() {
445 return Err(RuntimeError::NotSupported(
446 "当前运行时不支持文件系统访问".to_string(),
447 ));
448 }
449
450 let mut entries = Vec::new();
451 let mut dir = tokio::fs::read_dir(path)
452 .await
453 .map_err(|e| RuntimeError::IoError(e.to_string()))?;
454
455 while let Some(entry) = dir
456 .next_entry()
457 .await
458 .map_err(|e| RuntimeError::IoError(e.to_string()))?
459 {
460 entries.push(entry);
461 }
462
463 Ok(entries)
464 }
465
466 async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError> {
467 if !self.has_filesystem_access() {
468 return Err(RuntimeError::NotSupported(
469 "当前运行时不支持文件系统访问".to_string(),
470 ));
471 }
472
473 tokio::fs::create_dir_all(path)
474 .await
475 .map_err(|e| RuntimeError::IoError(e.to_string()))
476 }
477
478 async fn execute_shell(
479 &self,
480 command: &str,
481 working_dir: Option<&Path>,
482 timeout_secs: Option<u64>,
483 ) -> Result<ShellResult, RuntimeError> {
484 if !self.has_shell_access() {
485 return Err(RuntimeError::NotSupported(
486 "当前运行时不支持 shell 访问".to_string(),
487 ));
488 }
489
490 let mut cmd = self.build_shell_command(command, working_dir)?;
491
492 cmd.stdout(Stdio::piped());
493 cmd.stderr(Stdio::piped());
494
495 let start = std::time::Instant::now();
496
497 let output = if let Some(timeout) = timeout_secs {
498 tokio::time::timeout(tokio::time::Duration::from_secs(timeout), cmd.output())
499 .await
500 .map_err(|_| RuntimeError::Timeout("命令执行超时".to_string()))?
501 .map_err(|e| RuntimeError::IoError(e.to_string()))?
502 } else {
503 cmd.output()
504 .await
505 .map_err(|e| RuntimeError::IoError(e.to_string()))?
506 };
507
508 let duration_ms = start.elapsed().as_millis() as u64;
509
510 Ok(ShellResult {
511 exit_code: output.status.code().unwrap_or(-1),
512 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
513 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
514 duration_ms,
515 })
516 }
517
518 fn get_env(&self, key: &str) -> Option<String> {
519 std::env::var(key).ok()
520 }
521
522 fn set_env(&self, key: &str, value: &str) -> Result<(), RuntimeError> {
523 unsafe {
525 std::env::set_var(key, value);
526 }
527 Ok(())
528 }
529
530 #[allow(clippy::cognitive_complexity)]
531 fn log(&self, level: LogLevel, message: &str) {
532 match level {
533 LogLevel::Trace => tracing::trace!("{}", message),
534 LogLevel::Debug => tracing::debug!("{}", message),
535 LogLevel::Info => tracing::info!("{}", message),
536 LogLevel::Warn => tracing::warn!("{}", message),
537 LogLevel::Error => tracing::error!("{}", message),
538 }
539 }
540}
541
542pub struct RestrictedRuntimeAdapter {
544 name: String,
545 storage_path: PathBuf,
546}
547
548impl RestrictedRuntimeAdapter {
549 pub fn new() -> Self {
551 Self {
552 name: "restricted".to_string(),
553 storage_path: PathBuf::from("/tmp/rucora"),
554 }
555 }
556}
557
558impl Default for RestrictedRuntimeAdapter {
559 fn default() -> Self {
560 Self::new()
561 }
562}
563
564#[async_trait::async_trait]
565impl RuntimeAdapter for RestrictedRuntimeAdapter {
566 fn name(&self) -> &str {
567 &self.name
568 }
569
570 fn platform(&self) -> RuntimePlatform {
571 RuntimePlatform::Wasm
572 }
573
574 fn capabilities(&self) -> RuntimeCapabilities {
575 RuntimeCapabilities::restricted()
576 }
577
578 fn storage_path(&self) -> PathBuf {
579 self.storage_path.clone()
580 }
581
582 fn temp_path(&self) -> PathBuf {
583 PathBuf::from("/tmp")
584 }
585
586 fn build_shell_command(
587 &self,
588 _command: &str,
589 _working_dir: Option<&Path>,
590 ) -> Result<tokio::process::Command, RuntimeError> {
591 Err(RuntimeError::NotSupported(
592 "受限运行时不支持 shell 命令".to_string(),
593 ))
594 }
595
596 async fn read_file(&self, path: &Path) -> Result<String, RuntimeError> {
597 if !path.starts_with(&self.storage_path) {
599 return Err(RuntimeError::PermissionDenied(
600 "只能访问存储目录内的文件".to_string(),
601 ));
602 }
603
604 tokio::fs::read_to_string(path)
605 .await
606 .map_err(|e| RuntimeError::IoError(e.to_string()))
607 }
608
609 async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError> {
610 if !path.starts_with(&self.storage_path) {
611 return Err(RuntimeError::PermissionDenied(
612 "只能写入存储目录内的文件".to_string(),
613 ));
614 }
615
616 tokio::fs::write(path, content)
617 .await
618 .map_err(|e| RuntimeError::IoError(e.to_string()))
619 }
620
621 async fn file_exists(&self, path: &Path) -> bool {
622 tokio::fs::metadata(path).await.is_ok()
623 }
624
625 async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError> {
626 tokio::fs::metadata(path)
627 .await
628 .map(|m| m.len())
629 .map_err(|e| RuntimeError::IoError(e.to_string()))
630 }
631
632 async fn list_directory(&self, _path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError> {
633 Err(RuntimeError::NotSupported(
634 "受限运行时不支持目录列表".to_string(),
635 ))
636 }
637
638 async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError> {
639 if !path.starts_with(&self.storage_path) {
640 return Err(RuntimeError::PermissionDenied(
641 "只能在存储目录内创建目录".to_string(),
642 ));
643 }
644
645 tokio::fs::create_dir_all(path)
646 .await
647 .map_err(|e| RuntimeError::IoError(e.to_string()))
648 }
649
650 async fn execute_shell(
651 &self,
652 _command: &str,
653 _working_dir: Option<&Path>,
654 _timeout_secs: Option<u64>,
655 ) -> Result<ShellResult, RuntimeError> {
656 Err(RuntimeError::NotSupported(
657 "受限运行时不支持 shell 命令".to_string(),
658 ))
659 }
660
661 fn get_env(&self, key: &str) -> Option<String> {
662 if key.starts_with("rucora_") {
664 std::env::var(key).ok()
665 } else {
666 None
667 }
668 }
669
670 fn set_env(&self, _key: &str, _value: &str) -> Result<(), RuntimeError> {
671 Err(RuntimeError::NotSupported(
672 "受限运行时不支持设置环境变量".to_string(),
673 ))
674 }
675
676 fn log(&self, level: LogLevel, message: &str) {
677 eprintln!("[{level:?}] {message}");
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685
686 #[test]
687 fn test_runtime_platform_display() {
688 assert_eq!(RuntimePlatform::Native.to_string(), "native");
689 assert_eq!(RuntimePlatform::Docker.to_string(), "docker");
690 assert_eq!(RuntimePlatform::Wasm.to_string(), "wasm");
691 }
692
693 #[test]
694 fn test_runtime_capabilities() {
695 let full = RuntimeCapabilities::full();
696 assert!(full.has_shell_access);
697 assert!(full.has_filesystem_access);
698 assert!(full.supports_long_running);
699
700 let restricted = RuntimeCapabilities::restricted();
701 assert!(!restricted.has_shell_access);
702 assert!(!restricted.has_filesystem_access);
703 assert!(!restricted.supports_long_running);
704
705 let container = RuntimeCapabilities::container();
706 assert!(container.has_shell_access);
707 assert!(!container.supports_dynamic_loading);
708 }
709
710 #[test]
711 fn test_runtime_error_display() {
712 let err = RuntimeError::NotSupported("test".to_string());
713 assert!(err.to_string().contains("不被支持"));
714
715 let err = RuntimeError::PermissionDenied("test".to_string());
716 assert!(err.to_string().contains("权限不足"));
717 }
718
719 #[tokio::test]
720 async fn test_native_runtime_adapter() {
721 let adapter = NativeRuntimeAdapter::new();
722
723 assert_eq!(adapter.name(), "native");
724 assert!(adapter.has_shell_access());
725 assert!(adapter.has_filesystem_access());
726 assert!(adapter.supports_long_running());
727
728 let test_path = adapter.temp_path().join("test.txt");
730 adapter.write_file(&test_path, "hello").await.unwrap();
731
732 assert!(adapter.file_exists(&test_path).await);
733 assert_eq!(adapter.file_size(&test_path).await.unwrap(), 5);
734
735 let content = adapter.read_file(&test_path).await.unwrap();
736 assert_eq!(content, "hello");
737
738 tokio::fs::remove_file(&test_path).await.ok();
740 }
741
742 #[tokio::test]
743 async fn test_restricted_runtime_adapter() {
744 let adapter = RestrictedRuntimeAdapter::new();
745
746 assert_eq!(adapter.name(), "restricted");
747 assert!(!adapter.has_shell_access());
748 assert!(!adapter.has_filesystem_access());
749
750 let result = adapter.execute_shell("echo hello", None, None).await;
752 assert!(matches!(result, Err(RuntimeError::NotSupported(_))));
753 }
754}