1use std::future::{Future, IntoFuture};
5use std::marker::PhantomData;
6use std::pin::Pin;
7use std::time::Duration;
8
9use dvm_api::{
10 CommandLine, CommandNumber, CommandOutputEvent, CommandSignal, CommandState, NewCommand,
11 OutputStream, SandboxCommand,
12};
13use reqwest::Method;
14use tokio::time::Instant;
15
16use crate::error::{Error, Result};
17use crate::sandbox::SandboxCalls;
18
19const SIGKILL: i32 = 9;
20
21pub struct Commands {
23 calls: SandboxCalls,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct CommandResult {
30 pub number: CommandNumber,
31 pub stdout: String,
32 pub stderr: String,
33 pub exit_code: i32,
34 pub signal: Option<i32>,
35}
36
37pub type OutputCallback = Box<dyn FnMut(&str) + Send>;
39
40#[must_use = "a command starts once this is awaited"]
43pub struct CommandBuilder<Mode> {
44 calls: SandboxCalls,
45 new: NewCommand,
46 stdin: Option<Vec<u8>>,
47 waiting: Waiting,
48 mode: PhantomData<Mode>,
49}
50
51pub struct Foreground;
53pub struct Background;
55
56#[derive(Default)]
58struct Waiting {
59 timeout: Option<Duration>,
60 on_stdout: Option<OutputCallback>,
61 on_stderr: Option<OutputCallback>,
62}
63
64#[derive(Clone, Debug)]
66pub struct CommandHandle {
67 calls: SandboxCalls,
68 number: CommandNumber,
69 path: String,
70}
71
72#[must_use = "a command is waited for once this is awaited"]
75pub struct WaitForCommand {
76 handle: CommandHandle,
77 waiting: Waiting,
78}
79
80impl Commands {
81 pub(crate) fn new(calls: SandboxCalls) -> Self {
82 Self { calls }
83 }
84
85 pub fn run(&self, command: impl Into<CommandLine>) -> CommandBuilder<Foreground> {
90 CommandBuilder::new(self.calls.clone(), command.into())
91 }
92
93 pub fn spawn(&self, command: impl Into<CommandLine>) -> CommandBuilder<Background> {
96 CommandBuilder::new(self.calls.clone(), command.into())
97 }
98}
99
100impl<Mode> CommandBuilder<Mode> {
101 fn new(calls: SandboxCalls, command: CommandLine) -> Self {
102 Self {
103 calls,
104 new: NewCommand {
105 command,
106 cwd: None,
107 env: Default::default(),
108 stdin: false,
109 },
110 stdin: None,
111 waiting: Waiting::default(),
112 mode: PhantomData,
113 }
114 }
115
116 pub fn cwd(mut self, cwd: impl Into<String>) -> Self {
117 self.new.cwd = Some(cwd.into());
118 self
119 }
120
121 pub fn env(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
123 self.new.env.insert(name.into(), value.into());
124 self
125 }
126
127 pub fn stdin(mut self, data: impl Into<Vec<u8>>) -> Self {
129 self.new.stdin = true;
130 self.stdin = Some(data.into());
131 self
132 }
133
134 async fn start(self) -> Result<(CommandHandle, Waiting)> {
135 let calls = self.calls;
136 let path = format!("{}/commands", calls.path);
137 let started: SandboxCommand = calls
138 .while_live(calls.client.json(Method::POST, &path, &[], Some(&self.new)))
139 .await?;
140 let handle = CommandHandle::new(calls, started.number);
141 if let Some(data) = self.stdin {
142 handle.send(data, true).await?;
143 }
144 Ok((handle, self.waiting))
145 }
146}
147
148impl CommandBuilder<Foreground> {
149 pub fn timeout(mut self, timeout: Duration) -> Self {
152 self.waiting.timeout = Some(timeout);
153 self
154 }
155
156 pub fn on_stdout(mut self, callback: impl FnMut(&str) + Send + 'static) -> Self {
157 self.waiting.on_stdout = Some(Box::new(callback));
158 self
159 }
160
161 pub fn on_stderr(mut self, callback: impl FnMut(&str) + Send + 'static) -> Self {
162 self.waiting.on_stderr = Some(Box::new(callback));
163 self
164 }
165}
166
167impl CommandBuilder<Background> {
168 pub fn open_stdin(mut self) -> Self {
170 self.new.stdin = true;
171 self
172 }
173}
174
175impl IntoFuture for CommandBuilder<Foreground> {
176 type Output = Result<CommandResult>;
177 type IntoFuture = Pin<Box<dyn Future<Output = Result<CommandResult>> + Send>>;
178
179 fn into_future(self) -> Self::IntoFuture {
180 Box::pin(async move {
181 let (handle, waiting) = self.start().await?;
182 let mut kill_on_drop = KillOnDrop(Some(handle.clone()));
183 let result = wait_for(&handle, waiting).await;
184 kill_on_drop.0 = None;
185 result
186 })
187 }
188}
189
190impl IntoFuture for CommandBuilder<Background> {
191 type Output = Result<CommandHandle>;
192 type IntoFuture = Pin<Box<dyn Future<Output = Result<CommandHandle>> + Send>>;
193
194 fn into_future(self) -> Self::IntoFuture {
195 Box::pin(async move { Ok(self.start().await?.0) })
196 }
197}
198
199impl CommandHandle {
200 fn new(calls: SandboxCalls, number: CommandNumber) -> Self {
201 let path = format!("{}/commands/{number}", calls.path);
202 Self {
203 calls,
204 number,
205 path,
206 }
207 }
208
209 pub fn number(&self) -> CommandNumber {
210 self.number
211 }
212
213 pub fn wait(&self) -> WaitForCommand {
215 WaitForCommand {
216 handle: self.clone(),
217 waiting: Waiting::default(),
218 }
219 }
220
221 pub async fn send_stdin(&self, data: impl Into<Vec<u8>>) -> Result<()> {
223 self.send(data.into(), false).await
224 }
225
226 pub async fn close_stdin(&self) -> Result<()> {
228 self.send(Vec::new(), true).await
229 }
230
231 pub async fn kill(&self) -> Result<()> {
233 self.signal(SIGKILL).await
234 }
235
236 pub async fn signal(&self, signal: i32) -> Result<()> {
238 let path = format!("{}/signal", self.path);
239 let body = CommandSignal { signal };
240 let calls = &self.calls;
241 calls
242 .while_live(
243 calls
244 .client
245 .json::<()>(Method::POST, &path, &[], Some(&body)),
246 )
247 .await
248 }
249
250 pub async fn info(&self) -> Result<SandboxCommand> {
252 self.calls
253 .client
254 .json(Method::GET, &self.path, &[], None::<&()>)
255 .await
256 }
257
258 async fn send(&self, data: Vec<u8>, close: bool) -> Result<()> {
259 let path = format!("{}/stdin", self.path);
260 let closing = [("close", "true".to_owned())];
261 let query: &[(&str, String)] = if close { &closing } else { &[] };
262 let calls = &self.calls;
263 calls
264 .while_live(calls.client.bytes(Method::POST, &path, query, Some(data)))
265 .await
266 .map(drop)
267 }
268}
269
270impl WaitForCommand {
271 pub fn timeout(mut self, timeout: Duration) -> Self {
274 self.waiting.timeout = Some(timeout);
275 self
276 }
277
278 pub fn on_stdout(mut self, callback: impl FnMut(&str) + Send + 'static) -> Self {
280 self.waiting.on_stdout = Some(Box::new(callback));
281 self
282 }
283
284 pub fn on_stderr(mut self, callback: impl FnMut(&str) + Send + 'static) -> Self {
285 self.waiting.on_stderr = Some(Box::new(callback));
286 self
287 }
288}
289
290impl IntoFuture for WaitForCommand {
291 type Output = Result<CommandResult>;
292 type IntoFuture = Pin<Box<dyn Future<Output = Result<CommandResult>> + Send>>;
293
294 fn into_future(self) -> Self::IntoFuture {
295 Box::pin(async move { wait_for(&self.handle, self.waiting).await })
296 }
297}
298
299async fn wait_for(handle: &CommandHandle, waiting: Waiting) -> Result<CommandResult> {
301 let mut followed = FollowedOutput::new(handle, waiting.on_stdout, waiting.on_stderr);
302 match waiting.timeout {
303 None => followed.follow().await?,
304 Some(limit) => {
305 let within_limit = tokio::time::timeout(limit, followed.follow()).await;
306 match within_limit {
307 Ok(followed_to_its_end) => followed_to_its_end?,
308 Err(_) => {
309 handle.kill().await?;
310 followed.follow().await?;
311 }
312 }
313 }
314 }
315 followed.result().await
316}
317
318struct KillOnDrop(Option<CommandHandle>);
320
321impl Drop for KillOnDrop {
322 fn drop(&mut self) {
323 let (Some(handle), Ok(runtime)) = (self.0.take(), tokio::runtime::Handle::try_current())
324 else {
325 return;
326 };
327 runtime.spawn(async move {
328 let _ = handle.kill().await;
329 });
330 }
331}
332
333struct FollowedOutput<'a> {
336 handle: &'a CommandHandle,
337 path: String,
338 stdout: StreamOutput,
339 stderr: StreamOutput,
340 command: Option<SandboxCommand>,
341}
342
343impl<'a> FollowedOutput<'a> {
344 const MOST_FRUITLESS_FOLLOWS: u32 = 3;
348 const CUT_AFTER: Duration = Duration::from_secs(1);
349
350 fn new(
351 handle: &'a CommandHandle,
352 on_stdout: Option<OutputCallback>,
353 on_stderr: Option<OutputCallback>,
354 ) -> Self {
355 Self {
356 handle,
357 path: format!("{}/output", handle.path),
358 stdout: StreamOutput::new(on_stdout),
359 stderr: StreamOutput::new(on_stderr),
360 command: None,
361 }
362 }
363
364 async fn follow(&mut self) -> Result<()> {
366 let mut fruitless = 0;
367 while !self
368 .command
369 .as_ref()
370 .is_some_and(|command| !command.state.is_active())
371 {
372 let written = self.written();
373 let started = Instant::now();
374 let dropped = match self.follow_from_offsets().await {
375 Ok(()) => None,
376 Err(dropped @ Error::Network { .. }) => Some(dropped),
377 Err(error) => return Err(error),
378 };
379 let cut = started.elapsed() > Self::CUT_AFTER;
380 fruitless = if self.written() > written || cut {
381 0
382 } else {
383 fruitless + 1
384 };
385 if fruitless > Self::MOST_FRUITLESS_FOLLOWS {
386 let why = dropped
387 .map(|error| format!(": {error}"))
388 .unwrap_or_default();
389 return Err(Error::Unexpected(format!(
390 "{}'s stream ended before its command{why}",
391 self.path
392 )));
393 }
394 }
395 Ok(())
396 }
397
398 async fn follow_from_offsets(&mut self) -> Result<()> {
399 let query = [
400 ("follow", "true".to_owned()),
401 ("stdout_offset", self.stdout.bytes.len().to_string()),
402 ("stderr_offset", self.stderr.bytes.len().to_string()),
403 ];
404 let client = &self.handle.calls.client;
405 let mut events = client.events(&self.path, &query).await?;
406 while let Some(event) = events.next::<CommandOutputEvent>().await? {
407 match event {
408 CommandOutputEvent::Output {
409 stream,
410 offset,
411 chunk,
412 } => {
413 let output = match stream {
414 OutputStream::Stdout => &mut self.stdout,
415 OutputStream::Stderr => &mut self.stderr,
416 };
417 output.take(offset, chunk.bytes())?;
418 }
419 CommandOutputEvent::Command { command } => self.command = Some(command),
420 }
421 }
422 Ok(())
423 }
424
425 fn written(&self) -> usize {
426 self.stdout.bytes.len() + self.stderr.bytes.len()
427 }
428
429 async fn result(self) -> Result<CommandResult> {
430 let Some(command) = self.command else {
431 return Err(Error::Unexpected(format!("{} wasn't followed", self.path)));
432 };
433 match command.state {
434 CommandState::Exited { code, signal } => Ok(CommandResult {
435 number: command.number,
436 stdout: self.stdout.finish(),
437 stderr: self.stderr.finish(),
438 exit_code: code,
439 signal,
440 }),
441 CommandState::Refused { reason } => Err(Error::CommandRefused {
442 number: command.number,
443 reason,
444 }),
445 CommandState::Unfinished => {
446 let ended = self.handle.calls.ended_error().await?;
447 Err(ended.unwrap_or_else(|| {
448 Error::Unexpected(format!("command {} is unfinished", command.number))
449 }))
450 }
451 CommandState::Starting | CommandState::Running => Err(Error::Unexpected(format!(
452 "{} wasn't followed to its end",
453 self.path
454 ))),
455 }
456 }
457}
458
459struct StreamOutput {
462 bytes: Vec<u8>,
463 callback: Option<OutputCallback>,
464 undecoded: Vec<u8>,
465}
466
467impl StreamOutput {
468 fn new(callback: Option<OutputCallback>) -> Self {
469 Self {
470 bytes: Vec::new(),
471 callback,
472 undecoded: Vec::new(),
473 }
474 }
475
476 fn take(&mut self, offset: u64, chunk: &[u8]) -> Result<()> {
477 let length = self.bytes.len() as u64;
478 if offset > length {
479 return Err(Error::Unexpected(format!(
480 "output from byte {offset}, after only {length}"
481 )));
482 }
483 let seen = usize::try_from(length - offset).unwrap_or(usize::MAX);
484 let fresh = chunk.get(seen..).unwrap_or_default();
485 self.bytes.extend_from_slice(fresh);
486 if let Some(callback) = &mut self.callback {
487 self.undecoded.extend_from_slice(fresh);
488 let text = decode_whole_characters(&mut self.undecoded);
489 if !text.is_empty() {
490 callback(&text);
491 }
492 }
493 Ok(())
494 }
495
496 fn finish(mut self) -> String {
497 if let Some(callback) = &mut self.callback
498 && !self.undecoded.is_empty()
499 {
500 callback(&String::from_utf8_lossy(&self.undecoded));
501 }
502 String::from_utf8_lossy(&self.bytes).into_owned()
503 }
504}
505
506fn decode_whole_characters(bytes: &mut Vec<u8>) -> String {
510 let mut text = String::new();
511 loop {
512 match std::str::from_utf8(bytes) {
513 Ok(whole) => {
514 text.push_str(whole);
515 bytes.clear();
516 return text;
517 }
518 Err(error) => {
519 let valid = error.valid_up_to();
520 text.push_str(std::str::from_utf8(&bytes[..valid]).expect("valid up to here"));
521 match error.error_len() {
522 Some(invalid) => {
523 text.push(char::REPLACEMENT_CHARACTER);
524 bytes.drain(..valid + invalid);
525 }
526 None => {
527 bytes.drain(..valid);
528 return text;
529 }
530 }
531 }
532 }
533 }
534}
535
536#[cfg(test)]
537mod tests {
538 use super::*;
539
540 #[test]
541 fn a_character_split_across_chunks_waits_for_its_end() {
542 let mut pending = "é".as_bytes()[..1].to_vec();
543 assert_eq!(decode_whole_characters(&mut pending), "");
544 pending.extend_from_slice(&"é!".as_bytes()[1..]);
545 assert_eq!(decode_whole_characters(&mut pending), "é!");
546 assert!(pending.is_empty());
547 let mut invalid = vec![b'a', 0xff, b'b'];
548 assert_eq!(decode_whole_characters(&mut invalid), "a\u{fffd}b");
549 }
550
551 #[test]
552 fn output_seen_before_isnt_taken_again() {
553 let mut output = StreamOutput::new(None);
554 output.take(0, b"hel").unwrap();
555 output.take(1, b"ello").unwrap();
556 assert!(output.take(9, b"x").is_err());
557 assert_eq!(output.finish(), "hello");
558 }
559}