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}