Skip to main content

fm/io/
commands.rs

1use std::env;
2use std::fmt;
3use std::io::Write;
4use std::path::Path;
5use std::process::{Command, Stdio};
6
7use anyhow::{anyhow, bail, Context, Result};
8use nucleo::Injector;
9use tokio::{
10    io::AsyncBufReadExt, io::BufReader as TokioBufReader, process::Command as TokioCommand,
11};
12
13use crate::common::{current_username, is_in_path, GREP_EXECUTABLE, RG_EXECUTABLE, SETSID};
14use crate::modes::PasswordHolder;
15use crate::{log_info, log_line};
16
17/// Execute a command with options in a fork with setsid.
18/// If the `SETSID` application isn't there, call the program directly.
19/// but the program may be closed if the parent (fm) is stopped.
20/// Returns an handle to the child process.
21///
22/// # Errors
23///
24/// May fail if the command can't be spawned.
25pub fn execute<S, P>(exe: S, args: &[P]) -> Result<std::process::Child>
26where
27    S: AsRef<std::ffi::OsStr> + fmt::Debug,
28    P: AsRef<std::ffi::OsStr> + fmt::Debug,
29{
30    log_info!("execute. executable: {exe:?}, arguments: {args:?}");
31    log_line!("Execute: {exe:?}, arguments: {args:?}");
32    if is_in_path(SETSID) {
33        Ok(Command::new(SETSID)
34            .arg(exe)
35            .args(args)
36            .stdin(Stdio::null())
37            .stdout(Stdio::null())
38            .stderr(Stdio::null())
39            .spawn()?)
40    } else {
41        Ok(Command::new(exe)
42            .args(args)
43            .stdin(Stdio::null())
44            .stdout(Stdio::null())
45            .stderr(Stdio::null())
46            .spawn()?)
47    }
48}
49
50/// Execute a command with options in a fork.
51/// Returns an handle to the child process.
52/// Branch stdin, stderr and stdout to /dev/null
53pub fn execute_without_output<S: AsRef<std::ffi::OsStr> + fmt::Debug>(
54    exe: S,
55    args: &[&str],
56) -> Result<std::process::Child> {
57    log_info!("execute_in_child_without_output. executable: {exe:?}, arguments: {args:?}",);
58    Ok(Command::new(exe)
59        .args(args)
60        .stdin(Stdio::null())
61        .stdout(Stdio::null())
62        .stderr(Stdio::null())
63        .spawn()?)
64}
65
66/// Execute a command with options in a fork.
67/// Wait for termination and return either :
68/// `Ok(stdout)` if the status code is 0
69/// an Error otherwise
70/// Branch stdin and stderr to /dev/null
71pub fn execute_and_capture_output<S: AsRef<std::ffi::OsStr> + fmt::Debug>(
72    exe: S,
73    args: &[&str],
74) -> Result<String> {
75    log_info!("execute_and_capture_output. executable: {exe:?}, arguments: {args:?}",);
76    let output = Command::new(exe)
77        .args(args)
78        .stdin(Stdio::null())
79        .stdout(Stdio::piped())
80        .stderr(Stdio::null())
81        .output()?;
82    if output.status.success() {
83        Ok(String::from_utf8(output.stdout)?)
84    } else {
85        Err(anyhow!(
86            "execute_and_capture_output: command didn't finish properly",
87        ))
88    }
89}
90
91/// Creates a command without executing it.
92/// Arguments are pased to the command and a current dir is set.
93/// It's used to display a command before execution.
94pub fn command_with_path<S: AsRef<std::ffi::OsStr> + fmt::Debug, P: AsRef<Path>>(
95    exe: S,
96    path: P,
97    args: &[&str],
98) -> Command {
99    let mut command = Command::new(exe);
100    command
101        .args(args)
102        .current_dir(path)
103        .stdin(Stdio::null())
104        .stdout(Stdio::null())
105        .stderr(Stdio::null());
106    command
107}
108
109/// Execute a command with options in a fork.
110/// Wait for termination and return either :
111/// `Ok(stdout)` if the status code is 0
112/// an Error otherwise
113/// Branch stdin /dev/null
114/// Log stderr if non empty.
115pub fn execute_and_capture_output_with_path<
116    S: AsRef<std::ffi::OsStr> + fmt::Debug,
117    P: AsRef<Path>,
118>(
119    exe: S,
120    path: P,
121    args: &[&str],
122) -> Result<String> {
123    log_info!("execute_and_capture_output_with_path. executable: {exe:?}, arguments: {args:?}",);
124    let output = Command::new(exe)
125        .args(args)
126        .current_dir(path)
127        .stdin(Stdio::null())
128        .stdout(Stdio::piped())
129        .stderr(Stdio::piped())
130        .output()?;
131    if output.status.success() {
132        Ok(String::from_utf8(output.stdout)?)
133    } else {
134        let err = String::from_utf8(output.stderr)?;
135        log_info!("{err}");
136        Err(anyhow!(
137            "execute_and_capture_output: command didn't finish properly: {err}",
138        ))
139    }
140}
141
142/// Execute a command with options in a fork.
143/// Wait for termination and return either `Ok(stdout)`.
144/// Branch stdin and stderr to /dev/null
145pub fn execute_and_capture_output_without_check<S>(exe: S, args: &[&str]) -> Result<String>
146where
147    S: AsRef<std::ffi::OsStr> + fmt::Debug,
148{
149    log_info!("execute_and_capture_output_without_check. executable: {exe:?}, arguments: {args:?}",);
150    let child = Command::new(exe)
151        .args(args)
152        .stdin(Stdio::null())
153        .stdout(Stdio::piped())
154        .stderr(Stdio::null())
155        .spawn()?;
156    let output = child.wait_with_output()?;
157    Ok(String::from_utf8(output.stdout)?)
158}
159
160/// Execute a command with some arguments, returns its output. stdin & stderr are branched to /dev/null.
161pub fn execute_and_output<S, I>(exe: S, args: I) -> Result<std::process::Output>
162where
163    S: AsRef<std::ffi::OsStr> + fmt::Debug,
164    I: IntoIterator<Item = S> + fmt::Debug,
165{
166    log_info!("execute_and_output. executable: {exe:?}, arguments: {args:?}",);
167    Ok(Command::new(exe)
168        .args(args)
169        .stdin(Stdio::null())
170        .stderr(Stdio::null())
171        .output()?)
172}
173
174/// Executes a command with some arguments, returns its output. Doesn't log the call.
175pub fn execute_and_output_no_log<S, I>(exe: S, args: I) -> Result<std::process::Output>
176where
177    S: AsRef<std::ffi::OsStr> + fmt::Debug,
178    I: IntoIterator<Item = S> + fmt::Debug,
179{
180    Ok(Command::new(exe).args(args).stdin(Stdio::null()).output()?)
181}
182
183/// Executes a command with `CLICOLOR_FORCE=1` and `COLORTERM=ansi` environement variables.
184/// Returns the output.
185/// Used to run & display commands which may returns colored text.
186pub fn execute_with_ansi_colors(args: &[String]) -> Result<std::process::Output> {
187    log_info!("execute. {args:?}");
188    log_line!("Executed {args:?}");
189    Ok(Command::new(&args[0])
190        .args(&args[1..])
191        .env("CLICOLOR_FORCE", "1")
192        .env("COLORTERM", "ansi")
193        .stdin(Stdio::null())
194        .stdout(Stdio::piped())
195        .stderr(Stdio::null())
196        .output()?)
197}
198
199/// Spawn a sudo command with stdin, stdout and stderr piped.
200/// sudo is run with -S argument to read the password from stdin
201/// Args are sent.
202/// CWD is set to `path`.
203/// No password is set yet.
204/// A password should be sent with `inject_password`.
205fn new_sudo_command_awaiting_password<S, P>(args: &[S], path: P) -> Result<std::process::Child>
206where
207    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
208    P: AsRef<std::path::Path> + std::fmt::Debug,
209{
210    Ok(Command::new("sudo")
211        .arg("-S")
212        .args(args)
213        .stdin(Stdio::piped())
214        .stdout(Stdio::piped())
215        .stderr(Stdio::piped())
216        .current_dir(path)
217        .spawn()?)
218}
219
220/// Send password to a sudo command through its stdin.
221fn inject_password(password: &str, child: &mut std::process::Child) -> Result<()> {
222    let child_stdin = child
223        .stdin
224        .as_mut()
225        .context("inject_password: couldn't open child stdin")?;
226    child_stdin.write_all(password.as_bytes())?;
227    child_stdin.write_all(b"\n")?;
228    Ok(())
229}
230
231/// run a sudo command requiring a password (generally to establish the password.)
232/// Since I can't send 2 passwords at a time, it will only work with the sudo password
233/// It requires a path to establish CWD.
234pub fn execute_sudo_command_with_password<S, P>(
235    args: &[S],
236    password: &str,
237    path: P,
238) -> Result<(bool, String, String)>
239where
240    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
241    P: AsRef<std::path::Path> + std::fmt::Debug,
242{
243    execute_sudo_command_inner(args, Some(password), Some(path))
244}
245
246/// Runs a passwordless sudo command.
247/// Returns stdout & stderr
248pub fn execute_sudo_command_passwordless<S>(args: &[S]) -> Result<(bool, String, String)>
249where
250    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
251{
252    execute_sudo_command_inner::<S, &str>(args, None, None)
253}
254
255fn execute_sudo_command_inner<S, P>(
256    args: &[S],
257    password: Option<&str>,
258    path: Option<P>,
259) -> Result<(bool, String, String)>
260where
261    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
262    P: AsRef<std::path::Path> + std::fmt::Debug,
263{
264    log_info!("running sudo {args:?}");
265    log_line!("running sudo command. {args:?}");
266    let child = match (password, path) {
267        (None, None) => new_sudo_command_passwordless(args)?,
268        (Some(password), Some(path)) => {
269            log_info!("CWD {path:?}");
270            let mut child = new_sudo_command_awaiting_password(args, path)?;
271            inject_password(password, &mut child)?;
272            log_info!("injected sudo password");
273            child
274        }
275        _ => bail!("Password and Path should be set together"),
276    };
277    run_and_output(child)
278}
279
280fn run_and_output(child: std::process::Child) -> Result<(bool, String, String)> {
281    let output = child.wait_with_output()?;
282    Ok((
283        output.status.success(),
284        String::from_utf8(output.stdout)?,
285        String::from_utf8(output.stderr)?,
286    ))
287}
288/// Spawn a sudo command which shouldn't require a password.
289/// The command is executed immediatly and we return an handle to it.
290fn new_sudo_command_passwordless<S>(args: &[S]) -> Result<std::process::Child>
291where
292    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
293{
294    Ok(Command::new("sudo")
295        .args(args)
296        .stdin(Stdio::null())
297        .stdout(Stdio::piped())
298        .stderr(Stdio::piped())
299        .spawn()?)
300}
301
302/// Runs `sudo -k` removing sudo privileges of current running instance.
303pub fn drop_sudo_privileges() -> Result<()> {
304    Command::new("sudo")
305        .arg("-k")
306        .stdin(Stdio::null())
307        .stdout(Stdio::null())
308        .stderr(Stdio::null())
309        .spawn()?;
310    Ok(())
311}
312
313/// Reset the sudo faillock to avoid being blocked from running sudo commands.
314/// Runs `faillock --user $USERNAME --reset`
315pub fn reset_sudo_faillock() -> Result<()> {
316    Command::new("faillock")
317        .arg("--user")
318        .arg(current_username()?)
319        .arg("--reset")
320        .stdin(Stdio::null())
321        .stdout(Stdio::null())
322        .stderr(Stdio::null())
323        .spawn()?;
324    Ok(())
325}
326
327/// Execute `sudo -S ls -l /root`, passing the password into `stdin`.
328/// It sets a sudo session which will be reset later.
329/// The password isn't reset. It's the responsability of the caller to reset the sudo password afterward.
330pub fn set_sudo_session(password: &mut PasswordHolder) -> Result<bool> {
331    let root_path = std::path::Path::new("/");
332    // sudo
333    let (success, _, _) = execute_sudo_command_with_password(
334        &["ls", "/root"],
335        password
336            .sudo()
337            .as_ref()
338            .context("sudo password isn't set")?,
339        root_path,
340    )?;
341    Ok(success)
342}
343
344/// Inject. the command in a spawned [`tokio::process::Command`]
345#[tokio::main]
346pub async fn inject_command(mut command: TokioCommand, injector: Injector<String>) {
347    let Ok(mut cmd) = command
348        .stdout(Stdio::piped()) // Can do the same for stderr
349        .spawn()
350    else {
351        log_info!("Cannot spawn command");
352        return;
353    };
354    let Some(stdout) = cmd.stdout.take() else {
355        log_info!("no stdout");
356        return;
357    };
358    let mut lines = TokioBufReader::new(stdout).lines();
359    while let Ok(opt_line) = lines.next_line().await {
360        let Some(line) = opt_line else {
361            break;
362        };
363        injector.push(line.clone(), |line, cols| {
364            cols[0] = line.as_str().into();
365        });
366    }
367}
368
369/// Creates the tokio greper for fuzzy finding.
370pub fn build_tokio_greper() -> Option<TokioCommand> {
371    let shell_command = if is_in_path(RG_EXECUTABLE) {
372        RG_EXECUTABLE
373    } else if is_in_path(GREP_EXECUTABLE) {
374        GREP_EXECUTABLE
375    } else {
376        return None;
377    };
378    let mut args: Vec<_> = shell_command.split_whitespace().collect();
379    if args.is_empty() {
380        return None;
381    }
382    let grep = args.remove(0);
383    let mut tokio_greper = TokioCommand::new(grep);
384    tokio_greper.args(&args);
385    Some(tokio_greper)
386}
387
388/// Executes a command in a new shell.
389pub fn execute_in_shell<P>(args: &[&str], path: P) -> Result<bool>
390where
391    P: AsRef<Path>,
392{
393    let shell = env::var("SHELL").unwrap_or_else(|_| "bash".to_string());
394    let mut command = Command::new(&shell);
395    command.current_dir(path);
396    if !args.is_empty() {
397        command.arg("-c").args(args);
398    }
399    log_info!("execute_in_shell: shell: {shell}, args: {args:?}");
400    let success = command.status()?.success();
401    if !success {
402        log_info!("Shell exited with non-zero status:");
403    }
404    Ok(success)
405}