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 color_eyre::eyre::{Context, bail};
29use nix::libc;
30use std::collections::BTreeMap;
31use std::env;
32use std::ffi::{CString, OsStr, OsString};
33use std::os::unix::ffi::OsStringExt;
34use std::path::{Path, PathBuf};
35use tracing::warn;
36
37fn get_shell() -> String {
38  use nix::unistd::{AccessFlags, access};
39  use std::ffi::CStr;
40  use std::path::Path;
41  use std::str;
42
43  let ent = unsafe { libc::getpwuid(libc::getuid()) };
44  if !ent.is_null() {
45    let shell = unsafe { CStr::from_ptr((*ent).pw_shell) };
46    match shell.to_str().map(str::to_owned) {
47      Err(err) => {
48        warn!(
49          "passwd database shell could not be \
50                     represented as utf-8: {err:#}, \
51                     falling back to /bin/sh"
52        );
53      }
54      Ok(shell) => {
55        if let Err(err) = access(Path::new(&shell), AccessFlags::X_OK) {
56          warn!(
57            "passwd database shell={shell:?} which is \
58                         not executable ({err:#}), falling back to /bin/sh"
59          );
60        } else {
61          return shell;
62        }
63      }
64    }
65  }
66  "/bin/sh".into()
67}
68
69/// `CommandBuilder` is used to prepare a command to be spawned into a pty.
70/// The interface is intentionally similar to that of `std::process::Command`.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct CommandBuilder {
73  args: Vec<OsString>,
74  cwd: Option<PathBuf>,
75  pub(crate) umask: Option<libc::mode_t>,
76  controlling_tty: bool,
77}
78
79impl CommandBuilder {
80  /// Create a new builder instance with argv[0] set to the specified
81  /// program.
82  pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
83    Self {
84      args: vec![program.as_ref().to_owned()],
85      cwd: None,
86      umask: None,
87      controlling_tty: true,
88    }
89  }
90
91  /// Create a new builder instance from a pre-built argument vector
92  pub fn from_argv(args: Vec<OsString>) -> Self {
93    Self {
94      args,
95      cwd: None,
96      umask: None,
97      controlling_tty: true,
98    }
99  }
100
101  /// Set whether we should set the pty as the controlling terminal.
102  /// The default is true, which is usually what you want, but you
103  /// may need to set this to false if you are crossing container
104  /// boundaries (eg: flatpak) to workaround issues like:
105  /// <https://github.com/flatpak/flatpak/issues/3697>
106  /// <https://github.com/flatpak/flatpak/issues/3285>
107  pub fn set_controlling_tty(&mut self, controlling_tty: bool) {
108    self.controlling_tty = controlling_tty;
109  }
110
111  pub fn get_controlling_tty(&self) -> bool {
112    self.controlling_tty
113  }
114
115  /// Create a new builder instance that will run some idea of a default
116  /// program.  Such a builder will panic if `arg` is called on it.
117  pub fn new_default_prog() -> Self {
118    Self {
119      args: vec![],
120      cwd: None,
121      umask: None,
122      controlling_tty: true,
123    }
124  }
125
126  /// Returns true if this builder was created via `new_default_prog`
127  pub fn is_default_prog(&self) -> bool {
128    self.args.is_empty()
129  }
130
131  /// Append an argument to the current command line.
132  /// Will panic if called on a builder created via `new_default_prog`.
133  pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) {
134    if self.is_default_prog() {
135      panic!("attempted to add args to a default_prog builder");
136    }
137    self.args.push(arg.as_ref().to_owned());
138  }
139
140  /// Append a sequence of arguments to the current command line
141  pub fn args<I, S>(&mut self, args: I)
142  where
143    I: IntoIterator<Item = S>,
144    S: AsRef<OsStr>,
145  {
146    for arg in args {
147      self.arg(arg);
148    }
149  }
150
151  pub fn get_argv(&self) -> &Vec<OsString> {
152    &self.args
153  }
154
155  pub fn get_argv_mut(&mut self) -> &mut Vec<OsString> {
156    &mut self.args
157  }
158
159  pub fn cwd<D>(&mut self, dir: D)
160  where
161    D: AsRef<Path>,
162  {
163    self.cwd = Some(dir.as_ref().to_owned());
164  }
165
166  pub fn clear_cwd(&mut self) {
167    self.cwd.take();
168  }
169
170  pub fn get_cwd(&self) -> Option<&Path> {
171    self.cwd.as_deref()
172  }
173}
174
175impl CommandBuilder {
176  pub fn umask(&mut self, mask: Option<libc::mode_t>) {
177    self.umask = mask;
178  }
179
180  fn resolve_path(&self) -> Option<OsString> {
181    env::var_os("PATH")
182  }
183
184  fn search_path(&self, exe: &OsStr, cwd: &Path) -> color_eyre::Result<PathBuf> {
185    use nix::unistd::{AccessFlags, access};
186    use std::path::Path;
187
188    let exe_path: &Path = exe.as_ref();
189    if exe_path.is_relative() {
190      let abs_path = cwd.join(exe_path);
191      if abs_path.exists() {
192        return Ok(abs_path);
193      }
194
195      if let Some(path) = self.resolve_path() {
196        for path in std::env::split_paths(&path) {
197          let candidate = path.join(exe);
198          if access(&candidate, AccessFlags::X_OK).is_ok() {
199            return Ok(candidate);
200          }
201        }
202      }
203      bail!(
204        "Unable to spawn {} because it doesn't exist on the filesystem \
205                and was not found in PATH",
206        exe_path.display()
207      );
208    } else {
209      if let Err(err) = access(exe_path, AccessFlags::X_OK) {
210        bail!(
211          "Unable to spawn {} because it doesn't exist on the filesystem \
212                    or is not executable ({err:#})",
213          exe_path.display()
214        );
215      }
216
217      Ok(PathBuf::from(exe))
218    }
219  }
220
221  /// Convert the CommandBuilder to a `Command` instance.
222  pub(crate) fn build(self) -> color_eyre::Result<Command> {
223    use std::os::unix::process::CommandExt;
224    let cwd = env::current_dir()?;
225    let dir = if let Some(dir) = self.cwd.as_deref() {
226      dir.to_owned()
227    } else {
228      cwd
229    };
230    let resolved = self.search_path(&self.args[0], &dir)?;
231    tracing::trace!("resolved path to {:?}", resolved);
232
233    Ok(Command {
234      program: resolved,
235      args: self
236        .args
237        .into_iter()
238        .map(|a| CString::new(a.into_vec()))
239        .collect::<Result<_, _>>()?,
240      cwd: dir,
241    })
242  }
243}
244
245pub struct Command {
246  pub program: PathBuf,
247  pub args: Vec<CString>,
248  pub cwd: PathBuf,
249}