use std::path::{Path, PathBuf};
use std::process::Stdio;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimePlatform {
Native,
Docker,
Wasm,
Serverless,
Embedded,
Browser,
}
impl std::fmt::Display for RuntimePlatform {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimePlatform::Native => write!(f, "native"),
RuntimePlatform::Docker => write!(f, "docker"),
RuntimePlatform::Wasm => write!(f, "wasm"),
RuntimePlatform::Serverless => write!(f, "serverless"),
RuntimePlatform::Embedded => write!(f, "embedded"),
RuntimePlatform::Browser => write!(f, "browser"),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RuntimeCapabilities {
pub has_shell_access: bool,
pub has_filesystem_access: bool,
pub has_network_access: bool,
pub supports_long_running: bool,
pub supports_multithreading: bool,
pub supports_dynamic_loading: bool,
pub max_file_size: u64,
pub max_memory: u64,
}
impl RuntimeCapabilities {
pub fn full() -> Self {
Self {
has_shell_access: true,
has_filesystem_access: true,
has_network_access: true,
supports_long_running: true,
supports_multithreading: true,
supports_dynamic_loading: true,
max_file_size: 0,
max_memory: 0,
}
}
pub fn restricted() -> Self {
Self {
has_shell_access: false,
has_filesystem_access: false,
has_network_access: false,
supports_long_running: false,
supports_multithreading: false,
supports_dynamic_loading: false,
max_file_size: 10 * 1024 * 1024, max_memory: 128 * 1024 * 1024, }
}
pub fn container() -> Self {
Self {
has_shell_access: true,
has_filesystem_access: true,
has_network_access: true,
supports_long_running: true,
supports_multithreading: true,
supports_dynamic_loading: false, max_file_size: 100 * 1024 * 1024, max_memory: 512 * 1024 * 1024, }
}
}
#[async_trait::async_trait]
pub trait RuntimeAdapter: Send + Sync {
fn name(&self) -> &str;
fn platform(&self) -> RuntimePlatform;
fn capabilities(&self) -> RuntimeCapabilities;
fn storage_path(&self) -> PathBuf;
fn temp_path(&self) -> PathBuf;
fn memory_budget(&self) -> u64 {
self.capabilities().max_memory
}
fn has_shell_access(&self) -> bool {
self.capabilities().has_shell_access
}
fn has_filesystem_access(&self) -> bool {
self.capabilities().has_filesystem_access
}
fn has_network_access(&self) -> bool {
self.capabilities().has_network_access
}
fn supports_long_running(&self) -> bool {
self.capabilities().supports_long_running
}
fn build_shell_command(
&self,
command: &str,
working_dir: Option<&Path>,
) -> Result<tokio::process::Command, RuntimeError>;
async fn read_file(&self, path: &Path) -> Result<String, RuntimeError>;
async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError>;
async fn file_exists(&self, path: &Path) -> bool;
async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError>;
async fn list_directory(&self, path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError>;
async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError>;
async fn execute_shell(
&self,
command: &str,
working_dir: Option<&Path>,
timeout_secs: Option<u64>,
) -> Result<ShellResult, RuntimeError>;
fn get_env(&self, key: &str) -> Option<String>;
fn set_env(&self, key: &str, value: &str) -> Result<(), RuntimeError>;
fn log(&self, level: LogLevel, message: &str);
fn current_timestamp(&self) -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
}
#[derive(Debug, Clone)]
pub enum RuntimeError {
NotSupported(String),
PermissionDenied(String),
ResourceLimit(String),
IoError(String),
Timeout(String),
Other(String),
}
impl std::fmt::Display for RuntimeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RuntimeError::NotSupported(msg) => write!(f, "操作不被支持: {msg}"),
RuntimeError::PermissionDenied(msg) => write!(f, "权限不足: {msg}"),
RuntimeError::ResourceLimit(msg) => write!(f, "资源限制: {msg}"),
RuntimeError::IoError(msg) => write!(f, "IO 错误: {msg}"),
RuntimeError::Timeout(msg) => write!(f, "超时: {msg}"),
RuntimeError::Other(msg) => write!(f, "错误: {msg}"),
}
}
}
impl std::error::Error for RuntimeError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone)]
pub struct ShellResult {
pub exit_code: i32,
pub stdout: String,
pub stderr: String,
pub duration_ms: u64,
}
pub struct NativeRuntimeAdapter {
name: String,
storage_path: PathBuf,
capabilities: RuntimeCapabilities,
}
impl NativeRuntimeAdapter {
pub fn new() -> Self {
let storage_path = PathBuf::from("./.rucora");
Self {
name: "native".to_string(),
storage_path,
capabilities: RuntimeCapabilities::full(),
}
}
pub fn with_storage_path(mut self, path: impl AsRef<Path>) -> Self {
self.storage_path = path.as_ref().to_path_buf();
self
}
pub fn with_capabilities(mut self, capabilities: RuntimeCapabilities) -> Self {
self.capabilities = capabilities;
self
}
}
impl Default for NativeRuntimeAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl RuntimeAdapter for NativeRuntimeAdapter {
fn name(&self) -> &str {
&self.name
}
fn platform(&self) -> RuntimePlatform {
RuntimePlatform::Native
}
fn capabilities(&self) -> RuntimeCapabilities {
self.capabilities
}
fn storage_path(&self) -> PathBuf {
self.storage_path.clone()
}
fn temp_path(&self) -> PathBuf {
std::env::temp_dir().join("rucora")
}
fn build_shell_command(
&self,
command: &str,
working_dir: Option<&Path>,
) -> Result<tokio::process::Command, RuntimeError> {
if !self.has_shell_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持 shell 访问".to_string(),
));
}
#[cfg(target_os = "windows")]
let mut cmd = {
let mut c = tokio::process::Command::new("cmd");
c.arg("/C").arg(command);
c
};
#[cfg(not(target_os = "windows"))]
let mut cmd = {
let mut c = tokio::process::Command::new("sh");
c.arg("-c").arg(command);
c
};
if let Some(dir) = working_dir {
cmd.current_dir(dir);
}
Ok(cmd)
}
async fn read_file(&self, path: &Path) -> Result<String, RuntimeError> {
if !self.has_filesystem_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持文件系统访问".to_string(),
));
}
if let Ok(metadata) = tokio::fs::metadata(path).await {
let size = metadata.len();
let max_size = self.capabilities.max_file_size;
if max_size > 0 && size > max_size {
return Err(RuntimeError::ResourceLimit(format!(
"文件大小 {size} 超过限制 {max_size}"
)));
}
}
tokio::fs::read_to_string(path)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError> {
if !self.has_filesystem_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持文件系统访问".to_string(),
));
}
let content_size = content.len() as u64;
let max_size = self.capabilities.max_file_size;
if max_size > 0 && content_size > max_size {
return Err(RuntimeError::ResourceLimit(format!(
"内容大小 {content_size} 超过限制 {max_size}"
)));
}
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))?;
}
tokio::fs::write(path, content)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn file_exists(&self, path: &Path) -> bool {
if !self.has_filesystem_access() {
return false;
}
tokio::fs::metadata(path).await.is_ok()
}
async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError> {
if !self.has_filesystem_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持文件系统访问".to_string(),
));
}
tokio::fs::metadata(path)
.await
.map(|m| m.len())
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn list_directory(&self, path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError> {
if !self.has_filesystem_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持文件系统访问".to_string(),
));
}
let mut entries = Vec::new();
let mut dir = tokio::fs::read_dir(path)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))?;
while let Some(entry) = dir
.next_entry()
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))?
{
entries.push(entry);
}
Ok(entries)
}
async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError> {
if !self.has_filesystem_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持文件系统访问".to_string(),
));
}
tokio::fs::create_dir_all(path)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn execute_shell(
&self,
command: &str,
working_dir: Option<&Path>,
timeout_secs: Option<u64>,
) -> Result<ShellResult, RuntimeError> {
if !self.has_shell_access() {
return Err(RuntimeError::NotSupported(
"当前运行时不支持 shell 访问".to_string(),
));
}
let mut cmd = self.build_shell_command(command, working_dir)?;
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let start = std::time::Instant::now();
let output = if let Some(timeout) = timeout_secs {
tokio::time::timeout(tokio::time::Duration::from_secs(timeout), cmd.output())
.await
.map_err(|_| RuntimeError::Timeout("命令执行超时".to_string()))?
.map_err(|e| RuntimeError::IoError(e.to_string()))?
} else {
cmd.output()
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))?
};
let duration_ms = start.elapsed().as_millis() as u64;
Ok(ShellResult {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
duration_ms,
})
}
fn get_env(&self, key: &str) -> Option<String> {
std::env::var(key).ok()
}
fn set_env(&self, key: &str, value: &str) -> Result<(), RuntimeError> {
unsafe {
std::env::set_var(key, value);
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
fn log(&self, level: LogLevel, message: &str) {
match level {
LogLevel::Trace => tracing::trace!("{}", message),
LogLevel::Debug => tracing::debug!("{}", message),
LogLevel::Info => tracing::info!("{}", message),
LogLevel::Warn => tracing::warn!("{}", message),
LogLevel::Error => tracing::error!("{}", message),
}
}
}
pub struct RestrictedRuntimeAdapter {
name: String,
storage_path: PathBuf,
}
impl RestrictedRuntimeAdapter {
pub fn new() -> Self {
Self {
name: "restricted".to_string(),
storage_path: PathBuf::from("/tmp/rucora"),
}
}
}
impl Default for RestrictedRuntimeAdapter {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl RuntimeAdapter for RestrictedRuntimeAdapter {
fn name(&self) -> &str {
&self.name
}
fn platform(&self) -> RuntimePlatform {
RuntimePlatform::Wasm
}
fn capabilities(&self) -> RuntimeCapabilities {
RuntimeCapabilities::restricted()
}
fn storage_path(&self) -> PathBuf {
self.storage_path.clone()
}
fn temp_path(&self) -> PathBuf {
PathBuf::from("/tmp")
}
fn build_shell_command(
&self,
_command: &str,
_working_dir: Option<&Path>,
) -> Result<tokio::process::Command, RuntimeError> {
Err(RuntimeError::NotSupported(
"受限运行时不支持 shell 命令".to_string(),
))
}
async fn read_file(&self, path: &Path) -> Result<String, RuntimeError> {
if !path.starts_with(&self.storage_path) {
return Err(RuntimeError::PermissionDenied(
"只能访问存储目录内的文件".to_string(),
));
}
tokio::fs::read_to_string(path)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn write_file(&self, path: &Path, content: &str) -> Result<(), RuntimeError> {
if !path.starts_with(&self.storage_path) {
return Err(RuntimeError::PermissionDenied(
"只能写入存储目录内的文件".to_string(),
));
}
tokio::fs::write(path, content)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn file_exists(&self, path: &Path) -> bool {
tokio::fs::metadata(path).await.is_ok()
}
async fn file_size(&self, path: &Path) -> Result<u64, RuntimeError> {
tokio::fs::metadata(path)
.await
.map(|m| m.len())
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn list_directory(&self, _path: &Path) -> Result<Vec<tokio::fs::DirEntry>, RuntimeError> {
Err(RuntimeError::NotSupported(
"受限运行时不支持目录列表".to_string(),
))
}
async fn create_directory(&self, path: &Path) -> Result<(), RuntimeError> {
if !path.starts_with(&self.storage_path) {
return Err(RuntimeError::PermissionDenied(
"只能在存储目录内创建目录".to_string(),
));
}
tokio::fs::create_dir_all(path)
.await
.map_err(|e| RuntimeError::IoError(e.to_string()))
}
async fn execute_shell(
&self,
_command: &str,
_working_dir: Option<&Path>,
_timeout_secs: Option<u64>,
) -> Result<ShellResult, RuntimeError> {
Err(RuntimeError::NotSupported(
"受限运行时不支持 shell 命令".to_string(),
))
}
fn get_env(&self, key: &str) -> Option<String> {
if key.starts_with("rucora_") {
std::env::var(key).ok()
} else {
None
}
}
fn set_env(&self, _key: &str, _value: &str) -> Result<(), RuntimeError> {
Err(RuntimeError::NotSupported(
"受限运行时不支持设置环境变量".to_string(),
))
}
fn log(&self, level: LogLevel, message: &str) {
eprintln!("[{level:?}] {message}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_runtime_platform_display() {
assert_eq!(RuntimePlatform::Native.to_string(), "native");
assert_eq!(RuntimePlatform::Docker.to_string(), "docker");
assert_eq!(RuntimePlatform::Wasm.to_string(), "wasm");
}
#[test]
fn test_runtime_capabilities() {
let full = RuntimeCapabilities::full();
assert!(full.has_shell_access);
assert!(full.has_filesystem_access);
assert!(full.supports_long_running);
let restricted = RuntimeCapabilities::restricted();
assert!(!restricted.has_shell_access);
assert!(!restricted.has_filesystem_access);
assert!(!restricted.supports_long_running);
let container = RuntimeCapabilities::container();
assert!(container.has_shell_access);
assert!(!container.supports_dynamic_loading);
}
#[test]
fn test_runtime_error_display() {
let err = RuntimeError::NotSupported("test".to_string());
assert!(err.to_string().contains("不被支持"));
let err = RuntimeError::PermissionDenied("test".to_string());
assert!(err.to_string().contains("权限不足"));
}
#[tokio::test]
async fn test_native_runtime_adapter() {
let adapter = NativeRuntimeAdapter::new();
assert_eq!(adapter.name(), "native");
assert!(adapter.has_shell_access());
assert!(adapter.has_filesystem_access());
assert!(adapter.supports_long_running());
let test_path = adapter.temp_path().join("test.txt");
adapter.write_file(&test_path, "hello").await.unwrap();
assert!(adapter.file_exists(&test_path).await);
assert_eq!(adapter.file_size(&test_path).await.unwrap(), 5);
let content = adapter.read_file(&test_path).await.unwrap();
assert_eq!(content, "hello");
tokio::fs::remove_file(&test_path).await.ok();
}
#[tokio::test]
async fn test_restricted_runtime_adapter() {
let adapter = RestrictedRuntimeAdapter::new();
assert_eq!(adapter.name(), "restricted");
assert!(!adapter.has_shell_access());
assert!(!adapter.has_filesystem_access());
let result = adapter.execute_shell("echo hello", None, None).await;
assert!(matches!(result, Err(RuntimeError::NotSupported(_))));
}
}