atuin_scripts/
execution.rs1use 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
12pub fn build_executable_script(script: String, shebang: String) -> String {
14 if shebang.is_empty() {
15 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
24pub struct ScriptSession {
26 pub stdin_tx: mpsc::Sender<String>,
28 pub exit_code_rx: mpsc::Receiver<i32>,
30}
31
32impl ScriptSession {
33 pub async fn send_input(&self, input: String) -> Result<(), mpsc::error::SendError<String>> {
35 self.stdin_tx.send(input).await
36 }
37
38 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
52pub 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
64pub 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
72pub async fn execute_script_interactive(
74 script: String,
75 shebang: String,
76) -> Result<ScriptSession, Box<dyn std::error::Error + Send + Sync>> {
77 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 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 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 #[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 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 let Err(e) = &child_result {
119 debug!("direct execution failed: {}, trying with interpreter", e);
120
121 debug!("writing script content without shebang for interpreter execution");
124 tokio::fs::write(&temp_path, &script).await?;
125
126 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 for i in parts.iter().skip(1) {
133 cmd.arg(i);
134 }
135
136 cmd.arg(temp_path.to_str().unwrap());
138
139 child_result = cmd
141 .stdin(Stdio::piped())
142 .stdout(Stdio::piped())
143 .stderr(Stdio::piped())
144 .spawn();
145 }
146 }
147
148 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 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 let (stdin_tx, mut stdin_rx) = mpsc::channel::<String>(32);
172 let (exit_code_tx, exit_code_rx) = mpsc::channel::<i32>(1);
173
174 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 });
189
190 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, 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 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, 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 debug!("spawning exit code handler");
248 let _keep_temp_file_clone = _keep_temp_file;
249 tokio::spawn(async move {
250 let _temp_file_ref = _keep_temp_file_clone;
252
253 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 let _ = exit_code_tx.send(-1).await;
263 return;
264 }
265 };
266
267 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 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 Ok(ScriptSession {
284 stdin_tx,
285 exit_code_rx,
286 })
287}