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_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_commands = node.commands.len();
199    let mut task_error = None;
200
201    for (index, command) in node.commands.iter().enumerate() {
202        let rendered = match interpolate(command, &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(error);
221                break;
222            }
223        };
224
225        if code != ExitCode::SUCCESS {
226            task_error = Some(command_failed(
227                &node.name,
228                index + 1,
229                total_commands,
230                &rendered,
231                code,
232            ));
233            break;
234        }
235    }
236
237    drop(output_tx);
238    match forwarder.join() {
239        Ok(Ok(())) => {}
240        Ok(Err(error)) => {
241            if task_error.is_none() {
242                task_error = Some(error);
243            }
244        }
245        Err(_) => {
246            if task_error.is_none() {
247                task_error = Some(EngineError::Runtime(
248                    "task output forwarder thread panicked".to_string(),
249                ));
250            }
251        }
252    }
253
254    event_tx
255        .send(StageEvent::Finished {
256            task_index,
257            error: task_error,
258        })
259        .map_err(|_| EngineError::Runtime("failed to finalize task output".to_string()))?;
260    Ok(())
261}
262
263fn run_node_inherit(
264    node: &ExecutionNode,
265    working_dir: &std::path::Path,
266    default_shell: Option<&str>,
267) -> Result<(), EngineError> {
268    let total_commands = node.commands.len();
269
270    for (index, command) in node.commands.iter().enumerate() {
271        let rendered = interpolate(command, &node.params)?;
272        let shell = node.shell.as_deref().or(default_shell).unwrap_or("deno");
273        let code = run_command_inherit(&rendered, working_dir, shell, node.shell_fallback)?;
274
275        if code != ExitCode::SUCCESS {
276            return Err(command_failed(
277                &node.name,
278                index + 1,
279                total_commands,
280                &rendered,
281                code,
282            ));
283        }
284    }
285
286    Ok(())
287}
288
289#[derive(Debug)]
290enum StageEvent {
291    Output {
292        task_index: usize,
293        chunk: OutputChunk,
294    },
295    Finished {
296        task_index: usize,
297        error: Option<EngineError>,
298    },
299}
300
301fn flush_task_buffer(buffer: &mut Vec<OutputChunk>) -> Result<(), EngineError> {
302    for chunk in buffer.drain(..) {
303        print_output_chunk(&chunk)?;
304    }
305    Ok(())
306}
307
308fn print_output_chunk(chunk: &OutputChunk) -> Result<(), EngineError> {
309    match chunk.stream {
310        OutputStream::Stdout => write_output(&chunk.text, io::stdout()),
311        OutputStream::Stderr => write_output(&chunk.text, io::stderr()),
312    }
313}
314
315fn write_output(content: &str, mut writer: impl Write) -> Result<(), EngineError> {
316    write!(writer, "{content}")
317        .map_err(|error| EngineError::Runtime(format!("failed to write task output: {error}")))?;
318
319    writer
320        .flush()
321        .map_err(|error| EngineError::Runtime(format!("failed to flush task output: {error}")))?;
322    Ok(())
323}
324
325fn render_task_progress(task_index: usize, total_tasks: usize, task_name: &str) -> String {
326    let label_style = TermStyle::new()
327        .fg_color(Some(TermAnsiColor::BrightGreen.into()))
328        .bold();
329    let task_style = TermStyle::new()
330        .fg_color(Some(TermAnsiColor::BrightCyan.into()))
331        .bold();
332
333    format!(
334        "{}[{}/{}]{} {}{}{}",
335        label_style.render(),
336        task_index,
337        total_tasks,
338        label_style.render_reset(),
339        task_style.render(),
340        task_name,
341        task_style.render_reset()
342    )
343}