Skip to main content

only_engine/
runtime.rs

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
13/// Runs a pre-built execution plan.
14///
15/// Args:
16/// plan: Dependency-expanded execution plan.
17///
18/// Returns:
19/// Success when all execution nodes complete successfully.
20pub fn run_plan(plan: &ExecutionPlan) -> Result<ExitCode, EngineError> {
21    run_plan_with_options(plan, RuntimeOptions::default())
22}
23
24/// Runtime options that affect only the task host, not command semantics.
25///
26/// Args:
27/// None.
28///
29/// Returns:
30/// Host display options used while executing an already-built plan.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
32pub struct RuntimeOptions {
33    pub quiet: bool,
34}
35
36/// Runs a pre-built execution plan with host display options.
37///
38/// Args:
39/// plan: Dependency-expanded execution plan.
40/// options: Host-side display options.
41///
42/// Returns:
43/// Success when all execution nodes complete successfully.
44pub 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}