Skip to main content

cba/
broc.rs

1//! Utilities for (spawning) processes
2
3use crate::{StringError, bait::ResultExt, bog::BogOkExt, ebog};
4use cfg_if::cfg_if;
5use log::{debug, trace};
6use std::{
7    env,
8    ffi::{OsStr, OsString},
9    path::Path,
10    process::{Child, ChildStdout, Command, Stdio, exit},
11    sync::LazyLock,
12};
13
14#[easy_ext::ext(ChildExt)]
15impl Child {
16    pub fn wait_for_code(&mut self) -> i32 {
17        if let Some(status) = self.wait()._elog() {
18            status.code().unwrap_or(1)
19        } else {
20            1
21        }
22    }
23}
24
25#[easy_ext::ext(CommandExt)]
26impl Command {
27    /// Use [`SHELL`] to create a command from a shell script
28    /// On unix, the empty string is given to $0 so that subsequent args are fed to the script directly.
29    /// On windows (todo)
30    pub fn from_script(script: &str) -> Self {
31        let (shell, arg) = &*SHELL;
32
33        let mut ret = Command::new(shell);
34
35        ret.arg(arg).arg(script).arg(""); // the first argument is the program name which we leave empty
36
37        ret
38    }
39
40    pub fn with_arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
41        self.arg(arg);
42        self
43    }
44
45    pub fn with_args<I, S>(mut self, args: I) -> Self
46    where
47        I: IntoIterator<Item = S>,
48        S: AsRef<OsStr>,
49    {
50        self.args(args);
51        self
52    }
53
54    /// Display the command.
55    /// Does not escape arguments.
56    pub fn display(&self) -> String {
57        std::iter::once(self.get_program())
58            .chain(self.get_args())
59            .map(|s| s.to_string_lossy())
60            .collect::<Vec<_>>()
61            .join(" ")
62    }
63
64    /// Detach the command.
65    /// Does nothing on unsupported platforms (not windows or unix).
66    pub fn detach(&mut self) -> &mut Self {
67        cfg_if! {
68            if #[cfg(unix)] {
69                use std::os::unix::process::CommandExt;
70
71                unsafe {
72                    self.pre_exec(|| {
73                        // POSIX.1-2017 describes `fork` as async-signal-safe with the following note:
74                        //
75                        // > While the fork() function is async-signal-safe, there is no way for
76                        // > an implementation to determine whether the fork handlers established by
77                        // > pthread_atfork() are async-signal-safe. [...] It is therefore undefined for the
78                        // > fork handlers to execute functions that are not async-signal-safe when fork()
79                        // > is called from a signal handler.
80                        //
81                        // POSIX.1-2024 removes this guarantee and introduces an async-signal-safe
82                        // replacement `_Fork`, which we'd like to use, but macOS doesn't support it yet.
83                        //
84                        // Since we aren't registering any fork handlers, and hopefully the OS doesn't
85                        // either, we're fine on systems compatible with POSIX.1-2017, which should be
86                        // enough for a long while. If this ever becomes a problem in the future, we should
87                        // be able to switch to `_Fork`.
88                        match libc::fork() {
89                            -1 => (),
90                            0 => (),
91                            _ => libc::_exit(0),
92                        }
93
94                        if libc::setsid() == -1 {
95                            // continue even if setsid fails
96                        }
97                        Ok(())
98                    });
99                }
100            } else if #[cfg(windows)] {
101                use std::os::windows::process::CommandExt;
102
103                const DETACHED_PROCESS: u32 = 0x00000008;
104                const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
105
106                self.creation_flags(
107                    DETACHED_PROCESS // detaches console like CREATE_NO_WINDOW, but also eliminates io
108                    | CREATE_NEW_PROCESS_GROUP
109                );
110            } else {
111                log::info!("Failed to detach: unsupported platform")
112            }
113        }
114
115        self
116    }
117
118    /// One-off spawn executable.
119    /// Logs the command in debug builds.
120    /// Prints error.
121    pub fn spawn_detached(&mut self) -> Option<Child> {
122        let ep = format!("Failed to spawn: {}", self.display());
123        debug!("Spawning detached: {self:?}");
124
125        self.stdin(Stdio::null())
126            .stdout(Stdio::null())
127            .stderr(Stdio::null())
128            .detach();
129
130        self.spawn().prefix(&ep)._ebog()
131    }
132
133    /// Spawn command with piped stdout.
134    /// Debug logs the command.
135    pub fn spawn_piped(&mut self) -> Result<ChildStdout, StringError> {
136        trace!("Spawning piped: {self:?}");
137
138        match self
139            .stdin(Stdio::null())
140            .stdout(Stdio::piped())
141            .stderr(Stdio::null())
142            .spawn()
143            .prefix(&format!("Failed to spawn: {}", self.display()))?
144            .stdout
145            .take()
146        {
147            Some(s) => Ok(s),
148            None => Err(format!("No stdout for {}.", self.display()).into()), // stdout failure has no reason suffix
149        }
150    }
151
152    /// Naive check of whether a command succeeds. (i.e. health check).
153    pub fn success(&mut self) -> bool {
154        self.stdout(Stdio::null())
155            .stderr(Stdio::null())
156            .status()
157            .map(|status| status.success())
158            .unwrap_or(false)
159    }
160
161    /// Platform-agnostic exec the command.
162    ///
163    /// Logs and displays errors.
164    pub fn _exec(&mut self) -> ! {
165        debug!("Becoming: {self:?}");
166
167        #[cfg(not(windows))]
168        {
169            // replace current process
170            use std::os::unix::process::CommandExt;
171            let err = self.exec();
172
173            ebog!("Could not exec {}: {err}", self.display());
174            exit(1)
175        }
176
177        #[cfg(windows)]
178        {
179            match self.status() {
180                Ok(status) => exit(
181                    status
182                        .code()
183                        .unwrap_or(if status.success() { 0 } else { 1 }),
184                ),
185                Err(err) => {
186                    ebog!("Could not exec {}: {err}", self.display());
187                    exit(1)
188                }
189            }
190        }
191    }
192
193    /// Spawn the command, with trace and error logging.
194    pub fn _spawn(&mut self) -> Option<Child> {
195        trace!("Spawning: {self:?}");
196        self.spawn()
197            .prefix(&format!("Could not spawn: {}", self.display()))
198            ._elog()
199    }
200}
201
202/// Join arguments into a single string
203/// Non-UTF-8 arguments are not escaped
204/// Todo: support windows
205pub fn format_sh_command(inputs: &[impl AsRef<OsStr>], double: bool) -> OsString {
206    let mut cmd = OsString::new();
207    let mut first = true;
208
209    for arg in inputs {
210        if !first {
211            cmd.push(" ");
212        }
213        first = false;
214
215        let os = arg.as_ref();
216
217        match os.to_str() {
218            Some(s) => {
219                if double {
220                    let escaped = s
221                        .replace("\\", "\\\\")
222                        .replace('\"', "\\\"")
223                        .replace("$", "\\$");
224                    cmd.push("\"");
225                    cmd.push(escaped);
226                    cmd.push("\"");
227                } else {
228                    let escaped = s.replace('\'', "'\\''");
229                    cmd.push("'");
230                    cmd.push(escaped);
231                    cmd.push("'");
232                }
233            }
234            None => {
235                // no shell-escape if not valid UTF-8 since there is no safe way to do it.
236                cmd.push(os);
237            }
238        }
239    }
240
241    cmd
242}
243
244pub fn display_sh_prog_and_args(prog: impl AsRef<OsStr>, args: &[impl AsRef<OsStr>]) -> String {
245    format_sh_command(
246        &{
247            let mut i = vec![prog.as_ref()];
248            i.extend(args.iter().map(|x| x.as_ref()));
249            i
250        },
251        false,
252    )
253    .to_string_lossy()
254    .to_string()
255}
256
257/// (shell path, shell arg)
258pub static SHELL: LazyLock<(String, String)> = LazyLock::new(|| {
259    #[cfg(windows)]
260    {
261        let path = env::var("COMSPEC").unwrap_or_else(|_| "cmd.exe".to_string());
262
263        let lower = path.to_lowercase();
264        let flag = if lower.contains("powershell") || lower.contains("pwsh") {
265            "-Command".to_string()
266        } else {
267            "/C".to_string()
268        };
269        (path, flag)
270    }
271    #[cfg(unix)]
272    {
273        let path = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
274        let flag = "-c".to_string();
275        log::debug!("SHELL: {}, {}", path, flag);
276        (path, flag)
277    }
278});
279
280/// # Note
281/// AI generated cuz i dunno windows, seems fine?
282pub fn current_shell() -> String {
283    #[cfg(unix)]
284    {
285        if let Ok(shell) = env::var("SHELL") {
286            if let Some(name) = Path::new(&shell).file_name().and_then(|n| n.to_str()) {
287                return name.to_ascii_lowercase();
288            }
289        }
290    }
291
292    #[cfg(windows)]
293    {
294        // Prefer modern PowerShell if present
295        if let Ok(ps) = env::var("PSModulePath") {
296            if !ps.is_empty() {
297                return "pwsh".into();
298            }
299        }
300
301        if let Ok(comspec) = env::var("COMSPEC") {
302            if let Some(name) = Path::new(&comspec).file_name().and_then(|n| n.to_str()) {
303                return name.to_ascii_lowercase();
304            }
305        }
306    }
307
308    String::new()
309}
310
311pub fn tty_or_inherit() -> Stdio {
312    if let Ok(mut tty) = std::fs::File::open("/dev/tty") {
313        let _ = std::io::Write::flush(&mut tty); // does nothing but seems logical
314        Stdio::from(tty)
315    } else {
316        log::error!("Failed to open /dev/tty");
317        Stdio::inherit()
318    }
319}
320
321use std::{cell::RefCell, collections::HashMap};
322thread_local! {
323    static HAS_CACHE: RefCell<HashMap<String, bool>> = RefCell::new(HashMap::new());
324}
325
326pub fn has(name: &str) -> bool {
327    HAS_CACHE.with(|cache| {
328        let mut cache = cache.borrow_mut();
329        if let Some(&found) = cache.get(name) {
330            found
331        } else {
332            let found = which::which(name).is_ok();
333            cache.insert(name.to_owned(), found);
334            found
335        }
336    })
337}
338
339// ENV VARS
340pub type EnvVars = Vec<(String, String)>;
341
342#[macro_export]
343macro_rules! env_vars {
344    ($( $name:expr => $value:expr ),* $(,)?) => {
345        Vec::<(String, String)>::from([
346            $( ($name.into(), $value.into()) ),*
347            ]
348        )
349    };
350}