Skip to main content

dvm_run/
commands.rs

1//! Commands in a sandbox: run one and get its output, or spawn one and hold
2//! a handle to it.
3
4use 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
21/// A sandbox's commands: [`Sandbox::commands`](crate::Sandbox::commands).
22pub struct Commands {
23    calls: SandboxCalls,
24}
25
26/// A command that exited: its output, and its exit code. A signal's kill is
27/// code 128 and the signal, as a shell says, with the signal given.
28#[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
37/// Called with each piece of a stream's output, as text, as it's written.
38pub type OutputCallback = Box<dyn FnMut(&str) + Send>;
39
40/// A command to run: [`Commands::run`] or [`Commands::spawn`], then how it
41/// starts; awaited, its result or a handle to it.
42#[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
51/// Run to its exit, awaited: [`Commands::run`].
52pub struct Foreground;
53/// Started, and a handle to it: [`Commands::spawn`].
54pub struct Background;
55
56/// How a command is waited for.
57#[derive(Default)]
58struct Waiting {
59    timeout: Option<Duration>,
60    on_stdout: Option<OutputCallback>,
61    on_stderr: Option<OutputCallback>,
62}
63
64/// A command spawned in the background.
65#[derive(Clone, Debug)]
66pub struct CommandHandle {
67    calls: SandboxCalls,
68    number: CommandNumber,
69    path: String,
70}
71
72/// Waiting for a command, from [`CommandHandle::wait`]; awaited, its result
73/// once it exits. Dropped, it stops the waiting, not the command.
74#[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    /// Runs `command` to its exit: a script (`&str`), which the sandbox's
86    /// shell runs (`/bin/bash -lc`, or `/bin/sh -c` in an image without
87    /// bash), or an argv (`["ls", "-l"]`), which runs as it is. Dropped
88    /// before it exits, it's killed.
89    pub fn run(&self, command: impl Into<CommandLine>) -> CommandBuilder<Foreground> {
90        CommandBuilder::new(self.calls.clone(), command.into())
91    }
92
93    /// Starts `command`, as [`Commands::run`] takes it, and gives a handle
94    /// to it at once.
95    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    /// A variable, beside the sandbox's.
122    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    /// Sent to its stdin, which is closed after; else it reads `/dev/null`.
128    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    /// Once it passes, the command is killed (SIGKILL), and its result says
150    /// so.
151    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    /// Keeps its stdin open, for [`CommandHandle::send_stdin`].
169    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    /// Its result once it exits, with all it wrote.
214    pub fn wait(&self) -> WaitForCommand {
215        WaitForCommand {
216            handle: self.clone(),
217            waiting: Waiting::default(),
218        }
219    }
220
221    /// Sends `data` to its stdin, which it was spawned with open.
222    pub async fn send_stdin(&self, data: impl Into<Vec<u8>>) -> Result<()> {
223        self.send(data.into(), false).await
224    }
225
226    /// Closes its stdin: it reads no more than it was sent.
227    pub async fn close_stdin(&self) -> Result<()> {
228        self.send(Vec::new(), true).await
229    }
230
231    /// Kills its process (SIGKILL; not what that started).
232    pub async fn kill(&self) -> Result<()> {
233        self.signal(SIGKILL).await
234    }
235
236    /// Sends `signal` to its process.
237    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    /// The command as it is now.
251    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    /// Once it passes, the command is killed (SIGKILL), and its result says
272    /// so.
273    pub fn timeout(mut self, timeout: Duration) -> Self {
274        self.waiting.timeout = Some(timeout);
275        self
276    }
277
278    /// Its output from its start, as it's written.
279    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
299/// The command's result once it exits; past its timeout, killed first.
300async 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
318/// Kills the command if a foreground run is dropped before it exits.
319struct 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
333/// A command's output, followed from its start to its end over server-sent
334/// events; a stream that drops is followed again from where it got to.
335struct 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    /// Streams in a row that end before the command, at once and with
345    /// nothing new: one that went on a while was cut, by the network or a
346    /// proxy.
347    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    /// Returns once the command ended.
365    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
459/// One stream's bytes so far, and its text to a callback as it comes: a
460/// character split across chunks waits for its end.
461struct 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
506/// The text of `bytes`' whole characters, which leave it; a character not
507/// yet whole stays. Bytes that are no character are U+FFFD, as in a lossy
508/// decoding of the whole.
509fn 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}