atuin_scripts/
execution.rs1use 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
11pub fn build_executable_script(script: String, shebang: String) -> String {
13 if shebang.is_empty() {
14 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
23pub struct ScriptSession {
25 pub stdin_tx: mpsc::Sender<String>,
27 pub exit_code_rx: mpsc::Receiver<i32>,
29}
30
31impl ScriptSession {
32 pub async fn send_input(&self, input: String) -> Result<(), mpsc::error::SendError<String>> {
34 self.stdin_tx.send(input).await
35 }
36
37 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
51pub 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
63pub 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
71pub async fn execute_script_interactive(
73 script: String,
74 shebang: String,
75) -> Result<ScriptSession, Box<dyn std::error::Error + Send + Sync>> {
76 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 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 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 #[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 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 let Err(e) = &child_result {
118 debug!("direct execution failed: {}, trying with interpreter", e);
119
120 debug!("writing script content without shebang for interpreter execution");
123 tokio::fs::write(&temp_path, &script).await?;
124
125 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 for i in parts.iter().skip(1) {
132 cmd.arg(i);
133 }
134
135 cmd.arg(temp_path.to_str().unwrap());
137
138 child_result = cmd
140 .stdin(Stdio::piped())
141 .stdout(Stdio::piped())
142 .stderr(Stdio::piped())
143 .spawn();
144 }
145 }
146
147 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 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 let (stdin_tx, mut stdin_rx) = mpsc::channel::<String>(32);
171 let (exit_code_tx, exit_code_rx) = mpsc::channel::<i32>(1);
172
173 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 });
188
189 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, 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 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, 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 debug!("spawning exit code handler");
247 let _keep_temp_file_clone = _keep_temp_file;
248 tokio::spawn(async move {
249 let _temp_file_ref = _keep_temp_file_clone;
251
252 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 let _ = exit_code_tx.send(-1).await;
262 return;
263 }
264 };
265
266 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 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 Ok(ScriptSession {
283 stdin_tx,
284 exit_code_rx,
285 })
286}