1#[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
27pub struct Shell<'a, T: Utf8Encoding> {
31 client: &'a PipetteClient,
32 cwd: Utf8PathBuf<T>,
33 env: HashMap<String, String>,
34 chroot: Option<String>,
35}
36
37pub type WindowsShell<'a> = Shell<'a, Utf8WindowsEncoding>;
39
40pub 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 pub fn change_dir(&mut self, path: impl AsRef<Utf8Path<T>>) {
77 self.cwd = self.path(path);
78 }
79
80 pub fn chroot(&mut self, root: impl Into<String>) {
85 self.chroot = Some(root.into());
86 }
87
88 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 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 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
119pub 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 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 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 #[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 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 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 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 pub fn env_clear(mut self) -> Self {
201 self.env_changes.push(EnvChange::Clear);
202 self
203 }
204
205 pub fn ignore_status(mut self) -> Self {
209 self.ignore_status = true;
210 self
211 }
212
213 pub fn ignore_stdout(mut self) -> Self {
217 self.ignore_stdout = true;
218 self
219 }
220
221 pub fn ignore_stderr(mut self) -> Self {
225 self.ignore_stderr = true;
226 self
227 }
228
229 pub fn stdin(mut self, stdin: impl AsRef<[u8]>) -> Self {
231 self.stdin_contents = stdin.as_ref().to_vec();
232 self
233 }
234
235 pub async fn run(&self) -> anyhow::Result<()> {
241 self.read_output().await?;
242 Ok(())
243 }
244
245 pub async fn read(&self) -> anyhow::Result<String> {
251 self.read_stream(false).await
252 }
253
254 pub async fn read_stderr(&self) -> anyhow::Result<String> {
260 self.read_stream(true).await
261 }
262
263 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 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#[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}