Skip to main content

dvm_run/
sandbox.rs

1//! A sandbox: a VM made from an image, which takes commands and files until
2//! it's stopped. Everything it ran is its execution, under its id.
3
4use std::future::{Future, IntoFuture};
5use std::pin::Pin;
6use std::time::Duration;
7
8use dvm_api::{EnvironmentVariable, Run, RunState, SandboxDraft, SandboxId};
9use reqwest::Method;
10use tokio::time::Instant;
11
12use crate::client::Client;
13use crate::commands::Commands;
14use crate::error::{Error, Result};
15use crate::files::Files;
16
17/// A stopped sandbox's runner reports its log within this, or went away.
18const LOG_WAIT: Duration = Duration::from_secs(60);
19
20/// A sandbox, from [`Client::create_sandbox`] or [`Client::connect_sandbox`].
21/// It runs until [`Sandbox::stop`]: dropping it doesn't stop it.
22#[derive(Clone, Debug)]
23pub struct Sandbox {
24    calls: SandboxCalls,
25    name: String,
26}
27
28/// What a sandbox is made of: [`Client::create_sandbox`], then what differs
29/// from 1 vCPU, 1024 MiB, seed 0, the image's variables, and a name after
30/// the image; awaited, the sandbox, once it's running.
31#[must_use = "a sandbox is made once this is awaited"]
32pub struct SandboxBuilder {
33    client: Client,
34    draft: SandboxDraft,
35}
36
37/// A sandbox's calls, which its commands and files make too: its client,
38/// its path, and what a refusal means once it ended.
39#[derive(Clone, Debug)]
40pub(crate) struct SandboxCalls {
41    pub(crate) client: Client,
42    pub(crate) id: SandboxId,
43    pub(crate) path: String,
44}
45
46impl SandboxBuilder {
47    pub(crate) fn new(client: Client, image: String) -> Self {
48        let draft = SandboxDraft {
49            image,
50            ..SandboxDraft::default()
51        };
52        Self { client, draft }
53    }
54
55    pub fn cpus(mut self, cpus: u32) -> Self {
56        self.draft.cpus = cpus;
57        self
58    }
59
60    pub fn memory_mib(mut self, memory_mib: u32) -> Self {
61        self.draft.memory_mib = memory_mib;
62        self
63    }
64
65    /// The seed of everything random in it, so a run is the same each time.
66    pub fn seed(mut self, seed: u64) -> Self {
67        self.draft.seed = seed;
68        self
69    }
70
71    /// A variable for its processes, beside the image's.
72    pub fn env(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
73        let (name, value) = (name.into(), value.into());
74        self.draft
75            .environment
76            .push(EnvironmentVariable { name, value });
77        self
78    }
79
80    pub fn name(mut self, name: impl Into<String>) -> Self {
81        self.draft.name = name.into();
82        self
83    }
84
85    async fn create(self) -> Result<Sandbox> {
86        let created: dvm_api::Sandbox = self
87            .client
88            .json(Method::POST, "/sandboxes", &[], Some(&self.draft))
89            .await?;
90        let sandbox = Sandbox::new(self.client, created.clone());
91        // Dropped while it starts, it's stopped: no one holds it.
92        let mut stop_on_drop = StopOnDrop(Some(sandbox.calls.clone()));
93        sandbox.calls.wait_until_running(created.run).await?;
94        stop_on_drop.0 = None;
95        Ok(sandbox)
96    }
97}
98
99impl IntoFuture for SandboxBuilder {
100    type Output = Result<Sandbox>;
101    type IntoFuture = Pin<Box<dyn Future<Output = Result<Sandbox>> + Send>>;
102
103    fn into_future(self) -> Self::IntoFuture {
104        Box::pin(self.create())
105    }
106}
107
108impl Sandbox {
109    pub(crate) fn new(client: Client, sandbox: dvm_api::Sandbox) -> Self {
110        Self {
111            calls: SandboxCalls {
112                path: format!("/sandboxes/{}", sandbox.id),
113                client,
114                id: sandbox.id,
115            },
116            name: sandbox.spec.name().to_owned(),
117        }
118    }
119
120    /// Its execution's id too.
121    pub fn id(&self) -> SandboxId {
122        self.calls.id
123    }
124
125    pub fn name(&self) -> &str {
126        &self.name
127    }
128
129    pub fn commands(&self) -> Commands {
130        Commands::new(self.calls.clone())
131    }
132
133    pub fn files(&self) -> Files {
134        Files::new(self.calls.clone())
135    }
136
137    /// Where it is: queued, running, paused, stopped, failed.
138    pub async fn state(&self) -> Result<RunState> {
139        self.calls.state().await
140    }
141
142    pub async fn pause(&self) -> Result<()> {
143        self.act("pause").await
144    }
145
146    pub async fn resume(&self) -> Result<()> {
147        self.act("resume").await
148    }
149
150    /// Stops it, which ends its execution, and returns once its log is in,
151    /// so that the execution replays. A sandbox that already ended stays as
152    /// it is.
153    pub async fn stop(&self) -> Result<()> {
154        let calls = &self.calls;
155        let stop = format!("{}/stop", calls.path);
156        let run = match calls
157            .client
158            .json(Method::POST, &stop, &[], None::<&()>)
159            .await
160        {
161            Ok(run) => run,
162            Err(refused @ Error::Conflict { .. }) => {
163                let run = calls.sandbox().await?.run;
164                if run.state.is_active() {
165                    return Err(refused);
166                }
167                run
168            }
169            Err(error) => return Err(error),
170        };
171        calls.wait_for_log(run).await
172    }
173
174    async fn act(&self, action: &str) -> Result<()> {
175        let calls = &self.calls;
176        let path = format!("{}/{action}", calls.path);
177        calls
178            .while_live(
179                calls
180                    .client
181                    .json::<Run>(Method::POST, &path, &[], None::<&()>),
182            )
183            .await
184            .map(drop)
185    }
186}
187
188impl SandboxCalls {
189    pub(crate) async fn state(&self) -> Result<RunState> {
190        Ok(self.sandbox().await?.run.state)
191    }
192
193    /// What `call` answers; if it's refused because the sandbox ended, that
194    /// it ended, and how.
195    pub(crate) async fn while_live<T>(&self, call: impl Future<Output = Result<T>>) -> Result<T> {
196        match call.await {
197            Err(refused @ Error::Conflict { .. }) => {
198                Err(self.ended_error().await?.unwrap_or(refused))
199            }
200            answer => answer,
201        }
202    }
203
204    /// What a call to it returns if it ended; `None` if it hasn't.
205    pub(crate) async fn ended_error(&self) -> Result<Option<Error>> {
206        let state = self.state().await?;
207        Ok((!state.is_active()).then(|| Error::ended(self.id, state)))
208    }
209
210    async fn sandbox(&self) -> Result<dvm_api::Sandbox> {
211        self.client
212            .json(Method::GET, &self.path, &[], None::<&()>)
213            .await
214    }
215
216    async fn wait_until_running(&self, mut run: Run) -> Result<()> {
217        loop {
218            match run.state {
219                RunState::Running => return Ok(()),
220                state if !state.is_active() => return Err(Error::ended(self.id, state)),
221                _ => run = self.newer_than(&run).await?,
222            }
223        }
224    }
225
226    /// A stopped sandbox's runner ends dvm, then reports its log's size as
227    /// the run's next version; one that never reached a runner has none.
228    async fn wait_for_log(&self, mut run: Run) -> Result<()> {
229        let deadline = Instant::now() + LOG_WAIT;
230        while run.state == RunState::Stopped
231            && run.runner.is_some()
232            && run.log_bytes.is_none()
233            && Instant::now() < deadline
234        {
235            run = self.newer_than(&run).await?;
236        }
237        Ok(())
238    }
239
240    async fn newer_than(&self, run: &Run) -> Result<Run> {
241        let query = [("newer_than", run.version.to_string())];
242        let sandbox: dvm_api::Sandbox = self
243            .client
244            .json(Method::GET, &self.path, &query, None::<&()>)
245            .await?;
246        Ok(sandbox.run)
247    }
248}
249
250/// Stops the sandbox, if it's still held when dropped: a creation given up
251/// on leaves nothing running.
252struct StopOnDrop(Option<SandboxCalls>);
253
254impl Drop for StopOnDrop {
255    fn drop(&mut self) {
256        let (Some(calls), Ok(runtime)) = (self.0.take(), tokio::runtime::Handle::try_current())
257        else {
258            return;
259        };
260        runtime.spawn(async move {
261            let stop = format!("{}/stop", calls.path);
262            let _ = calls
263                .client
264                .json::<Run>(Method::POST, &stop, &[], None::<&()>)
265                .await;
266        });
267    }
268}