1use 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
17const LOG_WAIT: Duration = Duration::from_secs(60);
19
20#[derive(Clone, Debug)]
23pub struct Sandbox {
24 calls: SandboxCalls,
25 name: String,
26}
27
28#[must_use = "a sandbox is made once this is awaited"]
32pub struct SandboxBuilder {
33 client: Client,
34 draft: SandboxDraft,
35}
36
37#[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 pub fn seed(mut self, seed: u64) -> Self {
67 self.draft.seed = seed;
68 self
69 }
70
71 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 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 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 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 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 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 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 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
250struct 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}