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