Skip to main content

lurk_cli/
lib.rs

1//! lurk is a pretty (simple) alternative to strace.
2//!
3//! ## Installation
4//!
5//! Add the following dependencies to your `Cargo.toml`
6//!
7//! ```toml
8//! [dependencies]
9//! lurk-cli = "0.3.6"
10//! nix = { version = "0.27.1", features = ["ptrace", "signal"] }
11//! console = "0.15.8"
12//! ```
13//!
14//! ## Usage
15//!
16//! First crate a tracee using [`run_tracee`] method. Then you can construct a [`Tracer`]
17//! struct to trace the system calls via calling [`run_tracer`].
18//!
19//! ## Examples
20//!
21//! ```rust
22//! use anyhow::{bail, Result};
23//! use console::Style;
24//! use lurk_cli::{args::Args, style::StyleConfig, Tracer};
25//! use nix::unistd::{fork, ForkResult};
26//! use std::io;
27//!
28//! fn main() -> Result<()> {
29//!     let command = String::from("/usr/bin/ls");
30//!
31//!     let pid = match unsafe { fork() } {
32//!         Ok(ForkResult::Child) => {
33//!             return lurk_cli::run_tracee(&[command], &[], &None);
34//!         }
35//!         Ok(ForkResult::Parent { child }) => child,
36//!         Err(err) => bail!("fork() failed: {err}"),
37//!     };
38//!
39//!     let args = Args::default();
40//!     let output = io::stdout();
41//!     let style = StyleConfig {
42//!         pid: Style::new().cyan(),
43//!         syscall: Style::new().white().bold(),
44//!         success: Style::new().green(),
45//!         error: Style::new().red(),
46//!         result: Style::new().yellow(),
47//!         use_colors: true,
48//!     };
49//!
50//!     Tracer::new(pid, args, output, style)?.run_tracer()
51//! }
52//! ```
53//!
54//! [`run_tracee`]: crate::run_tracee
55//! [`Tracer`]: crate::Tracer
56//! [`run_tracer`]: crate::Tracer::run_tracer
57
58#[deny(clippy::pedantic, clippy::format_push_string)]
59// TODO: re-check the casting lints - they might indicate an issue
60#[allow(
61    clippy::cast_possible_truncation,
62    clippy::cast_possible_wrap,
63    clippy::cast_precision_loss,
64    clippy::missing_errors_doc,
65    clippy::missing_panics_doc,
66    clippy::must_use_candidate,
67    clippy::redundant_closure_for_method_calls,
68    clippy::struct_excessive_bools
69)]
70pub mod arch;
71pub mod args;
72pub mod style;
73pub mod syscall_info;
74
75use anyhow::{anyhow, Result};
76use comfy_table::modifiers::UTF8_ROUND_CORNERS;
77use comfy_table::presets::UTF8_BORDERS_ONLY;
78use comfy_table::CellAlignment::Right;
79use comfy_table::{Cell, ContentArrangement, Row, Table};
80use libc::user_regs_struct;
81use nix::sys::personality::{self, Persona};
82use nix::sys::ptrace::{self, Event};
83use nix::sys::signal::Signal;
84use nix::sys::wait::{wait, WaitStatus};
85use nix::unistd::Pid;
86use std::collections::HashMap;
87use std::fs;
88use std::io::Write;
89use std::os::unix::process::CommandExt;
90use std::process::{Command, Stdio};
91use std::time::{Duration, SystemTime};
92use style::StyleConfig;
93use syscalls::{Sysno, SysnoMap, SysnoSet};
94use uzers::get_user_by_name;
95
96use crate::args::{Args, Filter};
97use crate::syscall_info::{RetCode, SyscallArgs, SyscallInfo};
98
99const STRING_LIMIT: usize = 32;
100
101pub struct Tracer<W: Write> {
102    pid: Pid,
103    args: Args,
104    string_limit: Option<usize>,
105    filter: Filter,
106    syscalls_time: SysnoMap<Duration>,
107    syscalls_pass: SysnoMap<u64>,
108    syscalls_fail: SysnoMap<u64>,
109    style_config: StyleConfig,
110    output: W,
111    // If enabled, count and collapse repeated failing execve attempts per pid
112    exec_retry_counts: std::collections::HashMap<Pid, usize>,
113}
114
115impl<W: Write> Tracer<W> {
116    pub fn new(pid: Pid, args: Args, output: W, style_config: StyleConfig) -> Result<Self> {
117        Ok(Self {
118            pid,
119            filter: args.create_filter()?,
120            string_limit: if args.no_abbrev {
121                None
122            } else {
123                Some(args.string_limit.unwrap_or(STRING_LIMIT))
124            },
125            args,
126            syscalls_time: SysnoMap::from_iter(
127                SysnoSet::all().iter().map(|v| (v, Duration::default())),
128            ),
129            syscalls_pass: SysnoMap::from_iter(SysnoSet::all().iter().map(|v| (v, 0))),
130            syscalls_fail: SysnoMap::from_iter(SysnoSet::all().iter().map(|v| (v, 0))),
131            style_config,
132            output,
133            exec_retry_counts: HashMap::new(),
134        })
135    }
136
137    pub fn set_output(&mut self, output: W) {
138        self.output = output;
139    }
140
141    #[allow(clippy::too_many_lines)]
142    pub fn run_tracer(&mut self) -> Result<()> {
143        // Create a hashmap to track entry and exit times across all forked processes individually.
144        let mut start_times = HashMap::<Pid, Option<SystemTime>>::new();
145        // Store pre-parsed args for special syscalls (execve/execveat) captured at syscall entry
146        let mut pending_args = HashMap::<Pid, Option<SyscallArgs>>::new();
147        start_times.insert(self.pid, None);
148        pending_args.insert(self.pid, None);
149
150        let mut options_initialized = false;
151        let mut entry_regs = None;
152
153        loop {
154            let status = wait()?;
155
156            if !options_initialized {
157                if self.args.follow_forks {
158                    arch::ptrace_init_options_fork(self.pid)?;
159                } else {
160                    arch::ptrace_init_options(self.pid)?;
161                }
162                options_initialized = true;
163            }
164
165            match status {
166                // `WIFSTOPPED(status), signal is WSTOPSIG(status)
167                WaitStatus::Stopped(pid, signal) => {
168                    // There are three reasons why a child might stop with SIGTRAP:
169                    // 1) syscall entry
170                    // 2) syscall exit
171                    // 3) child calls exec
172                    //
173                    // Because we are tracing with PTRACE_O_TRACESYSGOOD, syscall entry and syscall exit
174                    // are stopped in PtraceSyscall and not here, which means if we get a SIGTRAP here,
175                    // it's because the child called exec.
176                    if signal == Signal::SIGTRAP {
177                        // At exec the address space may change; prefer registers captured
178                        // at the previous syscall entry (if present) so we can read argv/envp
179                        // from the original address space. Consume `entry_regs` if set.
180                        let regs = entry_regs.take();
181                        let pre = pending_args.remove(&pid).unwrap_or(None);
182
183                        // If we don't have entry registers (e.g. first-stop), try a best-effort
184                        // parse from the current registers before falling back to pointer hex.
185                        if regs.is_none() {
186                            if let Ok(cur_regs) = self.get_registers(pid) {
187                                if let Ok(sysno) = self.get_syscall(cur_regs) {
188                                    if sysno == Sysno::execve || sysno == Sysno::execveat {
189                                        // parse args now
190                                        let args_now = arch::parse_args(pid, sysno, cur_regs);
191                                        self.log_standard_syscall(
192                                            pid,
193                                            Some(cur_regs),
194                                            Some(args_now),
195                                            None,
196                                            None,
197                                        )?;
198                                        self.issue_ptrace_syscall_request(pid, None)?;
199                                        continue;
200                                    }
201                                }
202                            }
203                        }
204
205                        self.log_standard_syscall(pid, regs, pre, None, None)?;
206                        self.issue_ptrace_syscall_request(pid, None)?;
207                        continue;
208                    }
209
210                    // If we trace with PTRACE_O_TRACEFORK, PTRACE_O_TRACEVFORK, and PTRACE_O_TRACECLONE,
211                    // a created child of our tracee will stop with SIGSTOP.
212                    // If our tracee creates children of their own, we want to trace their syscall times with a new value.
213                    if signal == Signal::SIGSTOP {
214                        if self.args.follow_forks {
215                            start_times.insert(pid, None);
216
217                            if !self.args.summary_only {
218                                writeln!(&mut self.output, "Attaching to child {}", pid,)?;
219                            }
220                        }
221
222                        self.issue_ptrace_syscall_request(pid, None)?;
223                        continue;
224                    }
225
226                    // The SIGCHLD signal is sent to a process when a child process terminates, interrupted, or resumes after being interrupted
227                    // This means, that if our tracee forked and said fork exits before the parent, the parent will get stopped.
228                    // Therefor issue a PTRACE_SYSCALL request to the parent to continue execution.
229                    // This is also important if we trace without the following forks option.
230                    if signal == Signal::SIGCHLD {
231                        self.issue_ptrace_syscall_request(pid, Some(signal))?;
232                        continue;
233                    }
234
235                    // If we fall through to here, we have another signal that's been sent to the tracee,
236                    // in this case, just forward the singal to the tracee to let it handle it.
237                    // TODO: Finer signal handling, edge-cases etc.
238                    ptrace::cont(pid, signal)?;
239                }
240                // WIFEXITED(status)
241                WaitStatus::Exited(pid, _) => {
242                    // If the process that exits is the original tracee, we can safely break here,
243                    // but we need to continue if the process that exits is a child of the original tracee.
244                    if self.pid == pid {
245                        break;
246                    } else {
247                        continue;
248                    };
249                }
250                // The traced process was stopped by a `PTRACE_EVENT_*` event.
251                WaitStatus::PtraceEvent(pid, _, code) => {
252                    // Handle exec events specially: prefer pre-parsed args captured at syscall
253                    // entry time (stored in `pending_args`) so we can print argv/envp even
254                    // after the address space has been replaced. When we detect an exec
255                    // event, log it as a successful exec (return 0) using the stored
256                    // args if present, otherwise fall back to best-effort parsing.
257                    if code == Event::PTRACE_EVENT_EXEC as i32 {
258                        // consume any pending args parsed at entry
259                        let pre = pending_args.remove(&pid).unwrap_or(None);
260                        if let Some(args_now) = pre {
261                            // Log as successful exec (execve should not return on success).
262                            self.log_exec_event(pid, args_now)?;
263                        } else if let Ok(regs) = self.get_registers(pid) {
264                            if let Ok(sysno) = self.get_syscall(regs) {
265                                if sysno == Sysno::execve || sysno == Sysno::execveat {
266                                    let args_now = arch::parse_args(pid, sysno, regs);
267                                    self.log_exec_event(pid, args_now)?;
268                                }
269                            }
270                        }
271                    }
272
273                    // We also stop at the PTRACE_EVENT_EXIT event because of the PTRACE_O_TRACEEXIT option.
274                    // We do this to properly catch and log exit-family syscalls, which do not have an PTRACE_SYSCALL_INFO_EXIT event.
275                    if code == Event::PTRACE_EVENT_EXIT as i32 && self.is_exit_syscall(pid)? {
276                        // use any pending args if present
277                        let pre = pending_args.remove(&pid).unwrap_or(None);
278                        self.log_standard_syscall(pid, None, pre, None, None)?;
279                    }
280
281                    self.issue_ptrace_syscall_request(pid, None)?;
282                }
283                // Tracee is traced with the PTRACE_O_TRACESYSGOOD option.
284                WaitStatus::PtraceSyscall(pid) => {
285                    // ptrace(PTRACE_GETEVENTMSG,...) can be one of three values here:
286                    // 1) PTRACE_SYSCALL_INFO_NONE
287                    // 2) PTRACE_SYSCALL_INFO_ENTRY
288                    // 3) PTRACE_SYSCALL_INFO_EXIT
289                    let event = ptrace::getevent(pid)? as u8;
290
291                    // Snapshot current time, to avoid polluting the syscall time with
292                    // non-syscall related latency.
293                    let timestamp = Some(SystemTime::now());
294
295                    // We only want to log regular syscalls on exit
296                    if let Some(syscall_start_time) = start_times.get_mut(&pid) {
297                        if event == 2 {
298                            let pre = pending_args.remove(&pid).unwrap_or(None);
299                            self.log_standard_syscall(
300                                pid,
301                                entry_regs,
302                                pre,
303                                *syscall_start_time,
304                                timestamp,
305                            )?;
306                            *syscall_start_time = None;
307                        } else {
308                            *syscall_start_time = timestamp;
309                            let regs = self.get_registers(pid)?;
310                            // Save entry registers for later use
311                            entry_regs = Some(regs);
312
313                            // Try to detect exec-like syscalls and pre-parse their args while
314                            // the address space is still intact.
315                            if let Ok(sysno) = self.get_syscall(regs) {
316                                if sysno == Sysno::execve || sysno == Sysno::execveat {
317                                    let args = arch::parse_args(pid, sysno, regs);
318                                    pending_args.insert(pid, Some(args));
319                                } else {
320                                    pending_args.insert(pid, None);
321                                }
322                            }
323                        }
324                    } else {
325                        return Err(anyhow!("Unable to get start time for tracee {}", pid));
326                    }
327
328                    self.issue_ptrace_syscall_request(pid, None)?;
329                }
330                // WIFSIGNALED(status), signal is WTERMSIG(status) and coredump is WCOREDUMP(status)
331                WaitStatus::Signaled(pid, signal, coredump) => {
332                    writeln!(
333                        &mut self.output,
334                        "Child {} terminated by signal {} {}",
335                        pid,
336                        signal,
337                        if coredump { "(core dumped)" } else { "" }
338                    )?;
339                    break;
340                }
341                // WIFCONTINUED(status), this usually happens when a process receives a SIGCONT.
342                // Just continue with the next iteration of the loop.
343                WaitStatus::Continued(_) | WaitStatus::StillAlive => {
344                    continue;
345                }
346            }
347        }
348
349        if !self.args.json && (self.args.summary_only || self.args.summary) {
350            if !self.args.summary_only {
351                // Make a gap between the last syscall and the summary
352                writeln!(&mut self.output)?;
353            }
354            self.report_summary()?;
355        }
356
357        Ok(())
358    }
359
360    pub fn report_summary(&mut self) -> Result<()> {
361        let headers = vec!["% time", "time", "time/call", "calls", "errors", "syscall"];
362        let mut table = Table::new();
363        table
364            .load_preset(UTF8_BORDERS_ONLY)
365            .apply_modifier(UTF8_ROUND_CORNERS)
366            .set_content_arrangement(ContentArrangement::Dynamic)
367            .set_header(&headers);
368
369        for i in 0..headers.len() {
370            table.column_mut(i).unwrap().set_cell_alignment(Right);
371        }
372
373        let mut sorted_sysno: Vec<_> = self.filter.all_enabled().iter().collect();
374        sorted_sysno.sort_by_key(|k| k.name());
375        let t_time: Duration = self.syscalls_time.values().sum();
376
377        for sysno in sorted_sysno {
378            let (Some(pass), Some(fail), Some(time)) = (
379                self.syscalls_pass.get(sysno),
380                self.syscalls_fail.get(sysno),
381                self.syscalls_time.get(sysno),
382            ) else {
383                continue;
384            };
385
386            let calls = pass + fail;
387            if calls == 0 {
388                continue;
389            }
390
391            let time_percent = if !t_time.is_zero() {
392                time.as_secs_f32() / t_time.as_secs_f32() * 100f32
393            } else {
394                0f32
395            };
396
397            table.add_row(vec![
398                Cell::new(format!("{time_percent:.1}%")),
399                Cell::new(format!("{}µs", time.as_micros())),
400                Cell::new(format!("{:.1}ns", time.as_nanos() as f64 / calls as f64)),
401                Cell::new(format!("{calls}")),
402                Cell::new(format!("{fail}")),
403                Cell::new(sysno.name()),
404            ]);
405        }
406
407        // Create the totals row, but don't add it to the table yet
408        let failed = self.syscalls_fail.values().sum::<u64>();
409        let calls: u64 = self.syscalls_pass.values().sum::<u64>() + failed;
410        let totals: Row = vec![
411            Cell::new("100%"),
412            Cell::new(format!("{}µs", t_time.as_micros())),
413            Cell::new(format!("{:.1}ns", t_time.as_nanos() as f64 / calls as f64)),
414            Cell::new(calls),
415            Cell::new(failed.to_string()),
416            Cell::new("total"),
417        ]
418        .into();
419
420        // TODO: consider using another table-creating crate
421        //       https://github.com/Nukesor/comfy-table/issues/104
422        // This is a hack to add a line between the table and the summary,
423        // computing max column width of each existing row plus the totals row
424        let divider_row: Vec<String> = table
425            .column_max_content_widths()
426            .iter()
427            .copied()
428            .enumerate()
429            .map(|(idx, val)| {
430                let cell_at_idx = totals.cell_iter().nth(idx).unwrap();
431                (val as usize).max(cell_at_idx.content().len())
432            })
433            .map(|v| str::repeat("-", v))
434            .collect();
435        table.add_row(divider_row);
436        table.add_row(totals);
437
438        if !self.args.summary_only {
439            // separate a list of syscalls from the summary table with an blank line
440            writeln!(&mut self.output)?;
441        }
442        writeln!(&mut self.output, "{table}")?;
443
444        Ok(())
445    }
446
447    fn log_standard_syscall(
448        &mut self,
449        pid: Pid,
450        entry_regs: Option<user_regs_struct>,
451        pre_parsed_args: Option<SyscallArgs>,
452        syscall_start_time: Option<SystemTime>,
453        syscall_end_time: Option<SystemTime>,
454    ) -> Result<()> {
455        let register_data = self.parse_register_data(pid);
456        if let Err(e) = register_data {
457            eprintln!("{e}");
458            return Ok(());
459        }
460        let (syscall_number, registers) = register_data.unwrap();
461
462        // Theres no PTRACE_SYSCALL_INFO_EXIT for an exit-family syscall, hence ret_code will always be 0xffffffffffffffda (which is -38)
463        // -38 is ENOSYS which is put into RAX as a default return value by the kernel's syscall entry code.
464        // In order to not pollute the summary with this false positive, avoid exit-family syscalls from being counted (same behaviour as strace).
465        let ret_code = match syscall_number {
466            Sysno::exit | Sysno::exit_group => RetCode::from_raw(0),
467            _ => {
468                #[cfg(target_arch = "x86_64")]
469                let code = RetCode::from_raw(registers.rax);
470                #[cfg(target_arch = "riscv64")]
471                let code = RetCode::from_raw(registers.a7);
472                #[cfg(target_arch = "aarch64")]
473                let code = RetCode::from_raw(registers.regs[0]);
474                match code {
475                    RetCode::Err(_) => self.syscalls_fail[syscall_number] += 1,
476                    _ => self.syscalls_pass[syscall_number] += 1,
477                }
478                code
479            }
480        };
481
482        // Prefer entry registers if provided (they allow reading strings before exec).
483        let registers = entry_regs.unwrap_or(registers);
484
485        // Special handling: collapse repeated failing execve attempts if enabled.
486        if self.args.collapse_exec_retries
487            && (syscall_number == Sysno::execve || syscall_number == Sysno::execveat)
488        {
489            if let RetCode::Err(errno) = ret_code {
490                // ENOENT is -2: common when execvp probes PATH entries.
491                if errno == -2 {
492                    let counter = self.exec_retry_counts.entry(pid).or_default();
493                    *counter += 1;
494                    // suppress printing this failing execve
495                    return Ok(());
496                }
497            } else {
498                // success: if we suppressed prior failures, print a compact summary
499                if let Some(count) = self.exec_retry_counts.remove(&pid) {
500                    if count > 0 {
501                        writeln!(
502                            &mut self.output,
503                            "[{}] execve: collapsed {} failed attempts",
504                            pid, count
505                        )?;
506                    }
507                }
508            }
509        }
510
511        if self.filter.matches(syscall_number, ret_code) {
512            let elapsed = syscall_start_time.map_or(Duration::default(), |start_time| {
513                let end_time = syscall_end_time.unwrap_or(SystemTime::now());
514                end_time.duration_since(start_time).unwrap_or_default()
515            });
516
517            if syscall_start_time.is_some() {
518                self.syscalls_time[syscall_number] += elapsed;
519            }
520
521            if !self.args.summary_only {
522                // Use pre-parsed args if provided (captured at entry), otherwise parse now.
523                let args = pre_parsed_args
524                    .unwrap_or_else(|| arch::parse_args(pid, syscall_number, registers));
525                let info = SyscallInfo {
526                    typ: "SYSCALL",
527                    pid,
528                    syscall: syscall_number,
529                    args,
530                    result: ret_code,
531                    duration: elapsed,
532                };
533                self.write_syscall_info(&info)?;
534            }
535        }
536
537        Ok(())
538    }
539
540    fn log_exec_event(&mut self, pid: Pid, args: SyscallArgs) -> Result<()> {
541        // Construct a SyscallInfo-like record for exec events. Mark result as Ok(0)
542        // since PTRACE_EVENT_EXEC means the exec completed successfully.
543        let info = SyscallInfo {
544            typ: "SYSCALL",
545            pid,
546            syscall: Sysno::execve,
547            args,
548            result: RetCode::Ok(0),
549            duration: Duration::default(),
550        };
551        self.write_syscall_info(&info)?;
552        Ok(())
553    }
554
555    fn write_syscall_info(&mut self, info: &SyscallInfo) -> Result<()> {
556        if self.args.json {
557            let json = serde_json::to_string(&info)?;
558            Ok(writeln!(&mut self.output, "{json}")?)
559        } else {
560            info.write_syscall(
561                self.style_config.clone(),
562                self.string_limit,
563                self.args.syscall_number,
564                self.args.syscall_times,
565                &mut self.output,
566            )
567        }
568    }
569
570    // Issue a PTRACE_SYSCALL request to the tracee, forwarding a signal if one is provided.
571    fn issue_ptrace_syscall_request(&self, pid: Pid, signal: Option<Signal>) -> Result<()> {
572        ptrace::syscall(pid, signal)
573            .map_err(|_| anyhow!("Unable to issue a PTRACE_SYSCALL request in tracee {}", pid))
574    }
575
576    // TODO: This is arch-specific code and should be modularized
577    fn get_registers(&self, pid: Pid) -> Result<user_regs_struct> {
578        ptrace::getregs(pid).map_err(|_| anyhow!("Unable to get registers from tracee {}", pid))
579    }
580
581    fn get_syscall(&self, registers: user_regs_struct) -> Result<Sysno> {
582        #[cfg(target_arch = "x86_64")]
583        let reg = registers.orig_rax;
584        #[cfg(target_arch = "riscv64")]
585        let reg = registers.a7;
586        #[cfg(target_arch = "aarch64")]
587        let reg = registers.regs[8];
588
589        Ok(u32::try_from(reg)
590            .map_err(|_| anyhow!("Invalid syscall number {reg}"))?
591            .into())
592    }
593
594    // Issues a ptrace(PTRACE_GETREGS, ...) request and gets the corresponding syscall number (Sysno).
595    fn parse_register_data(&self, pid: Pid) -> Result<(Sysno, user_regs_struct)> {
596        let registers = self.get_registers(pid)?;
597        let syscall_number = self.get_syscall(registers)?;
598
599        Ok((syscall_number, registers))
600    }
601
602    fn is_exit_syscall(&self, pid: Pid) -> Result<bool> {
603        self.get_registers(pid).map(|registers| {
604            #[cfg(target_arch = "x86_64")]
605            let reg = registers.orig_rax;
606            #[cfg(target_arch = "riscv64")]
607            let reg = registers.a7;
608            #[cfg(target_arch = "aarch64")]
609            let reg = registers.regs[8];
610            reg == Sysno::exit as u64 || reg == Sysno::exit_group as u64
611        })
612    }
613}
614
615pub fn run_tracee(command: &[String], envs: &[String], username: &Option<String>) -> Result<()> {
616    ptrace::traceme()?;
617    // Stop ourselves so the tracer parent can set ptrace options before exec.
618    // This improves reliability of capturing the initial execve syscall arguments.
619    // Make this behavior conditional: set `LURK_DISABLE_SIGSTOP=1` in the environment
620    // to skip raising SIGSTOP (useful when running under debuggers or wrappers).
621    if std::env::var_os("LURK_DISABLE_SIGSTOP").is_none() {
622        nix::sys::signal::raise(Signal::SIGSTOP).map_err(|_| anyhow!("Unable to raise SIGSTOP"))?;
623    }
624    personality::set(Persona::ADDR_NO_RANDOMIZE)
625        .map_err(|_| anyhow!("Unable to set ADDR_NO_RANDOMIZE"))?;
626    let mut binary = command
627        .first()
628        .ok_or_else(|| anyhow!("No command"))?
629        .to_string();
630    if let Ok(bin) = fs::canonicalize(&binary) {
631        binary = bin
632            .to_str()
633            .ok_or_else(|| anyhow!("Invalid binary path"))?
634            .to_string()
635    }
636    let mut cmd = Command::new(binary);
637    cmd.args(command[1..].iter()).stdout(Stdio::null());
638
639    for token in envs {
640        let mut parts = token.splitn(2, '=');
641        match (parts.next(), parts.next()) {
642            (Some(key), Some(value)) => cmd.env(key, value),
643            (Some(key), None) => cmd.env_remove(key),
644            _ => unreachable!(),
645        };
646    }
647
648    if let Some(username) = username {
649        if let Some(user) = get_user_by_name(username) {
650            cmd.uid(user.uid());
651        }
652    }
653
654    let _ = cmd.exec();
655
656    Ok(())
657}