#[cfg(feature = "vm")]
use crate::config::{Config, IsolationLevel};
use crate::error::SdkError;
#[cfg(feature = "vm")]
use crate::router::resolve_isolation;
use crate::sandbox::SandboxInner;
#[cfg(all(feature = "vm", target_os = "linux"))]
use crate::types::StreamEvent;
use mimobox_core::{ErrorCode, Sandbox as CoreSandbox};
#[cfg(feature = "vm")]
use mimobox_vm::GuestFileErrorKind;
#[cfg(feature = "vm")]
use std::sync::Arc;
#[cfg(all(feature = "vm", target_os = "linux"))]
use std::sync::mpsc;
pub(crate) fn destroy_backend_inner(inner: SandboxInner) -> Result<(), SdkError> {
match inner {
#[cfg(all(feature = "os", target_os = "linux"))]
SandboxInner::Os(sandbox) => sandbox
.destroy()
.map_err(SdkError::from_sandbox_destroy_error),
#[cfg(all(feature = "os", target_os = "macos"))]
SandboxInner::OsMac(sandbox) => sandbox
.destroy()
.map_err(SdkError::from_sandbox_destroy_error),
#[cfg(all(feature = "vm", target_os = "linux"))]
SandboxInner::MicroVm(sandbox) => sandbox
.destroy()
.map_err(SdkError::from_sandbox_destroy_error),
#[cfg(all(feature = "vm", target_os = "linux"))]
SandboxInner::PooledMicroVm(pooled) => {
drop(pooled);
Ok(())
}
#[cfg(all(feature = "vm", target_os = "linux"))]
SandboxInner::RestoredPooledMicroVm(pooled) => {
drop(pooled);
Ok(())
}
#[cfg(feature = "wasm")]
SandboxInner::Wasm(sandbox) => sandbox
.destroy()
.map_err(SdkError::from_sandbox_destroy_error),
}
}
pub(crate) fn parse_command(command: &str) -> Result<Vec<String>, SdkError> {
shlex::split(command).ok_or_else(|| {
SdkError::Config("command parsing failed: mismatched shell-style quotes".to_string())
})
}
pub(crate) fn build_code_command(language: &str, code: &str) -> Result<String, SdkError> {
let quoted = shlex::try_quote(code).map_err(|_| {
SdkError::Config("code contains characters that cannot be shell-escaped".to_string())
})?;
match language {
"bash" => Ok(format!("bash -c {quoted}")),
"sh" | "shell" => Ok(format!("sh -c {quoted}")),
"python" | "python3" | "py" => Ok(format!("python3 -c {quoted}")),
"javascript" | "js" | "node" | "nodejs" => Ok(format!("node -e {quoted}")),
_ => Err(SdkError::sandbox(
ErrorCode::InvalidConfig,
format!("unsupported language: {language}"),
Some(
"Supported: bash, sh, shell, python, python3, py, javascript, js, node, nodejs"
.to_string(),
),
)),
}
}
pub(crate) fn map_pty_create_error(error: mimobox_core::SandboxError) -> SdkError {
match error {
mimobox_core::SandboxError::UnsupportedOperation(message) => SdkError::sandbox(
ErrorCode::UnsupportedPlatform,
message,
Some("set isolation to `Os` or use default Auto".to_string()),
),
other => SdkError::sandbox(ErrorCode::SandboxCreateFailed, other.to_string(), None),
}
}
pub(crate) fn map_pty_session_error(error: mimobox_core::SandboxError) -> SdkError {
match error {
mimobox_core::SandboxError::UnsupportedOperation(message) => SdkError::sandbox(
ErrorCode::UnsupportedPlatform,
message,
Some("set isolation to `Os` or use default Auto".to_string()),
),
mimobox_core::SandboxError::Timeout => SdkError::sandbox(
ErrorCode::CommandTimeout,
"PTY session execution timed out",
Some("increase Config.timeout or PtyConfig.timeout".to_string()),
),
other => SdkError::sandbox(ErrorCode::SandboxDestroyed, other.to_string(), None),
}
}
pub(crate) fn map_snapshot_bytes_error(error: mimobox_core::SandboxError) -> SdkError {
match error {
mimobox_core::SandboxError::InvalidSnapshot => SdkError::sandbox(
ErrorCode::InvalidConfig,
"invalid sandbox snapshot",
Some(
"for file-mode snapshots, prefer from_snapshot()/restore() or to_bytes()"
.to_string(),
),
),
mimobox_core::SandboxError::ExecutionFailed(message) => SdkError::sandbox(
ErrorCode::InvalidConfig,
message,
Some(
"ensure snapshot data is non-empty and from a mimobox microVM snapshot".to_string(),
),
),
mimobox_core::SandboxError::Io(error) => SdkError::Io(error),
other => SdkError::sandbox(ErrorCode::InvalidConfig, other.to_string(), None),
}
}
#[cfg(all(feature = "vm", target_os = "linux"))]
pub(crate) fn bridge_vm_stream(
source: mpsc::Receiver<mimobox_vm::StreamEvent>,
) -> mpsc::Receiver<StreamEvent> {
let (tx, rx) = mpsc::sync_channel(32);
std::thread::spawn(move || {
while let Ok(event) = source.recv() {
let mapped = match event {
mimobox_vm::StreamEvent::Stdout(data) => StreamEvent::Stdout(data),
mimobox_vm::StreamEvent::Stderr(data) => StreamEvent::Stderr(data),
mimobox_vm::StreamEvent::Exit(code) => StreamEvent::Exit(code),
mimobox_vm::StreamEvent::TimedOut => StreamEvent::TimedOut,
};
if tx.send(mapped).is_err() {
break;
}
}
});
rx
}
#[cfg(feature = "vm")]
pub(crate) fn should_prepare_vm_pool(config: &Config) -> bool {
matches!(resolve_isolation(config, ""), Ok(IsolationLevel::MicroVm))
}
#[cfg(feature = "vm")]
pub(crate) fn initialize_default_vm_pool(
config: &Config,
) -> Result<Option<Arc<mimobox_vm::VmPool>>, SdkError> {
if !should_prepare_vm_pool(config) {
return Ok(None);
}
let microvm_config = config.to_microvm_config()?;
let sandbox_config = config.to_sandbox_config();
let pool_config = mimobox_vm::VmPoolConfig {
min_size: 1,
max_size: 4,
max_idle_duration: std::time::Duration::from_secs(60),
health_check_interval: None,
};
match mimobox_vm::VmPool::new_with_base(sandbox_config, microvm_config, pool_config) {
Ok(pool) => Ok(Some(Arc::new(pool))),
Err(error) => {
tracing::warn!("初始化 microVM 预热池失败,回退到冷启动路径: {error}");
Ok(None)
}
}
}
#[cfg(feature = "vm")]
pub(crate) fn map_http_proxy_error(error: mimobox_vm::HttpProxyError) -> SdkError {
use mimobox_vm::HttpProxyError;
match error {
HttpProxyError::DeniedHost(message) => SdkError::sandbox(
ErrorCode::HttpDeniedHost,
message,
Some("ensure target domain is in allowed_http_domains whitelist".to_string()),
),
HttpProxyError::DnsRebind(message) => SdkError::sandbox(
ErrorCode::HttpDeniedHost,
message,
Some("target domain resolved to private/loopback address, access denied".to_string()),
),
HttpProxyError::Timeout => SdkError::sandbox(
ErrorCode::HttpTimeout,
"HTTP request timed out",
Some("check target service reachability or increase timeout config".to_string()),
),
HttpProxyError::BodyTooLarge => SdkError::sandbox(
ErrorCode::HttpBodyTooLarge,
"HTTP body exceeds size limit",
Some("reduce request/response body size or transfer volume".to_string()),
),
HttpProxyError::ConnectFail(message) => SdkError::sandbox(
ErrorCode::HttpConnectFail,
message,
Some("check target service connectivity and port reachability".to_string()),
),
HttpProxyError::TlsFail(message) => SdkError::sandbox(
ErrorCode::HttpTlsFail,
message,
Some("check target certificate chain and TLS configuration".to_string()),
),
HttpProxyError::InvalidUrl(message) => SdkError::sandbox(
ErrorCode::HttpInvalidUrl,
message,
Some("only HTTPS URLs are supported, and direct IP access is not allowed".to_string()),
),
HttpProxyError::Internal(message) => SdkError::Config(message),
}
}
#[cfg(feature = "vm")]
pub(crate) fn map_microvm_error(error: mimobox_vm::MicrovmError) -> SdkError {
use mimobox_vm::MicrovmError;
let error_message = error.to_string();
match error {
MicrovmError::UnsupportedPlatform => SdkError::sandbox(
ErrorCode::UnsupportedPlatform,
"KVM microVM backend not supported on current platform",
Some("use microVM features only on Linux with vm feature enabled".to_string()),
),
MicrovmError::InvalidConfig(message) => SdkError::sandbox(
ErrorCode::InvalidConfig,
message,
Some("check microVM config, kernel/rootfs paths, and memory settings".to_string()),
),
MicrovmError::Lifecycle(error) => {
use mimobox_vm::LifecycleError;
let code = match error {
LifecycleError::Destroyed(_) | LifecycleError::Released(_) => {
ErrorCode::SandboxDestroyed
}
_ => ErrorCode::SandboxNotReady,
};
SdkError::sandbox(
code,
error.to_string(),
Some(
"ensure sandbox creation has completed and current state allows this operation"
.to_string(),
),
)
}
MicrovmError::HttpProxy(error) => map_http_proxy_error(error),
MicrovmError::Backend(message) => SdkError::sandbox(
ErrorCode::SandboxCreateFailed,
message,
Some("ensure KVM is available and guest runtime state is healthy".to_string()),
),
MicrovmError::GuestFile { kind, path } => {
let code = match kind {
GuestFileErrorKind::NotFound => ErrorCode::FileNotFound,
GuestFileErrorKind::PermissionDenied => ErrorCode::FilePermissionDenied,
_ => mimobox_core::ErrorCode::SandboxCreateFailed,
};
SdkError::sandbox(
code,
error_message,
Some(match kind {
GuestFileErrorKind::NotFound => format!(
"ensure target file exists and path is within allowed access scope: {path}"
),
GuestFileErrorKind::PermissionDenied => {
format!("check file permissions and sandbox mount policy: {path}")
}
_ => format!("check guest file system state: {path}"),
}),
)
}
MicrovmError::SnapshotFormat(message) => SdkError::sandbox(
ErrorCode::InvalidConfig,
message,
Some("ensure snapshot comes from a compatible mimobox microVM version".to_string()),
),
MicrovmError::Io(error) => SdkError::sandbox(
ErrorCode::SandboxCreateFailed,
error.to_string(),
Some(
"check snapshot file I/O and underlying virtualization resource state".to_string(),
),
),
}
}
#[cfg(feature = "vm")]
pub(crate) fn map_pool_error(error: mimobox_vm::PoolError) -> SdkError {
match error {
mimobox_vm::PoolError::InvalidConfig { min_size, max_size } => SdkError::Config(format!(
"invalid warm pool config: min_size={min_size}, max_size={max_size}"
)),
mimobox_vm::PoolError::StatePoisoned => {
SdkError::Config("warm pool internal state lock poisoned".to_string())
}
mimobox_vm::PoolError::Microvm(error) => map_microvm_error(error),
}
}
#[cfg(all(feature = "vm", target_os = "linux"))]
pub(crate) fn map_restore_pool_error(error: mimobox_vm::RestorePoolError) -> SdkError {
match error {
mimobox_vm::RestorePoolError::InvalidConfig { min_size, max_size } => SdkError::Config(
format!("invalid restore pool config: min_size={min_size}, max_size={max_size}"),
),
mimobox_vm::RestorePoolError::StatePoisoned => SdkError::sandbox(
ErrorCode::SandboxDestroyed,
"restore pool internal state lock poisoned",
Some("destroy current restore pool and recreate".to_string()),
),
mimobox_vm::RestorePoolError::Microvm(error) => map_microvm_error(error),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn parse_for_test(command: &str) -> Result<Vec<String>, SdkError> {
parse_command(command)
}
#[test]
fn invalid_shell_quoting_returns_sdk_error_instead_of_fallback_execution() {
let result = parse_for_test("'unterminated");
assert!(matches!(result, Err(SdkError::Config(_))));
}
#[test]
fn build_code_command_maps_languages_correctly() {
assert!(
build_code_command("bash", "echo 1")
.unwrap()
.starts_with("bash -c ")
);
assert!(
build_code_command("sh", "echo 1")
.unwrap()
.starts_with("sh -c ")
);
assert!(
build_code_command("shell", "echo 1")
.unwrap()
.starts_with("sh -c ")
);
assert!(
build_code_command("python", "print(1)")
.unwrap()
.starts_with("python3 -c ")
);
assert!(
build_code_command("python3", "print(1)")
.unwrap()
.starts_with("python3 -c ")
);
assert!(
build_code_command("py", "print(1)")
.unwrap()
.starts_with("python3 -c ")
);
assert!(
build_code_command("node", "console.log(1)")
.unwrap()
.starts_with("node -e ")
);
assert!(
build_code_command("js", "console.log(1)")
.unwrap()
.starts_with("node -e ")
);
assert!(
build_code_command("javascript", "console.log(1)")
.unwrap()
.starts_with("node -e ")
);
assert!(
build_code_command("nodejs", "console.log(1)")
.unwrap()
.starts_with("node -e ")
);
}
#[test]
fn build_code_command_rejects_unknown_language() {
let err = build_code_command("ruby", "puts 1").unwrap_err();
assert!(matches!(
err,
SdkError::Sandbox {
code: ErrorCode::InvalidConfig,
..
}
));
}
}