blackjack/
script.rs

1// Copyright 2024 Ole Kliemann
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::error::{Error, Result};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::process::{ExitStatus, Stdio};
8use tempfile::NamedTempFile;
9use tokio::fs;
10use tokio::io::{AsyncBufReadExt, BufReader};
11use tokio::process::Command;
12use colored::Colorize;
13
14pub async fn execute_script(
15    command_line: &str,
16    wd: PathBuf,
17    env: &mut HashMap<String, String>,
18) -> Result<(ExitStatus, String, String)> {
19    let env_file = NamedTempFile::new()?;
20    let env_file_path = env_file.path().to_owned();
21    let shell_command = format!(
22        ". {} && env -0 > {}",
23        command_line,
24        env_file_path.display()
25    );
26
27    let mut child = Command::new("sh")
28        .arg("-c")
29        .arg(shell_command)
30        .stdout(Stdio::piped())
31        .stderr(Stdio::piped())
32        .current_dir(wd)
33        .envs(env.clone())
34        .spawn()?;
35
36    let stdout = child
37        .stdout
38        .take()
39        .ok_or(Error::Other("unable to capture script stdout".to_string()))?;
40    let stderr = child
41        .stderr
42        .take()
43        .ok_or(Error::Other("unable to capture script stderr".to_string()))?;
44
45    let stdout_future: tokio::task::JoinHandle<std::result::Result<_, futures::io::Error>> =
46        tokio::spawn(async move {
47            let mut buf = BufReader::new(stdout);
48            let mut result: Vec<String> = Vec::new();
49            loop {
50                let mut s = String::new();
51                let size = buf.read_line(&mut s).await?;
52                if size == 0 {
53                    break Ok(result);
54                }
55                log::info!("{}", s.strip_suffix("\n").or(Some(&s)).unwrap().dimmed());
56                result.push(s);
57            }
58        });
59    let stderr_future: tokio::task::JoinHandle<std::result::Result<_, futures::io::Error>> =
60        tokio::spawn(async move {
61            let mut buf = BufReader::new(stderr);
62            let mut result: Vec<String> = Vec::new();
63            loop {
64                let mut s = String::new();
65                let size = buf.read_line(&mut s).await?;
66                if size == 0 {
67                    break Ok(result);
68                }
69                log::info!("{}", s.strip_suffix("\n").or(Some(&s)).unwrap().dimmed());
70                result.push(s);
71            }
72        });
73
74    let status = child.wait().await?;
75    let stdout_result = stdout_future.await??;
76    let stderr_result = stderr_future.await??;
77
78    let env_contents = fs::read_to_string(env_file_path).await?;
79    for line in env_contents.split('\0') {
80        log::trace!("captured env: {line}");
81        if let Some(eq_pos) = line.find('=') {
82            let var_name = &line[..eq_pos];
83            if var_name.starts_with("BLACKJACK_") {
84                let value = &line[eq_pos + 1..];
85                let value = value.trim_matches('\'');
86                env.insert(var_name.to_string(), value.to_string());
87            }
88        }
89    }
90    log::trace!("exit code: {status}");
91
92    Ok((status, stdout_result.join("\n"), stderr_result.join("\n")))
93}