atuin_scripts/
execution.rs

1use crate::store::script::Script;
2use eyre::Result;
3use std::collections::{HashMap, HashSet};
4use std::process::Stdio;
5use tempfile::NamedTempFile;
6use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
7use tokio::sync::mpsc;
8use tokio::task;
9use tracing::debug;
10
11// Helper function to build a complete script with shebang
12pub fn build_executable_script(script: String, shebang: String) -> String {
13    if shebang.is_empty() {
14        // Default to bash if no shebang is provided
15        format!("#!/usr/bin/env bash\n{script}")
16    } else if script.starts_with("#!") {
17        format!("{shebang}\n{script}")
18    } else {
19        format!("#!{shebang}\n{script}")
20    }
21}
22
23/// Represents the communication channels for an interactive script
24pub struct ScriptSession {
25    /// Channel to send input to the script
26    pub stdin_tx: mpsc::Sender<String>,
27    /// Exit code of the process once it completes
28    pub exit_code_rx: mpsc::Receiver<i32>,
29}
30
31impl ScriptSession {
32    /// Send input to the running script
33    pub async fn send_input(&self, input: String) -> Result<(), mpsc::error::SendError<String>> {
34        self.stdin_tx.send(input).await
35    }
36
37    /// Wait for the script to complete and get the exit code
38    pub async fn wait_for_exit(&mut self) -> Option<i32> {
39        self.exit_code_rx.recv().await
40    }
41}
42
43fn setup_template(script: &Script) -> Result<minijinja::Environment<'_>> {
44    let mut env = minijinja::Environment::new();
45    env.set_trim_blocks(true);
46    env.add_template("script", script.script.as_str())?;
47
48    Ok(env)
49}
50
51/// Template a script with the given context
52pub fn template_script(
53    script: &Script,
54    context: &HashMap<String, serde_json::Value>,
55) -> Result<String> {
56    let env = setup_template(script)?;
57    let template = env.get_template("script")?;
58    let rendered = template.render(context)?;
59
60    Ok(rendered)
61}
62
63/// Get the variables that need to be templated in a script
64pub fn template_variables(script: &Script) -> Result<HashSet<String>> {
65    let env = setup_template(script)?;
66    let template = env.get_template("script")?;
67
68    Ok(template.undeclared_variables(true))
69}
70
71/// Execute a script interactively, allowing for ongoing stdin/stdout interaction
72pub async fn execute_script_interactive(
73    script: String,
74    shebang: String,
75) -> Result<ScriptSession, Box<dyn std::error::Error + Send + Sync>> {
76    // Create a temporary file for the script
77    let temp_file = NamedTempFile::new()?;
78    let temp_path = temp_file.path().to_path_buf();
79
80    debug!("creating temp file at {}", temp_path.display());
81
82    // Extract interpreter from shebang for fallback execution
83    let interpreter = if !shebang.is_empty() {
84        shebang.trim_start_matches("#!").trim().to_string()
85    } else {
86        "/usr/bin/env bash".to_string()
87    };
88
89    // Write script content to the temp file, including the shebang
90    let full_script_content = build_executable_script(script.clone(), shebang.clone());
91
92    debug!("writing script content to temp file");
93    tokio::fs::write(&temp_path, &full_script_content).await?;
94
95    // Make it executable on Unix systems
96    #[cfg(unix)]
97    {
98        debug!("making script executable");
99        use std::os::unix::fs::PermissionsExt;
100        let mut perms = std::fs::metadata(&temp_path)?.permissions();
101        perms.set_mode(0o755);
102        std::fs::set_permissions(&temp_path, perms)?;
103    }
104
105    // Store the temp_file to prevent it from being dropped
106    // This ensures it won't be deleted while the script is running
107    let _keep_temp_file = temp_file;
108
109    debug!("attempting direct script execution");
110    let mut child_result = tokio::process::Command::new(temp_path.to_str().unwrap())
111        .stdin(Stdio::piped())
112        .stdout(Stdio::piped())
113        .stderr(Stdio::piped())
114        .spawn();
115
116    // If direct execution fails, try using the interpreter
117    if let Err(e) = &child_result {
118        debug!("direct execution failed: {}, trying with interpreter", e);
119
120        // When falling back to interpreter, remove the shebang from the file
121        // Some interpreters don't handle scripts with shebangs well
122        debug!("writing script content without shebang for interpreter execution");
123        tokio::fs::write(&temp_path, &script).await?;
124
125        // Parse the interpreter command
126        let parts: Vec<&str> = interpreter.split_whitespace().collect();
127        if !parts.is_empty() {
128            let mut cmd = tokio::process::Command::new(parts[0]);
129
130            // Add any interpreter args
131            for i in parts.iter().skip(1) {
132                cmd.arg(i);
133            }
134
135            // Add the script path
136            cmd.arg(temp_path.to_str().unwrap());
137
138            // Try with the interpreter
139            child_result = cmd
140                .stdin(Stdio::piped())
141                .stdout(Stdio::piped())
142                .stderr(Stdio::piped())
143                .spawn();
144        }
145    }
146
147    // If it still fails, return the error
148    let mut child = match child_result {
149        Ok(child) => child,
150        Err(e) => {
151            return Err(format!("Failed to execute script: {e}").into());
152        }
153    };
154
155    // Get handles to stdin, stdout, stderr
156    let mut stdin = child
157        .stdin
158        .take()
159        .ok_or_else(|| "Failed to open child process stdin".to_string())?;
160    let stdout = child
161        .stdout
162        .take()
163        .ok_or_else(|| "Failed to open child process stdout".to_string())?;
164    let stderr = child
165        .stderr
166        .take()
167        .ok_or_else(|| "Failed to open child process stderr".to_string())?;
168
169    // Create channels for the interactive session
170    let (stdin_tx, mut stdin_rx) = mpsc::channel::<String>(32);
171    let (exit_code_tx, exit_code_rx) = mpsc::channel::<i32>(1);
172
173    // handle user stdin
174    debug!("spawning stdin handler");
175    tokio::spawn(async move {
176        while let Some(input) = stdin_rx.recv().await {
177            if let Err(e) = stdin.write_all(input.as_bytes()).await {
178                eprintln!("Error writing to stdin: {e}");
179                break;
180            }
181            if let Err(e) = stdin.flush().await {
182                eprintln!("Error flushing stdin: {e}");
183                break;
184            }
185        }
186        // when the channel closes (sender dropped), we let stdin close naturally
187    });
188
189    // handle stdout
190    debug!("spawning stdout handler");
191    let stdout_handle = task::spawn(async move {
192        let mut stdout_reader = BufReader::new(stdout);
193        let mut buffer = [0u8; 1024];
194        let mut stdout_writer = tokio::io::stdout();
195
196        loop {
197            match stdout_reader.read(&mut buffer).await {
198                Ok(0) => break, // End of stdout
199                Ok(n) => {
200                    if let Err(e) = stdout_writer.write_all(&buffer[0..n]).await {
201                        eprintln!("Error writing to stdout: {e}");
202                        break;
203                    }
204                    if let Err(e) = stdout_writer.flush().await {
205                        eprintln!("Error flushing stdout: {e}");
206                        break;
207                    }
208                }
209                Err(e) => {
210                    eprintln!("Error reading from process stdout: {e}");
211                    break;
212                }
213            }
214        }
215    });
216
217    // Process stderr in a separate task
218    debug!("spawning stderr handler");
219    let stderr_handle = task::spawn(async move {
220        let mut stderr_reader = BufReader::new(stderr);
221        let mut buffer = [0u8; 1024];
222        let mut stderr_writer = tokio::io::stderr();
223
224        loop {
225            match stderr_reader.read(&mut buffer).await {
226                Ok(0) => break, // End of stderr
227                Ok(n) => {
228                    if let Err(e) = stderr_writer.write_all(&buffer[0..n]).await {
229                        eprintln!("Error writing to stderr: {e}");
230                        break;
231                    }
232                    if let Err(e) = stderr_writer.flush().await {
233                        eprintln!("Error flushing stderr: {e}");
234                        break;
235                    }
236                }
237                Err(e) => {
238                    eprintln!("Error reading from process stderr: {e}");
239                    break;
240                }
241            }
242        }
243    });
244
245    // Spawn a task to wait for the child process to complete
246    debug!("spawning exit code handler");
247    let _keep_temp_file_clone = _keep_temp_file;
248    tokio::spawn(async move {
249        // Keep the temp file alive until the process completes
250        let _temp_file_ref = _keep_temp_file_clone;
251
252        // Wait for the child process to complete
253        let status = match child.wait().await {
254            Ok(status) => {
255                debug!("Process exited with status: {:?}", status);
256                status
257            }
258            Err(e) => {
259                eprintln!("Error waiting for child process: {e}");
260                // Send a default error code
261                let _ = exit_code_tx.send(-1).await;
262                return;
263            }
264        };
265
266        // Wait for stdout/stderr tasks to complete
267        if let Err(e) = stdout_handle.await {
268            eprintln!("Error joining stdout task: {e}");
269        }
270
271        if let Err(e) = stderr_handle.await {
272            eprintln!("Error joining stderr task: {e}");
273        }
274
275        // Send the exit code
276        let exit_code = status.code().unwrap_or(-1);
277        debug!("Sending exit code: {}", exit_code);
278        let _ = exit_code_tx.send(exit_code).await;
279    });
280
281    // Return the communication channels as a ScriptSession
282    Ok(ScriptSession {
283        stdin_tx,
284        exit_code_rx,
285    })
286}