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