atuin_scripts/
execution.rs

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