use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::time::Duration;
use dvm_api::{EnvironmentVariable, Run, RunState, SandboxDraft, SandboxId};
use reqwest::Method;
use tokio::time::Instant;
use crate::client::Client;
use crate::commands::Commands;
use crate::error::{Error, Result};
use crate::files::Files;
const LOG_WAIT: Duration = Duration::from_secs(60);
#[derive(Clone, Debug)]
pub struct Sandbox {
calls: SandboxCalls,
name: String,
}
#[must_use = "a sandbox is made once this is awaited"]
pub struct SandboxBuilder {
client: Client,
draft: SandboxDraft,
}
#[derive(Clone, Debug)]
pub(crate) struct SandboxCalls {
pub(crate) client: Client,
pub(crate) id: SandboxId,
pub(crate) path: String,
}
impl SandboxBuilder {
pub(crate) fn new(client: Client, image: String) -> Self {
let draft = SandboxDraft {
image,
..SandboxDraft::default()
};
Self { client, draft }
}
pub fn cpus(mut self, cpus: u32) -> Self {
self.draft.cpus = cpus;
self
}
pub fn memory_mib(mut self, memory_mib: u32) -> Self {
self.draft.memory_mib = memory_mib;
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.draft.seed = seed;
self
}
pub fn env(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
let (name, value) = (name.into(), value.into());
self.draft
.environment
.push(EnvironmentVariable { name, value });
self
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.draft.name = name.into();
self
}
async fn create(self) -> Result<Sandbox> {
let created: dvm_api::Sandbox = self
.client
.json(Method::POST, "/sandboxes", &[], Some(&self.draft))
.await?;
let sandbox = Sandbox::new(self.client, created.clone());
let mut stop_on_drop = StopOnDrop(Some(sandbox.calls.clone()));
sandbox.calls.wait_until_running(created.run).await?;
stop_on_drop.0 = None;
Ok(sandbox)
}
}
impl IntoFuture for SandboxBuilder {
type Output = Result<Sandbox>;
type IntoFuture = Pin<Box<dyn Future<Output = Result<Sandbox>> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.create())
}
}
impl Sandbox {
pub(crate) fn new(client: Client, sandbox: dvm_api::Sandbox) -> Self {
Self {
calls: SandboxCalls {
path: format!("/sandboxes/{}", sandbox.id),
client,
id: sandbox.id,
},
name: sandbox.spec.name().to_owned(),
}
}
pub fn id(&self) -> SandboxId {
self.calls.id
}
pub fn name(&self) -> &str {
&self.name
}
pub fn commands(&self) -> Commands {
Commands::new(self.calls.clone())
}
pub fn files(&self) -> Files {
Files::new(self.calls.clone())
}
pub async fn state(&self) -> Result<RunState> {
self.calls.state().await
}
pub async fn pause(&self) -> Result<()> {
self.act("pause").await
}
pub async fn resume(&self) -> Result<()> {
self.act("resume").await
}
pub async fn stop(&self) -> Result<()> {
let calls = &self.calls;
let stop = format!("{}/stop", calls.path);
let run = match calls
.client
.json(Method::POST, &stop, &[], None::<&()>)
.await
{
Ok(run) => run,
Err(refused @ Error::Conflict { .. }) => {
let run = calls.sandbox().await?.run;
if run.state.is_active() {
return Err(refused);
}
run
}
Err(error) => return Err(error),
};
calls.wait_for_log(run).await
}
async fn act(&self, action: &str) -> Result<()> {
let calls = &self.calls;
let path = format!("{}/{action}", calls.path);
calls
.while_live(
calls
.client
.json::<Run>(Method::POST, &path, &[], None::<&()>),
)
.await
.map(drop)
}
}
impl SandboxCalls {
pub(crate) async fn state(&self) -> Result<RunState> {
Ok(self.sandbox().await?.run.state)
}
pub(crate) async fn while_live<T>(&self, call: impl Future<Output = Result<T>>) -> Result<T> {
match call.await {
Err(refused @ Error::Conflict { .. }) => {
Err(self.ended_error().await?.unwrap_or(refused))
}
answer => answer,
}
}
pub(crate) async fn ended_error(&self) -> Result<Option<Error>> {
let state = self.state().await?;
Ok((!state.is_active()).then(|| Error::ended(self.id, state)))
}
async fn sandbox(&self) -> Result<dvm_api::Sandbox> {
self.client
.json(Method::GET, &self.path, &[], None::<&()>)
.await
}
async fn wait_until_running(&self, mut run: Run) -> Result<()> {
loop {
match run.state {
RunState::Running => return Ok(()),
state if !state.is_active() => return Err(Error::ended(self.id, state)),
_ => run = self.newer_than(&run).await?,
}
}
}
async fn wait_for_log(&self, mut run: Run) -> Result<()> {
let deadline = Instant::now() + LOG_WAIT;
while run.state == RunState::Stopped
&& run.runner.is_some()
&& run.log_bytes.is_none()
&& Instant::now() < deadline
{
run = self.newer_than(&run).await?;
}
Ok(())
}
async fn newer_than(&self, run: &Run) -> Result<Run> {
let query = [("newer_than", run.version.to_string())];
let sandbox: dvm_api::Sandbox = self
.client
.json(Method::GET, &self.path, &query, None::<&()>)
.await?;
Ok(sandbox.run)
}
}
struct StopOnDrop(Option<SandboxCalls>);
impl Drop for StopOnDrop {
fn drop(&mut self) {
let (Some(calls), Ok(runtime)) = (self.0.take(), tokio::runtime::Handle::try_current())
else {
return;
};
runtime.spawn(async move {
let stop = format!("{}/stop", calls.path);
let _ = calls
.client
.json::<Run>(Method::POST, &stop, &[], None::<&()>)
.await;
});
}
}