pipette_client/
shell.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Provides a shell abstraction to interact with the guest, similar to
5//! `xshell::Shell`.
6
7// This is a bit of a hack, since `__cmd` is an internal detail of xshell. But
8// it does exactly what we need, so let's use it for now. If the internal
9// details change, we can fork it.
10#[doc(hidden)]
11pub use xshell_macros::__cmd;
12
13use crate::PipetteClient;
14use crate::process::Command;
15use crate::process::Output;
16use crate::process::Stdio;
17use anyhow::Context;
18use futures::AsyncWriteExt;
19use futures_concurrency::future::Join;
20use std::collections::HashMap;
21use typed_path::Utf8Encoding;
22use typed_path::Utf8Path;
23use typed_path::Utf8PathBuf;
24use typed_path::Utf8UnixEncoding;
25use typed_path::Utf8WindowsEncoding;
26
27/// A stateful shell abstraction for interacting with the guest.
28///
29/// This is modeled after `xshell::Shell`.
30pub struct Shell<'a, T: Utf8Encoding> {
31    client: &'a PipetteClient,
32    cwd: Utf8PathBuf<T>,
33    env: HashMap<String, String>,
34    chroot: Option<String>,
35}
36
37/// A shell for a Windows guest.
38pub type WindowsShell<'a> = Shell<'a, Utf8WindowsEncoding>;
39
40/// A shell for a Linux guest.
41pub type UnixShell<'a> = Shell<'a, Utf8UnixEncoding>;
42
43impl<'a> UnixShell<'a> {
44    pub(crate) fn new(client: &'a PipetteClient) -> Self {
45        Self {
46            client,
47            cwd: Utf8PathBuf::from("/"),
48            env: HashMap::new(),
49            chroot: None,
50        }
51    }
52}
53
54impl<'a> WindowsShell<'a> {
55    pub(crate) fn new(client: &'a PipetteClient) -> Self {
56        Self {
57            client,
58            cwd: Utf8PathBuf::from("C:/"),
59            env: HashMap::new(),
60            chroot: None,
61        }
62    }
63}
64
65impl<T> Shell<'_, T>
66where
67    T: Utf8Encoding,
68{
69    fn path(&self, path: impl AsRef<Utf8Path<T>>) -> Utf8PathBuf<T> {
70        self.cwd.join(path)
71    }
72
73    /// Change the effective working directory of the shell.
74    ///
75    /// Other paths will be resolved relative to this directory.
76    pub fn change_dir(&mut self, path: impl AsRef<Utf8Path<T>>) {
77        self.cwd = self.path(path);
78    }
79
80    /// Set the chroot directory for commands run by this shell.
81    ///
82    /// Each spawned process will chroot into this directory before exec
83    /// (Linux only). The shell itself is unaffected.
84    pub fn chroot(&mut self, root: impl Into<String>) {
85        self.chroot = Some(root.into());
86    }
87
88    /// Reads a file from the guest into a string.
89    pub async fn read_file(&self, path: impl AsRef<Utf8Path<T>>) -> anyhow::Result<String> {
90        let path = self.path(path);
91        let v = self.client.read_file(path.as_str()).await?;
92        String::from_utf8(v).with_context(|| format!("file '{}' is not valid utf-8", path.as_str()))
93    }
94
95    /// Reads a file from the guest into a raw byte vector.
96    pub async fn read_file_raw(&self, path: impl AsRef<Utf8Path<T>>) -> anyhow::Result<Vec<u8>> {
97        let path = self.path(path);
98        let v = self.client.read_file(path.as_str()).await?;
99        Ok(v)
100    }
101
102    /// Creates a builder to execute a command inside the guest.
103    ///
104    /// Consider using the [`cmd!`](crate::cmd!) macro.
105    pub fn cmd(&self, program: impl AsRef<Utf8Path<T>>) -> Cmd<'_, T> {
106        Cmd {
107            shell: self,
108            prog: program.as_ref().to_owned(),
109            args: Vec::new(),
110            env_changes: Vec::new(),
111            ignore_status: false,
112            stdin_contents: Vec::new(),
113            ignore_stdout: false,
114            ignore_stderr: false,
115        }
116    }
117}
118
119/// A command builder.
120pub struct Cmd<'a, T: Utf8Encoding> {
121    shell: &'a Shell<'a, T>,
122    prog: Utf8PathBuf<T>,
123    args: Vec<String>,
124    env_changes: Vec<EnvChange>,
125    ignore_status: bool,
126    stdin_contents: Vec<u8>,
127    ignore_stdout: bool,
128    ignore_stderr: bool,
129}
130
131enum EnvChange {
132    Set(String, String),
133    Remove(String),
134    Clear,
135}
136
137impl<'a, T: Utf8Encoding> Cmd<'a, T> {
138    /// Adds an argument to the command.
139    pub fn arg<P: AsRef<str>>(mut self, arg: P) -> Self {
140        self.args.push(arg.as_ref().to_owned());
141        self
142    }
143
144    /// Adds multiple arguments to the command.
145    pub fn args<I>(mut self, args: I) -> Self
146    where
147        I: IntoIterator,
148        I::Item: AsRef<str>,
149    {
150        for it in args.into_iter() {
151            self = self.arg(it.as_ref());
152        }
153        self
154    }
155
156    // Used by xshell_macros::__cmd
157    #[doc(hidden)]
158    pub fn __extend_arg(mut self, arg_fragment: impl AsRef<str>) -> Self {
159        match self.args.last_mut() {
160            Some(last_arg) => last_arg.push_str(arg_fragment.as_ref()),
161            None => {
162                let mut prog = std::mem::take(&mut self.prog).into_string();
163                prog.push_str(arg_fragment.as_ref());
164                self.prog = prog.into();
165            }
166        }
167        self
168    }
169
170    /// Sets an environment variable for the command.
171    pub fn env(mut self, key: impl AsRef<str>, val: impl AsRef<str>) -> Self {
172        self.env_changes.push(EnvChange::Set(
173            key.as_ref().to_owned(),
174            val.as_ref().to_owned(),
175        ));
176        self
177    }
178
179    /// Sets multiple environment variables for the command.
180    pub fn envs<I, K, V>(mut self, vars: I) -> Self
181    where
182        I: IntoIterator<Item = (K, V)>,
183        K: AsRef<str>,
184        V: AsRef<str>,
185    {
186        for (k, v) in vars.into_iter() {
187            self = self.env(k.as_ref(), v.as_ref());
188        }
189        self
190    }
191
192    /// Removes an environment variable for the command.
193    pub fn env_remove(mut self, key: impl AsRef<str>) -> Self {
194        self.env_changes
195            .push(EnvChange::Remove(key.as_ref().to_owned()));
196        self
197    }
198
199    /// Clears the environment for the command.
200    pub fn env_clear(mut self) -> Self {
201        self.env_changes.push(EnvChange::Clear);
202        self
203    }
204
205    /// Ignores the status of the command.
206    ///
207    /// By default, the command will fail if the exit code is non-zero.
208    pub fn ignore_status(mut self) -> Self {
209        self.ignore_status = true;
210        self
211    }
212
213    /// Ignores the stdout of the command.
214    ///
215    /// By default, the command's stdout will be captured or printed to stdout.
216    pub fn ignore_stdout(mut self) -> Self {
217        self.ignore_stdout = true;
218        self
219    }
220
221    /// Ignores the stderr of the command.
222    ///
223    /// By default, the command's stderr will be captured or printed to stderr.
224    pub fn ignore_stderr(mut self) -> Self {
225        self.ignore_stderr = true;
226        self
227    }
228
229    /// Sets contents to be written to the command's stdin.
230    pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Self {
231        self.stdin_contents = stdin.as_ref().to_vec();
232        self
233    }
234
235    /// Runs the command and waits for it to complete.
236    ///
237    /// By default, this will fail if the command's exit code is non-zero.
238    ///
239    /// By default, the command's stdout and stderr will be captured and traced.
240    pub async fn run(&self) -> anyhow::Result<()> {
241        self.read_output().await?;
242        Ok(())
243    }
244
245    /// Runs the command and waits for it to complete, returning the stdout.
246    ///
247    /// By default, this will fail if the command's exit code is non-zero.
248    ///
249    /// By default, the command's stderr will be captured and traced.
250    pub async fn read(&self) -> anyhow::Result<String> {
251        self.read_stream(false).await
252    }
253
254    /// Runs the command and waits for it to complete, returning the stderr.
255    ///
256    /// By default, this will fail if the command's exit code is non-zero.
257    ///
258    /// By default, the command's stdout will be captured and traced.
259    pub async fn read_stderr(&self) -> anyhow::Result<String> {
260        self.read_stream(true).await
261    }
262
263    /// Runs the command and waits for it to complete, returning the stdout and
264    /// stderr.
265    ///
266    /// By default, this will fail if the command's exit code is non-zero.
267    pub async fn output(&self) -> anyhow::Result<Output> {
268        self.read_output().await
269    }
270
271    fn command(&self) -> Command<'a> {
272        let mut command = self.shell.client.command(&self.prog);
273        command.args(&self.args);
274        command.current_dir(&self.shell.cwd);
275        if let Some(ref root) = self.shell.chroot {
276            command.chroot(root);
277        }
278        for (name, value) in &self.shell.env {
279            command.env(name, value);
280        }
281        for change in &self.env_changes {
282            match change {
283                EnvChange::Set(name, value) => {
284                    command.env(name, value);
285                }
286                EnvChange::Remove(name) => {
287                    command.env_remove(name);
288                }
289                EnvChange::Clear => {
290                    command.env_clear();
291                }
292            }
293        }
294        if self.ignore_stdout {
295            command.stdout(Stdio::null());
296        }
297        if self.ignore_stderr {
298            command.stderr(Stdio::null());
299        }
300        command
301    }
302
303    async fn read_stream(&self, read_stderr: bool) -> anyhow::Result<String> {
304        let output = self.read_output().await?;
305        let stream = if read_stderr {
306            output.stderr
307        } else {
308            output.stdout
309        };
310        let mut stream = String::from_utf8(stream).context("stream is not utf-8")?;
311        if stream.ends_with('\n') {
312            stream.pop();
313        }
314        if stream.ends_with('\r') {
315            stream.pop();
316        }
317        Ok(stream)
318    }
319
320    async fn read_output(&self) -> anyhow::Result<Output> {
321        let mut command = self.command();
322        if !self.ignore_stdout {
323            command.stdout(Stdio::piped());
324        }
325        if !self.ignore_stderr {
326            command.stderr(Stdio::piped());
327        }
328        if !self.stdin_contents.is_empty() {
329            command.stdin(Stdio::piped());
330        }
331        let mut child = command.spawn().await.context("failed to spawn child")?;
332
333        // put in task
334        let stdin = child.stdin.take();
335        let copy_stdin = async move {
336            if let Some(mut stdin) = stdin {
337                stdin.write_all(&self.stdin_contents).await?;
338            }
339            anyhow::Ok(())
340        };
341
342        let wait = child.wait_with_output();
343
344        let (copy_r, wait_r) = (copy_stdin, wait).join().await;
345        let output = wait_r.context("failed to wait for child")?;
346        copy_r.context("failed to write stdin")?;
347
348        let out = String::from_utf8_lossy(&output.stdout);
349        tracing::info!(?out, "command stdout");
350
351        let err = String::from_utf8_lossy(&output.stderr);
352        tracing::info!(?err, "command stderr");
353
354        if !self.ignore_status && !output.status.success() {
355            anyhow::bail!("command failed: {}", output.status);
356        }
357
358        Ok(output)
359    }
360}
361
362/// Constructs a [`Cmd`] from the given string, with interpolation.
363///
364/// # Example
365///
366/// ```no_run
367/// # use pipette_client::{cmd, shell::UnixShell};
368/// async fn example(sh: &mut UnixShell<'_>) {
369///     let args = ["hello", "world"];
370///     assert_eq!(cmd!(sh, "echo {args...}").read().await.unwrap(), "hello world");
371/// }
372#[macro_export]
373macro_rules! cmd {
374    ($sh:expr, $cmd:literal) => {{
375        let f = |prog| $sh.cmd(prog);
376        let cmd: $crate::shell::Cmd<'_, _> = $crate::shell::__cmd!(f $cmd);
377        cmd
378    }};
379}