1use anstyle::{AnsiColor as TermAnsiColor, Style as TermStyle};
2use std::io::{self, Write};
3use std::process::ExitCode;
4use std::sync::mpsc;
5use std::thread;
6
7use crate::error::{command_block_failed, command_block_start_failed, command_failed};
8use crate::interpolate::interpolate;
9use crate::process::{OutputChunk, OutputStream};
10use crate::shell::{run_command, run_command_inherit};
11use crate::{EngineError, ExecutionNode, ExecutionPlan};
12
13pub fn run_plan(plan: &ExecutionPlan) -> Result<ExitCode, EngineError> {
21 run_plan_with_options(plan, RuntimeOptions::default())
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub struct RuntimeOptions {
33 pub quiet: bool,
34}
35
36pub fn run_plan_with_options(
45 plan: &ExecutionPlan,
46 options: RuntimeOptions,
47) -> Result<ExitCode, EngineError> {
48 let total_tasks = plan.nodes.len();
49 let mut task_index = 0usize;
50
51 while task_index < total_tasks {
52 let stage = plan.nodes[task_index].stage;
53 let stage_start = task_index;
54 while task_index < total_tasks && plan.nodes[task_index].stage == stage {
55 task_index += 1;
56 }
57 let stage_nodes = &plan.nodes[stage_start..task_index];
58
59 if !options.quiet {
60 for (offset, node) in stage_nodes.iter().enumerate() {
61 eprintln!(
62 "{}",
63 render_task_progress(stage_start + offset + 1, total_tasks, &node.name)
64 );
65 }
66 }
67
68 if stage_nodes.len() == 1 {
69 run_node_inherit(&stage_nodes[0], &plan.working_dir, plan.shell.as_deref())?;
70 } else {
71 execute_stage(stage_nodes, &plan.working_dir, plan.shell.as_deref())?;
72 }
73 }
74
75 Ok(ExitCode::SUCCESS)
76}
77
78fn execute_stage(
79 stage_nodes: &[ExecutionNode],
80 working_dir: &std::path::Path,
81 default_shell: Option<&str>,
82) -> Result<(), EngineError> {
83 let stage_len = stage_nodes.len();
84 let (event_tx, event_rx) = mpsc::channel::<StageEvent>();
85
86 thread::scope(|scope| {
87 let mut handles = Vec::new();
88
89 for (index, node) in stage_nodes.iter().cloned().enumerate() {
90 let working_dir = working_dir.to_path_buf();
91 let shell = default_shell.map(str::to_string);
92 let event_tx = event_tx.clone();
93 handles.push(
94 scope.spawn(move || {
95 run_node(index, &node, &working_dir, shell.as_deref(), event_tx)
96 }),
97 );
98 }
99 drop(event_tx);
100
101 run_ordered_output_event_loop(&event_rx, stage_len)?;
102
103 collect_thread_results(handles)
104 })
105}
106
107fn run_ordered_output_event_loop(
108 event_rx: &mpsc::Receiver<StageEvent>,
109 stage_len: usize,
110) -> Result<(), EngineError> {
111 let mut buffers = vec![Vec::<OutputChunk>::new(); stage_len];
112 let mut finished = vec![false; stage_len];
113 let mut task_errors = (0..stage_len)
114 .map(|_| None)
115 .collect::<Vec<Option<EngineError>>>();
116 let mut current_index = 0usize;
117 let mut finished_count = 0usize;
118 let mut first_error = None;
119
120 while finished_count < stage_len {
121 match event_rx.recv() {
122 Ok(StageEvent::Output { task_index, chunk }) => {
123 if task_index == current_index {
124 print_output_chunk(&chunk)?;
125 } else {
126 buffers[task_index].push(chunk);
127 }
128 }
129 Ok(StageEvent::Finished { task_index, error }) => {
130 finished[task_index] = true;
131 task_errors[task_index] = error;
132 finished_count += 1;
133
134 while current_index < stage_len {
135 flush_task_buffer(&mut buffers[current_index])?;
136 if !finished[current_index] {
137 break;
138 }
139 if first_error.is_none() {
140 first_error = task_errors[current_index].take();
141 }
142 current_index += 1;
143 }
144 }
145 Err(_) => break,
146 }
147 }
148
149 match first_error {
150 Some(error) => Err(error),
151 None => Ok(()),
152 }
153}
154
155fn collect_thread_results(
156 handles: Vec<thread::ScopedJoinHandle<'_, Result<(), EngineError>>>,
157) -> Result<(), EngineError> {
158 let mut first_error = None;
159 for handle in handles {
160 match handle.join() {
161 Ok(Ok(())) => {}
162 Ok(Err(error)) => {
163 if first_error.is_none() {
164 first_error = Some(error);
165 }
166 }
167 Err(payload) => std::panic::resume_unwind(payload),
168 }
169 }
170 match first_error {
171 Some(error) => Err(error),
172 None => Ok(()),
173 }
174}
175
176fn run_node(
177 task_index: usize,
178 node: &ExecutionNode,
179 working_dir: &std::path::Path,
180 default_shell: Option<&str>,
181 event_tx: mpsc::Sender<StageEvent>,
182) -> Result<(), EngineError> {
183 let (output_tx, output_rx) = mpsc::channel::<OutputChunk>();
184 let forwarder = thread::spawn({
185 let event_tx = event_tx.clone();
186 move || -> Result<(), EngineError> {
187 while let Ok(chunk) = output_rx.recv() {
188 event_tx
189 .send(StageEvent::Output { task_index, chunk })
190 .map_err(|_| {
191 EngineError::Runtime("failed to forward task output".to_string())
192 })?;
193 }
194 Ok(())
195 }
196 });
197
198 let total_steps = node.steps.len();
199 let mut task_error = None;
200
201 for (index, step) in node.steps.iter().enumerate() {
202 let rendered = match interpolate(step.source(), &node.params) {
203 Ok(rendered) => rendered,
204 Err(error) => {
205 task_error = Some(error);
206 break;
207 }
208 };
209
210 let shell = node.shell.as_deref().or(default_shell).unwrap_or("deno");
211 let code = match run_command(
212 &rendered,
213 working_dir,
214 shell,
215 node.shell_fallback,
216 output_tx.clone(),
217 ) {
218 Ok(code) => code,
219 Err(error) => {
220 task_error = Some(if step.is_block() {
221 command_block_start_failed(shell, error)
222 } else {
223 error
224 });
225 break;
226 }
227 };
228
229 if code != ExitCode::SUCCESS {
230 task_error = Some(if step.is_block() {
231 command_block_failed(&node.name, index + 1, total_steps, code)
232 } else {
233 command_failed(&node.name, index + 1, total_steps, &rendered, code)
234 });
235 break;
236 }
237 }
238
239 drop(output_tx);
240 match forwarder.join() {
241 Ok(Ok(())) => {}
242 Ok(Err(error)) => {
243 if task_error.is_none() {
244 task_error = Some(error);
245 }
246 }
247 Err(_) => {
248 if task_error.is_none() {
249 task_error = Some(EngineError::Runtime(
250 "task output forwarder thread panicked".to_string(),
251 ));
252 }
253 }
254 }
255
256 event_tx
257 .send(StageEvent::Finished {
258 task_index,
259 error: task_error,
260 })
261 .map_err(|_| EngineError::Runtime("failed to finalize task output".to_string()))?;
262 Ok(())
263}
264
265fn run_node_inherit(
266 node: &ExecutionNode,
267 working_dir: &std::path::Path,
268 default_shell: Option<&str>,
269) -> Result<(), EngineError> {
270 let total_steps = node.steps.len();
271
272 for (index, step) in node.steps.iter().enumerate() {
273 let rendered = interpolate(step.source(), &node.params)?;
274 let shell = node.shell.as_deref().or(default_shell).unwrap_or("deno");
275 let code = run_command_inherit(&rendered, working_dir, shell, node.shell_fallback)
276 .map_err(|error| {
277 if step.is_block() {
278 command_block_start_failed(shell, error)
279 } else {
280 error
281 }
282 })?;
283
284 if code != ExitCode::SUCCESS {
285 return Err(if step.is_block() {
286 command_block_failed(&node.name, index + 1, total_steps, code)
287 } else {
288 command_failed(&node.name, index + 1, total_steps, &rendered, code)
289 });
290 }
291 }
292
293 Ok(())
294}
295
296#[derive(Debug)]
297enum StageEvent {
298 Output {
299 task_index: usize,
300 chunk: OutputChunk,
301 },
302 Finished {
303 task_index: usize,
304 error: Option<EngineError>,
305 },
306}
307
308fn flush_task_buffer(buffer: &mut Vec<OutputChunk>) -> Result<(), EngineError> {
309 for chunk in buffer.drain(..) {
310 print_output_chunk(&chunk)?;
311 }
312 Ok(())
313}
314
315fn print_output_chunk(chunk: &OutputChunk) -> Result<(), EngineError> {
316 match chunk.stream {
317 OutputStream::Stdout => write_output(&chunk.text, io::stdout()),
318 OutputStream::Stderr => write_output(&chunk.text, io::stderr()),
319 }
320}
321
322fn write_output(content: &str, mut writer: impl Write) -> Result<(), EngineError> {
323 write!(writer, "{content}")
324 .map_err(|error| EngineError::Runtime(format!("failed to write task output: {error}")))?;
325
326 writer
327 .flush()
328 .map_err(|error| EngineError::Runtime(format!("failed to flush task output: {error}")))?;
329 Ok(())
330}
331
332fn render_task_progress(task_index: usize, total_tasks: usize, task_name: &str) -> String {
333 let label_style = TermStyle::new()
334 .fg_color(Some(TermAnsiColor::BrightGreen.into()))
335 .bold();
336 let task_style = TermStyle::new()
337 .fg_color(Some(TermAnsiColor::BrightCyan.into()))
338 .bold();
339
340 format!(
341 "{}[{}/{}]{} {}{}{}",
342 label_style.render(),
343 task_index,
344 total_tasks,
345 label_style.render_reset(),
346 task_style.render(),
347 task_name,
348 task_style.render_reset()
349 )
350}