like_shell/
lib.rs

1use std::ffi::OsString;
2use std::io::ErrorKind;
3use std::os::unix::ffi::OsStringExt;
4use std::pin::Pin;
5use std::process::Stdio;
6use std::sync::Arc;
7use std::{env::set_current_dir, path::Path};
8
9use anyhow::bail;
10use fs_extra::dir::CopyOptions;
11// use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
12// use nix::unistd::Pid;
13use tempdir::TempDir;
14use tokio::io::{AsyncRead, AsyncReadExt};
15use tokio::process::Command;
16use tokio::sync::Mutex;
17
18/// Create a temporary directory filled with a copy of `source_dir`.
19pub fn temp_dir_from_template(source_dir: &Path) -> Result<TempDir, Box<dyn std::error::Error>> {
20    let temp_dir = TempDir::new("test")?;
21    let options = CopyOptions::new().content_only(true); // FIXME: What should be `copy_inside` value?
22    fs_extra::dir::copy(source_dir, temp_dir.path(), &options)?;
23    set_current_dir(temp_dir.path())?;
24    Ok(temp_dir)
25}
26
27/// Child process (or process group) that runs till the object is dropped.
28pub struct TemporaryChild {
29    // child: Child,
30}
31
32pub struct Capture {
33    pub stdout: Option<Arc<Mutex<String>>>,
34    pub stderr: Option<Arc<Mutex<String>>>,
35}
36
37impl TemporaryChild {
38    /// Spawn child process with optional capturing its output.
39    pub async fn spawn(cmd: &mut Command, capture: Capture) -> std::io::Result<Self> {
40        if capture.stdout.is_some() {
41            cmd.stdout(Stdio::piped());
42        }
43
44        if capture.stderr.is_some() {
45            cmd.stderr(Stdio::piped());
46        }
47
48        // unsafe {
49        //     cmd.pre_exec(|| {
50        //         libc::setpgid(0, 0);
51        //         Ok(())
52        //     });
53        // }
54
55        let mut child = cmd.kill_on_drop(true).spawn()?;
56
57        // Threads terminate, when the child exits.
58
59        if let Some(capture_stdout) = capture.stdout {
60            let stdout = child.stdout.take().unwrap();
61            spawn_dump_to_string(Box::pin(stdout), capture_stdout).await;
62        }
63
64        if let Some(capture_stderr) = capture.stderr {
65            let stderr = child.stderr.take().unwrap();
66            spawn_dump_to_string(Box::pin(stderr), capture_stderr).await;
67        }
68
69        Ok(TemporaryChild { /*child*/ })
70    }
71}
72
73async fn spawn_dump_to_string(
74    mut stream: Pin<Box<dyn AsyncRead + Send + Sync>>, // Box<dyn std::io::Read + Send + Sync>,
75    string: Arc<Mutex<String>>,
76) {
77    tokio::spawn(async move {
78        let mut buf = [0; 4096];
79        loop {
80            let r = stream.read(&mut buf).await;
81            match r {
82                Err(err) => {
83                    if err.kind() == ErrorKind::UnexpectedEof {
84                        return; // terminate the thread.
85                    } else {
86                        panic!("Error in reading child output: {}", err);
87                    }
88                }
89                Ok(r) => {
90                    let s = OsString::from_vec(Vec::from(&buf[..r]));
91                    string
92                        .lock()
93                        .await
94                        .push_str(s.to_str().expect("Wrong text encoding in child output"));
95                }
96            }
97        }
98    });
99}
100
101// impl Drop for TemporaryChild {
102//     fn drop(&mut self) {
103//         if let Some(id) = self.child.id() {
104//             // Get the process group ID of the child process
105//             let pid = -(id as i32); // Negative PID targets process group
106
107//             unsafe {
108//                 libc::kill(pid, libc::SIGTERM);
109//             } // Send SIGTERM to the group
110
111//             // Wait for the child process to exit
112//             loop {
113//                 match waitpid(Pid::from_raw(id as i32), Some(WaitPidFlag::WNOHANG)) {
114//                     Ok(WaitStatus::Exited(_, _)) | Ok(WaitStatus::Signaled(_, _, _)) => break,
115//                     Ok(_) => continue,
116//                     Err(_) => break,
117//                 }
118//             }
119//         }
120//     }
121// }
122
123pub async fn run_successful_command(cmd: &mut Command) -> anyhow::Result<()> {
124    let status = cmd.status().await?;
125    if !status.success() {
126        match status.code() {
127            Some(code) => bail!("Command failed with exit code: {}.", code),
128            None => bail!("Process terminated by a signal."),
129        }
130    }
131    Ok(())
132}
133
134pub async fn run_failed_command(cmd: &mut Command) -> anyhow::Result<()> {
135    let status = cmd.status().await?;
136    if status.success() {
137        bail!("Command succeeded though should have failed.");
138    }
139    Ok(())
140}