mod builder;
mod commands;
mod filesystem;
mod lifecycle;
mod options;
mod script;
use std::sync::{Arc, RwLock};
use a3s_box_core::{
ExecutionGeneration, ExecutionId, ExecutionIsolation, ExecutionSnapshot, ExecutionSnapshotId,
ExecutionState, ExecutionStatus,
};
pub use builder::SandboxBuilder;
pub use commands::{CommandResult, CommandRunOptions, Commands, SandboxCommand};
pub use filesystem::{Filesystem, FilesystemOptions, WriteInfo};
pub use lifecycle::{SandboxLogOptions, SandboxRestartOptions};
pub use options::{
SandboxCreateOptions, SandboxNetwork, TmpfsMount, VolumeMount, VolumeSource,
DEFAULT_SANDBOX_IMAGE, DEFAULT_SANDBOX_TIMEOUT_SECONDS,
};
pub use script::ScriptBuilder;
use crate::{A3sBoxClient, ClientError, Result};
#[derive(Debug, Clone, Copy)]
struct SandboxState {
generation: ExecutionGeneration,
state: ExecutionState,
closed: bool,
}
pub(crate) struct SandboxInner {
client: A3sBoxClient,
execution_id: ExecutionId,
isolation: ExecutionIsolation,
state: RwLock<SandboxState>,
}
impl SandboxInner {
fn state(&self) -> SandboxState {
*self
.state
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn update(&self, generation: ExecutionGeneration, state: ExecutionState) {
let mut current = self
.state
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
current.generation = generation;
current.state = state;
}
fn close(&self, generation: ExecutionGeneration) {
let mut current = self
.state
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
current.generation = generation;
current.state = ExecutionState::Stopped;
current.closed = true;
}
pub(crate) fn active_execution(&self) -> Result<(ExecutionId, ExecutionGeneration)> {
let state = self.state();
if state.closed || state.state != ExecutionState::Running {
return Err(ClientError::Validation(format!(
"sandbox {} is not running",
self.execution_id
)));
}
Ok((self.execution_id.clone(), state.generation))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SandboxInfo {
pub sandbox_id: String,
pub generation: u64,
pub state: ExecutionState,
pub isolation: ExecutionIsolation,
}
#[derive(Clone)]
pub struct Sandbox {
inner: Arc<SandboxInner>,
pub commands: Commands,
pub files: Filesystem,
}
impl std::fmt::Debug for Sandbox {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Sandbox")
.field("info", &self.info())
.finish_non_exhaustive()
}
}
impl Sandbox {
pub async fn create(image: impl Into<String>) -> Result<Self> {
Self::create_with_options(SandboxCreateOptions::new(image)).await
}
pub async fn create_with_options(options: SandboxCreateOptions) -> Result<Self> {
Self::create_with_client(A3sBoxClient::new(), options).await
}
pub async fn create_with_client(
client: A3sBoxClient,
options: SandboxCreateOptions,
) -> Result<Self> {
let isolation = options.isolation;
let (request, operation) = options.into_runtime_request(&client)?;
let lease = client.run_box(request, &operation).await?;
Ok(Self::from_known_state(
client,
lease.execution_id,
lease.generation,
ExecutionState::Running,
isolation,
))
}
pub async fn connect(sandbox_id: impl Into<String>) -> Result<Self> {
Self::connect_with_client(A3sBoxClient::new(), sandbox_id).await
}
pub async fn connect_with_client(
client: A3sBoxClient,
sandbox_id: impl Into<String>,
) -> Result<Self> {
let execution_id = ExecutionId::new(sandbox_id.into())
.map_err(|error| ClientError::Validation(error.to_string()))?;
let status = client.inspect_execution(&execution_id).await?;
Ok(Self::from_status(client, status))
}
fn from_status(client: A3sBoxClient, status: ExecutionStatus) -> Self {
Self::from_known_state(
client,
status.execution_id,
status.generation,
status.state,
status.plan.requested_isolation,
)
}
pub(crate) fn from_known_state(
client: A3sBoxClient,
execution_id: ExecutionId,
generation: ExecutionGeneration,
state: ExecutionState,
isolation: ExecutionIsolation,
) -> Self {
let inner = Arc::new(SandboxInner {
client,
execution_id,
isolation,
state: RwLock::new(SandboxState {
generation,
state,
closed: false,
}),
});
Self {
commands: Commands {
inner: Arc::clone(&inner),
},
files: Filesystem {
inner: Arc::clone(&inner),
},
inner,
}
}
pub fn id(&self) -> &str {
self.inner.execution_id.as_str()
}
pub fn info(&self) -> SandboxInfo {
let state = self.inner.state();
SandboxInfo {
sandbox_id: self.id().to_string(),
generation: state.generation.get(),
state: state.state,
isolation: self.inner.isolation,
}
}
pub fn isolation(&self) -> ExecutionIsolation {
self.inner.isolation
}
pub fn script(&self, source: impl AsRef<[u8]>) -> ScriptBuilder {
self.commands.script(source)
}
pub async fn create_filesystem_snapshot(
&self,
snapshot_id: ExecutionSnapshotId,
) -> Result<ExecutionSnapshot> {
let state = self.inner.state();
if state.closed
|| !matches!(
state.state,
ExecutionState::Running | ExecutionState::Paused
)
{
return Err(ClientError::Validation(format!(
"sandbox {} cannot be snapshotted while it is {:?}",
self.id(),
state.state
)));
}
let snapshot = self
.inner
.client
.create_execution_snapshot(&self.inner.execution_id, state.generation, &snapshot_id)
.await?;
self.inner.update(snapshot.lease.generation, snapshot.state);
Ok(snapshot)
}
pub async fn pause(&self, keep_memory: bool) -> Result<()> {
let (_, generation) = self.inner.active_execution()?;
let lease = self
.inner
.client
.pause_execution(&self.inner.execution_id, generation, keep_memory)
.await?;
self.inner.update(lease.generation, ExecutionState::Paused);
Ok(())
}
pub async fn resume(&self) -> Result<()> {
let state = self.inner.state();
if state.closed {
return Err(ClientError::Validation(format!(
"sandbox {} has been killed",
self.id()
)));
}
let lease = self
.inner
.client
.resume_execution(&self.inner.execution_id, state.generation)
.await?;
self.inner.update(lease.generation, ExecutionState::Running);
Ok(())
}
pub async fn is_running(&self) -> Result<bool> {
if self.inner.state().closed {
return Ok(false);
}
match self
.inner
.client
.inspect_execution(&self.inner.execution_id)
.await
{
Ok(status) => {
self.inner.update(status.generation, status.state);
Ok(status.state == ExecutionState::Running)
}
Err(ClientError::Execution(a3s_box_core::ExecutionManagerError::NotFound(_)))
| Err(ClientError::BoxNotFound(_)) => Ok(false),
Err(error) => Err(error),
}
}
pub async fn kill(&self) -> Result<()> {
self.stop().await?;
self.remove().await
}
}
#[cfg(test)]
mod tests;