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 and stderr to /dev/null
114pub fn execute_and_capture_output_with_path<
115    S: AsRef<std::ffi::OsStr> + fmt::Debug,
116    P: AsRef<Path>,
117>(
118    exe: S,
119    path: P,
120    args: &[&str],
121) -> Result<String> {
122    log_info!("execute_and_capture_output_with_path. executable: {exe:?}, arguments: {args:?}",);
123    let output = Command::new(exe)
124        .args(args)
125        .current_dir(path)
126        .stdin(Stdio::null())
127        .stdout(Stdio::piped())
128        .stderr(Stdio::piped())
129        .output()?;
130    if output.status.success() {
131        Ok(String::from_utf8(output.stdout)?)
132    } else {
133        let err = String::from_utf8(output.stderr)?;
134        log_info!("{err}");
135        Err(anyhow!(
136            "execute_and_capture_output: command didn't finish properly: {err}",
137        ))
138    }
139}
140
141/// Execute a command with options in a fork.
142/// Wait for termination and return either `Ok(stdout)`.
143/// Branch stdin and stderr to /dev/null
144pub fn execute_and_capture_output_without_check<S>(exe: S, args: &[&str]) -> Result<String>
145where
146    S: AsRef<std::ffi::OsStr> + fmt::Debug,
147{
148    log_info!("execute_and_capture_output_without_check. executable: {exe:?}, arguments: {args:?}",);
149    let child = Command::new(exe)
150        .args(args)
151        .stdin(Stdio::null())
152        .stdout(Stdio::piped())
153        .stderr(Stdio::null())
154        .spawn()?;
155    let output = child.wait_with_output()?;
156    Ok(String::from_utf8(output.stdout)?)
157}
158
159/// Execute a command with some arguments, returns its output. stdin & stderr are branched to /dev/null.
160pub fn execute_and_output<S, I>(exe: S, args: I) -> Result<std::process::Output>
161where
162    S: AsRef<std::ffi::OsStr> + fmt::Debug,
163    I: IntoIterator<Item = S> + fmt::Debug,
164{
165    log_info!("execute_and_output. executable: {exe:?}, arguments: {args:?}",);
166    Ok(Command::new(exe)
167        .args(args)
168        .stdin(Stdio::null())
169        .stderr(Stdio::null())
170        .output()?)
171}
172
173/// Executes a command with some arguments, returns its output. Doesn't log the call.
174pub fn execute_and_output_no_log<S, I>(exe: S, args: I) -> Result<std::process::Output>
175where
176    S: AsRef<std::ffi::OsStr> + fmt::Debug,
177    I: IntoIterator<Item = S> + fmt::Debug,
178{
179    Ok(Command::new(exe).args(args).stdin(Stdio::null()).output()?)
180}
181
182/// Executes a command with `CLICOLOR_FORCE=1` and `COLORTERM=ansi` environement variables.
183/// Returns the output.
184/// Used to run & display commands which may returns colored text.
185pub fn execute_with_ansi_colors(args: &[String]) -> Result<std::process::Output> {
186    log_info!("execute. {args:?}");
187    log_line!("Executed {args:?}");
188    Ok(Command::new(&args[0])
189        .args(&args[1..])
190        .env("CLICOLOR_FORCE", "1")
191        .env("COLORTERM", "ansi")
192        .stdin(Stdio::null())
193        .stdout(Stdio::piped())
194        .stderr(Stdio::null())
195        .output()?)
196}
197
198/// Spawn a sudo command with stdin, stdout and stderr piped.
199/// sudo is run with -S argument to read the password from stdin
200/// Args are sent.
201/// CWD is set to `path`.
202/// No password is set yet.
203/// A password should be sent with `inject_password`.
204fn new_sudo_command_awaiting_password<S, P>(args: &[S], path: P) -> Result<std::process::Child>
205where
206    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
207    P: AsRef<std::path::Path> + std::fmt::Debug,
208{
209    Ok(Command::new("sudo")
210        .arg("-S")
211        .args(args)
212        .stdin(Stdio::piped())
213        .stdout(Stdio::piped())
214        .stderr(Stdio::piped())
215        .current_dir(path)
216        .spawn()?)
217}
218
219/// Send password to a sudo command through its stdin.
220fn inject_password(password: &str, child: &mut std::process::Child) -> Result<()> {
221    let child_stdin = child
222        .stdin
223        .as_mut()
224        .context("inject_password: couldn't open child stdin")?;
225    child_stdin.write_all(password.as_bytes())?;
226    child_stdin.write_all(b"\n")?;
227    Ok(())
228}
229
230/// run a sudo command requiring a password (generally to establish the password.)
231/// Since I can't send 2 passwords at a time, it will only work with the sudo password
232/// It requires a path to establish CWD.
233pub fn execute_sudo_command_with_password<S, P>(
234    args: &[S],
235    password: &str,
236    path: P,
237) -> Result<(bool, String, String)>
238where
239    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
240    P: AsRef<std::path::Path> + std::fmt::Debug,
241{
242    execute_sudo_command_inner(args, Some(password), Some(path))
243}
244
245/// Runs a passwordless sudo command.
246/// Returns stdout & stderr
247pub fn execute_sudo_command_passwordless<S>(args: &[S]) -> Result<(bool, String, String)>
248where
249    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
250{
251    execute_sudo_command_inner::<S, &str>(args, None, None)
252}
253
254fn execute_sudo_command_inner<S, P>(
255    args: &[S],
256    password: Option<&str>,
257    path: Option<P>,
258) -> Result<(bool, String, String)>
259where
260    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
261    P: AsRef<std::path::Path> + std::fmt::Debug,
262{
263    log_info!("running sudo {args:?}");
264    log_line!("running sudo command. {args:?}");
265    let child = match (password, path) {
266        (None, None) => new_sudo_command_passwordless(args)?,
267        (Some(password), Some(path)) => {
268            log_info!("CWD {path:?}");
269            let mut child = new_sudo_command_awaiting_password(args, path)?;
270            inject_password(password, &mut child)?;
271            log_info!("injected sudo password");
272            child
273        }
274        _ => bail!("Password and Path should be set together"),
275    };
276    run_and_output(child)
277}
278
279fn run_and_output(child: std::process::Child) -> Result<(bool, String, String)> {
280    let output = child.wait_with_output()?;
281    Ok((
282        output.status.success(),
283        String::from_utf8(output.stdout)?,
284        String::from_utf8(output.stderr)?,
285    ))
286}
287/// Spawn a sudo command which shouldn't require a password.
288/// The command is executed immediatly and we return an handle to it.
289fn new_sudo_command_passwordless<S>(args: &[S]) -> Result<std::process::Child>
290where
291    S: AsRef<std::ffi::OsStr> + std::fmt::Debug,
292{
293    Ok(Command::new("sudo")
294        .args(args)
295        .stdin(Stdio::null())
296        .stdout(Stdio::piped())
297        .stderr(Stdio::piped())
298        .spawn()?)
299}
300
301/// Runs `sudo -k` removing sudo privileges of current running instance.
302pub fn drop_sudo_privileges() -> Result<()> {
303    Command::new("sudo")
304        .arg("-k")
305        .stdin(Stdio::null())
306        .stdout(Stdio::null())
307        .stderr(Stdio::null())
308        .spawn()?;
309    Ok(())
310}
311
312/// Reset the sudo faillock to avoid being blocked from running sudo commands.
313/// Runs `faillock --user $USERNAME --reset`
314pub fn reset_sudo_faillock() -> Result<()> {
315    Command::new("faillock")
316        .arg("--user")
317        .arg(current_username()?)
318        .arg("--reset")
319        .stdin(Stdio::null())
320        .stdout(Stdio::null())
321        .stderr(Stdio::null())
322        .spawn()?;
323    Ok(())
324}
325
326/// Execute `sudo -S ls -l /root`, passing the password into `stdin`.
327/// It sets a sudo session which will be reset later.
328/// The password isn't reset. It's the responsability of the caller to reset the sudo password afterward.
329pub fn set_sudo_session(password: &mut PasswordHolder) -> Result<bool> {
330    let root_path = std::path::Path::new("/");
331    // sudo
332    let (success, _, _) = execute_sudo_command_with_password(
333        &["ls", "/root"],
334        password
335            .sudo()
336            .as_ref()
337            .context("sudo password isn't set")?,
338        root_path,
339    )?;
340    Ok(success)
341}
342
343/// Inject. the command in a spawned [`tokio::process::Command`]
344#[tokio::main]
345pub async fn inject_command(mut command: TokioCommand, injector: Injector<String>) {
346    let Ok(mut cmd) = command
347        .stdout(Stdio::piped()) // Can do the same for stderr
348        .spawn()
349    else {
350        log_info!("Cannot spawn command");
351        return;
352    };
353    let Some(stdout) = cmd.stdout.take() else {
354        log_info!("no stdout");
355        return;
356    };
357    let mut lines = TokioBufReader::new(stdout).lines();
358    while let Ok(opt_line) = lines.next_line().await {
359        let Some(line) = opt_line else {
360            break;
361        };
362        injector.push(line.clone(), |line, cols| {
363            cols[0] = line.as_str().into();
364        });
365    }
366}
367
368/// Creates the tokio greper for fuzzy finding.
369pub fn build_tokio_greper() -> Option<TokioCommand> {
370    let shell_command = if is_in_path(RG_EXECUTABLE) {
371        RG_EXECUTABLE
372    } else if is_in_path(GREP_EXECUTABLE) {
373        GREP_EXECUTABLE
374    } else {
375        return None;
376    };
377    let mut args: Vec<_> = shell_command.split_whitespace().collect();
378    if args.is_empty() {
379        return None;
380    }
381    let grep = args.remove(0);
382    let mut tokio_greper = TokioCommand::new(grep);
383    tokio_greper.args(&args);
384    Some(tokio_greper)
385}
386
387/// Executes a command in a new shell.
388pub fn execute_in_shell(args: &[&str]) -> Result<bool> {
389    let shell = env::var("SHELL").unwrap_or_else(|_| "bash".to_string());
390    let mut command = Command::new(&shell);
391    if !args.is_empty() {
392        command.arg("-c").args(args);
393    }
394    log_info!("execute_in_shell: shell: {shell}, args: {args:?}");
395    let success = command.status()?.success();
396    if !success {
397        log_info!("Shell exited with non-zero status:");
398    }
399    Ok(success)
400}