Skip to main content

tracexec_core/
cmdbuilder.rs

1// MIT License
2
3// Copyright (c) 2018 Wez Furlong
4// Copyright (c) 2024 Levi Zim
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy
7// of this software and associated documentation files (the "Software"), to deal
8// in the Software without restriction, including without limitation the rights
9// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10// copies of the Software, and to permit persons to whom the Software is
11// furnished to do so, subject to the following conditions:
12
13// The above copyright notice and this permission notice shall be included in all
14// copies or substantial portions of the Software.
15
16// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22// SOFTWARE.
23
24//! Modified from https://github.com/wez/wezterm/tree/main/pty
25
26#![allow(unused)]
27
28use std::{
29  collections::BTreeMap,
30  env,
31  ffi::{
32    CString,
33    OsStr,
34    OsString,
35  },
36  os::unix::ffi::OsStringExt,
37  path::{
38    Path,
39    PathBuf,
40  },
41};
42
43use color_eyre::eyre::{
44  Context,
45  bail,
46};
47use nix::libc;
48use tracing::warn;
49
50/// `CommandBuilder` is used to prepare a command to be spawned into a pty.
51/// The interface is intentionally similar to that of `std::process::Command`.
52#[derive(Clone, Debug, PartialEq, Eq)]
53pub struct CommandBuilder {
54  args: Vec<OsString>,
55  env: Option<Vec<(OsString, OsString)>>,
56  cwd: Option<PathBuf>,
57  pub(crate) umask: Option<libc::mode_t>,
58  controlling_tty: bool,
59}
60
61impl CommandBuilder {
62  /// Create a new builder instance with argv[0] set to the specified
63  /// program.
64  pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
65    Self {
66      args: vec![program.as_ref().to_owned()],
67      env: None,
68      cwd: None,
69      umask: None,
70      controlling_tty: true,
71    }
72  }
73
74  /// Create a new builder instance from a pre-built argument vector
75  pub fn from_argv(args: Vec<OsString>) -> Self {
76    Self {
77      args,
78      env: None,
79      cwd: None,
80      umask: None,
81      controlling_tty: true,
82    }
83  }
84
85  /// Set whether we should set the pty as the controlling terminal.
86  /// The default is true, which is usually what you want, but you
87  /// may need to set this to false if you are crossing container
88  /// boundaries (eg: flatpak) to workaround issues like:
89  /// <https://github.com/flatpak/flatpak/issues/3697>
90  /// <https://github.com/flatpak/flatpak/issues/3285>
91  pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
92    self.controlling_tty = controlling_tty;
93  }
94
95  pub fn get_controlling_tty(&self) -> bool {
96    self.controlling_tty
97  }
98
99  /// Create a new builder instance that will run some idea of a default
100  /// program.  Such a builder will panic if `arg` is called on it.
101  pub fn new_default_prog() -> Self {
102    Self {
103      args: vec![],
104      env: None,
105      cwd: None,
106      umask: None,
107      controlling_tty: true,
108    }
109  }
110
111  /// Returns true if this builder was created via `new_default_prog`
112  pub fn is_default_prog(&self) -> bool {
113    self.args.is_empty()
114  }
115
116  /// Append an argument to the current command line.
117  /// Will panic if called on a builder created via `new_default_prog`.
118  pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
119    if self.is_default_prog() {
120      panic!("attempted to add args to a default_prog builder");
121    }
122    self.args.push(arg.as_ref().to_owned());
123  }
124
125  /// Append a sequence of arguments to the current command line
126  pub fn args<I, S>(&mut self, args: I)
127  where
128    I: IntoIterator<Item = S>,
129    S: AsRef<OsStr>,
130  {
131    for arg in args {
132      self.arg(arg);
133    }
134  }
135
136  pub fn get_argv(&self) -> &Vec<OsString> {
137    &self.args
138  }
139
140  pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
141    &mut self.args
142  }
143
144  pub fn env<K, V>(&mut self, key: K, value: V)
145  where
146    K: AsRef<OsStr>,
147    V: AsRef<OsStr>,
148  {
149    self
150      .env
151      .get_or_insert_with(Vec::new)
152      .push((key.as_ref().to_owned(), value.as_ref().to_owned()));
153  }
154
155  pub fn envs<I, K, V>(&mut self, envs: I)
156  where
157    I: IntoIterator<Item = (K, V)>,
158    K: Into<OsString>,
159    V: Into<OsString>,
160  {
161    self.env = Some(
162      envs
163        .into_iter()
164        .map(|(key, value)| (key.into(), value.into()))
165        .collect(),
166    );
167  }
168
169  pub fn get_env(&self) -> Option<&[(OsString, OsString)]> {
170    self.env.as_deref()
171  }
172
173  pub fn cwd<D>(&mut self, dir: D)
174  where
175    D: AsRef<Path>,
176  {
177    self.cwd = Some(dir.as_ref().to_owned());
178  }
179
180  pub fn clear_cwd(&mut self) {
181    self.cwd.take();
182  }
183
184  pub fn get_cwd(&self) -> Option<&Path> {
185    self.cwd.as_deref()
186  }
187}
188
189impl CommandBuilder {
190  pub fn umask(&mut self, mask: Option<libc::mode_t>) {
191    self.umask = mask;
192  }
193
194  fn resolve_path(&self) -> Option<OsString> {
195    match &self.env {
196      Some(env) => env
197        .iter()
198        .rev()
199        .find_map(|(key, value)| (key == OsStr::new("PATH")).then_some(value.clone())),
200      None => env::var_os("PATH"),
201    }
202  }
203
204  fn search_path(&self, exe: &OsStr, cwd: &Path) -> color_eyre::Result<PathBuf> {
205    use std::path::Path;
206
207    use nix::unistd::{
208      AccessFlags,
209      access,
210    };
211
212    let exe_path: &Path = exe.as_ref();
213    if exe_path.is_relative() {
214      let abs_path = cwd.join(exe_path);
215      if abs_path.exists() {
216        return Ok(abs_path);
217      }
218
219      if let Some(path) = self.resolve_path() {
220        for path in std::env::split_paths(&path) {
221          let candidate = path.join(exe);
222          if access(&candidate, AccessFlags::X_OK).is_ok() {
223            return Ok(candidate);
224          }
225        }
226      }
227      bail!(
228        "Unable to spawn {} because it doesn't exist on the filesystem \
229                and was not found in PATH",
230        exe_path.display()
231      );
232    } else {
233      if let Err(err) = access(exe_path, AccessFlags::X_OK) {
234        bail!(
235          "Unable to spawn {} because it doesn't exist on the filesystem \
236                    or is not executable ({err:#})",
237          exe_path.display()
238        );
239      }
240
241      Ok(PathBuf::from(exe))
242    }
243  }
244
245  /// Convert the CommandBuilder to a `Command` instance.
246  pub(crate) fn build(self) -> color_eyre::Result<Command> {
247    let cwd = env::current_dir()?;
248    let dir = if let Some(dir) = self.cwd.as_deref() {
249      dir.to_owned()
250    } else {
251      cwd
252    };
253    let resolved = self.search_path(&self.args[0], &dir)?;
254    tracing::trace!("resolved path to {:?}", resolved);
255
256    Ok(Command {
257      program: resolved,
258      args: self
259        .args
260        .into_iter()
261        .map(|a| CString::new(a.into_vec()))
262        .collect::<Result<_, _>>()?,
263      env: self
264        .env
265        .map(|env| {
266          env
267            .into_iter()
268            .map(|(key, value)| {
269              let mut bytes = key.into_vec();
270              bytes.push(b'=');
271              bytes.extend_from_slice(&value.into_vec());
272              CString::new(bytes)
273            })
274            .collect::<Result<Vec<_>, _>>()
275        })
276        .transpose()?,
277      cwd: dir,
278    })
279  }
280}
281
282pub struct Command {
283  pub program: PathBuf,
284  pub args: Vec<CString>,
285  pub env: Option<Vec<CString>>,
286  pub cwd: PathBuf,
287}
288
289#[cfg(test)]
290mod tests {
291  use std::{
292    ffi::{
293      OsStr,
294      OsString,
295    },
296    fs::{
297      self,
298      File,
299    },
300    os::unix::fs::PermissionsExt,
301    path::PathBuf,
302  };
303
304  use rusty_fork::rusty_fork_test;
305  use tempfile::TempDir;
306  use test_that::prelude::*;
307
308  use super::*;
309
310  fn make_executable(dir: &TempDir, name: &str) -> PathBuf {
311    let path = dir.path().join(name);
312    File::create(&path).unwrap();
313    let mut perms = fs::metadata(&path).unwrap().permissions();
314    perms.set_mode(0o755);
315    fs::set_permissions(&path, perms).unwrap();
316    path
317  }
318
319  #[test]
320  fn test_new_builder() {
321    let b = CommandBuilder::new("echo");
322    assert_eq!(b.get_argv(), &vec![OsString::from("echo")]);
323    assert_that!(b.get_cwd(), none());
324    assert!(b.get_controlling_tty());
325  }
326
327  #[test]
328  fn test_from_argv() {
329    let argv = vec![OsString::from("ls"), OsString::from("-l")];
330    let b = CommandBuilder::from_argv(argv.clone());
331    assert_eq!(b.get_argv(), &argv);
332  }
333
334  #[test]
335  fn test_default_prog() {
336    let b = CommandBuilder::new_default_prog();
337    assert!(b.is_default_prog());
338  }
339
340  #[test]
341  #[should_panic(expected = "attempted to add args to a default_prog builder")]
342  fn test_default_prog_panics_on_arg() {
343    let mut b = CommandBuilder::new_default_prog();
344    b.arg("ls");
345  }
346
347  #[test]
348  fn test_arg_and_args() {
349    let mut b = CommandBuilder::new("cmd");
350    b.arg("a");
351    b.args(["b", "c"]);
352    let argv: Vec<&OsStr> = b.get_argv().iter().map(|s| s.as_os_str()).collect();
353    assert_eq!(argv, ["cmd", "a", "b", "c"]);
354  }
355
356  #[test]
357  fn test_cwd_set_and_clear() {
358    let mut b = CommandBuilder::new("cmd");
359    let tmp = TempDir::new().unwrap();
360
361    b.cwd(tmp.path());
362    assert_eq!(b.get_cwd(), Some(tmp.path()));
363
364    b.clear_cwd();
365    assert_that!(b.get_cwd(), none());
366  }
367
368  #[test]
369  fn test_controlling_tty_flag() {
370    let mut b = CommandBuilder::new("cmd");
371    assert!(b.get_controlling_tty());
372
373    b.set_controlling_tty(false);
374    assert!(!b.get_controlling_tty());
375  }
376
377  rusty_fork_test! {
378    #[test]
379    fn test_search_path_finds_executable_in_path() {
380      let dir = TempDir::new().unwrap();
381      let exe = make_executable(&dir, "mycmd");
382
383      unsafe {
384        // SAFETY: we do this in a separate subprocess.
385        std::env::set_var("PATH", dir.path());
386      }
387
388      let b = CommandBuilder::new("mycmd");
389      let resolved = b.search_path(OsStr::new("mycmd"), dir.path()).unwrap();
390
391      assert_eq!(resolved, exe);
392    }
393  }
394
395  #[test]
396  fn test_search_path_relative_to_cwd() {
397    let dir = TempDir::new().unwrap();
398    let exe = make_executable(&dir, "tool");
399
400    let b = CommandBuilder::new("./tool");
401    let resolved = b.search_path(OsStr::new("./tool"), dir.path()).unwrap();
402
403    assert_eq!(resolved, exe);
404  }
405
406  #[test]
407  fn test_search_path_missing_binary_fails() {
408    let dir = TempDir::new().unwrap();
409    let b = CommandBuilder::new("does_not_exist");
410
411    let result = b.search_path(OsStr::new("does_not_exist"), dir.path());
412    assert_that!(result, err(anything()));
413  }
414
415  rusty_fork_test! {
416
417    #[test]
418    fn test_build_sets_program_args_and_cwd() {
419      let dir = TempDir::new().unwrap();
420      let exe = make_executable(&dir, "echo");
421
422      unsafe {
423        // SAFETY: we do this in a separate subprocess.
424        std::env::set_var("PATH", dir.path());
425      }
426
427      let mut b = CommandBuilder::new("echo");
428      b.arg("hello");
429      b.cwd(dir.path());
430
431      let cmd = b.build().unwrap();
432
433      assert_eq!(cmd.program, exe);
434      assert_eq!(cmd.cwd, dir.path());
435
436      let args: Vec<&str> = cmd.args.iter().map(|c| c.to_str().unwrap()).collect();
437
438      assert_eq!(args, ["echo", "hello"]);
439      assert_that!(cmd.env, none());
440      dir.close().unwrap()
441    }
442
443    #[test]
444    fn test_build_sets_explicit_env() {
445      let dir = TempDir::new().unwrap();
446      let exe = make_executable(&dir, "cmd");
447
448      let mut b = CommandBuilder::new("cmd");
449      b.envs([
450        (OsString::from("PATH"), dir.path().as_os_str().to_owned()),
451        (OsString::from("TRACEXEC_TEST"), OsString::from("value")),
452      ]);
453
454      let cmd = b.build().unwrap();
455
456      assert_eq!(cmd.program, exe);
457      let env: Vec<String> = cmd
458        .env
459        .as_ref()
460        .unwrap()
461        .iter()
462        .map(|c| c.to_str().unwrap().to_owned())
463        .collect();
464      assert_eq!(
465        env,
466        vec![
467          "PATH=".to_string() + dir.path().to_str().unwrap(),
468          "TRACEXEC_TEST=value".to_string(),
469        ]
470      );
471      dir.close().unwrap()
472    }
473
474    #[test]
475    fn test_build_uses_current_dir_when_cwd_not_set() {
476      let dir = TempDir::new().unwrap();
477      let exe = make_executable(&dir, "cmd");
478
479      // SAFETY: we do this in a separate subprocess.
480      unsafe { std::env::set_var("PATH", dir.path()); }
481
482      let b = CommandBuilder::new("cmd");
483      let cmd = b.build().unwrap();
484
485      assert_eq!(cmd.program, exe);
486      assert_eq!(cmd.cwd, std::env::current_dir().unwrap());
487      dir.close().unwrap()
488    }
489  }
490}