use std::collections::HashSet;
use anyhow::Result;
use crate::chain::{Chain, ChainItem, ChainItemKind, ChainMode, FailurePolicy};
use crate::resolver::ResolutionOverrides;
use crate::types::{DetectionWarning, ProjectContext};
pub(crate) fn run_chain(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
) -> Result<i32> {
let mut warnings: HashSet<DetectionWarning> = HashSet::new();
let mut outcomes: Vec<ItemOutcome> = Vec::new();
for item in &chain.items {
if let ChainItemKind::Task(name) = &item.kind {
crate::cmd::run::precheck_task(ctx, overrides, name)?;
}
}
let result = match chain.mode {
ChainMode::Sequential => {
run_sequential(ctx, overrides, chain, &mut warnings, &mut outcomes)
}
ChainMode::Parallel => run_parallel(ctx, overrides, chain, &mut warnings, &mut outcomes),
};
crate::cmd::emit_collected_warnings(&warnings, overrides);
if let Ok(code) = result {
emit_chain_summary(overrides, &outcomes, code);
}
result
}
struct ItemOutcome {
name: String,
status: ItemStatus,
}
enum ItemStatus {
Ran {
code: i32,
elapsed: std::time::Duration,
},
Skipped,
}
fn run_sequential(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
outcomes: &mut Vec<ItemOutcome>,
) -> Result<i32> {
let keep_going = matches!(chain.failure, FailurePolicy::KeepGoing);
let mut first_failure: Option<i32> = None;
for (index, item) in chain.items.iter().enumerate() {
let started = std::time::Instant::now();
let code = dispatch_item(ctx, overrides, item, warnings)?;
let elapsed = started.elapsed();
crate::cmd::emit_task_timing(overrides, item.display_name(), elapsed, code);
outcomes.push(ItemOutcome {
name: item.display_name().to_string(),
status: ItemStatus::Ran { code, elapsed },
});
if code != 0 {
first_failure.get_or_insert(code);
if !keep_going {
outcomes.extend(chain.items[index + 1..].iter().map(|skipped| ItemOutcome {
name: skipped.display_name().to_string(),
status: ItemStatus::Skipped,
}));
return Ok(code);
}
}
}
Ok(first_failure.unwrap_or(0))
}
fn run_parallel(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
outcomes: &mut Vec<ItemOutcome>,
) -> Result<i32> {
let in_gha = actions_rs::env::is_github_actions();
let grouped = if in_gha {
overrides.group_output && overrides.github_group_parallel && !overrides.parent_group_open
} else {
overrides.parallel_grouped
};
if grouped {
let style = if overrides.quiet {
BlockStyle::Bare
} else if in_gha {
BlockStyle::Gha
} else {
BlockStyle::Header
};
run_parallel_grouped(ctx, overrides, chain, warnings, outcomes, style, in_gha)
} else {
run_parallel_streaming(ctx, overrides, chain, warnings, outcomes)
}
}
#[derive(Debug, Clone, Copy)]
enum BlockStyle {
Gha,
Header,
Bare,
}
fn run_parallel_streaming(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
outcomes: &mut Vec<ItemOutcome>,
) -> Result<i32> {
use std::process::Child;
use std::sync::Arc;
use std::time::Instant;
use crate::chain::mux::{LineSink, StdioSink, prefix_width, render_prefix, spawn_readers};
let names: Vec<&str> = chain.items.iter().map(ChainItem::display_name).collect();
let width = prefix_width(&names);
let colorize = colored::control::SHOULD_COLORIZE.should_colorize();
let sink: Arc<dyn LineSink> = Arc::new(StdioSink);
let mut children: Vec<(String, Instant, Child)> = Vec::with_capacity(chain.items.len());
let mut reader_handles = Vec::new();
let spawn_outcome: Result<()> = (|| {
for item in &chain.items {
let prefix = render_prefix(item.display_name(), width, colorize);
let started = Instant::now();
let mut child = match &item.kind {
ChainItemKind::Task(name) => crate::cmd::run::dispatch_task_piped(
ctx,
overrides,
name,
&item.args,
Some(warnings),
)?,
ChainItemKind::Install { .. } => {
anyhow::bail!("install items cannot run in parallel chains")
}
};
let stdout: Box<dyn std::io::Read + Send> =
Box::new(child.stdout.take().expect("stdout piped"));
let stderr: Box<dyn std::io::Read + Send> =
Box::new(child.stderr.take().expect("stderr piped"));
reader_handles.extend(spawn_readers(
vec![
(prefix.clone(), false, stdout),
(prefix.clone(), true, stderr),
],
&sink,
));
children.push((item.display_name().to_string(), started, child));
}
Ok(())
})();
if let Err(e) = spawn_outcome {
kill_and_reap(children);
wait_for_readers(&mut reader_handles, READER_DRAIN_GRACE);
return Err(e);
}
let mut remaining: Vec<(String, Instant, Child)> = children;
let mut first_failure: Option<i32> = None;
let kill_on_fail = matches!(chain.failure, FailurePolicy::KillOnFail);
while !remaining.is_empty() {
let mut next: Vec<(String, Instant, Child)> = Vec::with_capacity(remaining.len());
let mut poll_error: Option<anyhow::Error> = None;
let mut pending = std::mem::take(&mut remaining).into_iter();
for (name, started, mut child) in pending.by_ref() {
match child.try_wait() {
Ok(Some(status)) => {
let code = crate::cmd::exit_code(status);
if code != 0 {
first_failure.get_or_insert(code);
}
record_finished(overrides, outcomes, name, started.elapsed(), code);
}
Ok(None) => {
if kill_on_fail && first_failure.is_some() {
let _ = child.kill();
if let Ok(status) = child.wait() {
let code = crate::cmd::exit_code(status);
record_finished(overrides, outcomes, name, started.elapsed(), code);
}
} else {
next.push((name, started, child));
}
}
Err(e) => {
let _ = child.kill();
let _ = child.wait();
poll_error = Some(e.into());
break;
}
}
}
if let Some(e) = poll_error {
kill_and_reap(next.into_iter().chain(pending));
wait_for_readers(&mut reader_handles, READER_DRAIN_GRACE);
return Err(e);
}
remaining = next;
if !remaining.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
wait_for_readers(&mut reader_handles, READER_DRAIN_GRACE);
Ok(first_failure.unwrap_or(0))
}
fn record_finished(
overrides: &ResolutionOverrides,
outcomes: &mut Vec<ItemOutcome>,
name: String,
elapsed: std::time::Duration,
code: i32,
) {
crate::cmd::emit_task_timing(overrides, &name, elapsed, code);
outcomes.push(ItemOutcome {
name,
status: ItemStatus::Ran { code, elapsed },
});
}
struct GroupedTask {
name: String,
started: std::time::Instant,
child: std::process::Child,
sink: std::sync::Arc<crate::chain::mux::BufferSink>,
readers: Vec<std::thread::JoinHandle<()>>,
}
const READER_DRAIN_GRACE: std::time::Duration = std::time::Duration::from_millis(500);
fn run_parallel_grouped(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
outcomes: &mut Vec<ItemOutcome>,
style: BlockStyle,
in_gha: bool,
) -> Result<i32> {
use std::sync::Arc;
use crate::chain::mux::{BufferSink, LineSink, spawn_readers};
let mut tasks: Vec<GroupedTask> = Vec::with_capacity(chain.items.len());
let spawn_outcome: Result<()> = (|| {
for item in &chain.items {
let started = std::time::Instant::now();
let (name, mut child, sink) = match &item.kind {
ChainItemKind::Task(task_name) => {
let sink = Arc::new(BufferSink::new()?);
let child = crate::cmd::run::dispatch_task_piped(
ctx,
overrides,
task_name,
&item.args,
Some(warnings),
)?;
(item.display_name().to_string(), child, sink)
}
ChainItemKind::Install { .. } => {
anyhow::bail!("install items cannot run in parallel chains")
}
};
let stdout: Box<dyn std::io::Read + Send> =
Box::new(child.stdout.take().expect("stdout piped"));
let stderr: Box<dyn std::io::Read + Send> =
Box::new(child.stderr.take().expect("stderr piped"));
let dyn_sink: Arc<dyn LineSink> = sink.clone();
let readers = spawn_readers(
vec![
(String::new(), false, stdout),
(String::new(), true, stderr),
],
&dyn_sink,
);
tasks.push(GroupedTask {
name,
started,
child,
sink,
readers,
});
}
Ok(())
})();
if let Err(e) = spawn_outcome {
for t in tasks {
cleanup_grouped_task(t);
}
return Err(e);
}
let colorize = colored::control::SHOULD_COLORIZE.should_colorize();
let mut remaining = tasks;
let mut first_failure: Option<i32> = None;
let kill_on_fail = matches!(chain.failure, FailurePolicy::KillOnFail);
while !remaining.is_empty() {
let mut next: Vec<GroupedTask> = Vec::with_capacity(remaining.len());
let mut poll_error: Option<anyhow::Error> = None;
let mut pending = std::mem::take(&mut remaining).into_iter();
for mut t in pending.by_ref() {
match t.child.try_wait() {
Ok(Some(status)) => {
let code = crate::cmd::exit_code(status);
if code != 0 {
first_failure.get_or_insert(code);
}
let footer = record_grouped(overrides, outcomes, &t, code);
flush_grouped_task(t, style, in_gha, colorize, footer.as_deref());
}
Ok(None) => {
if kill_on_fail && first_failure.is_some() {
let _ = t.child.kill();
let footer = t.child.wait().ok().and_then(|status| {
record_grouped(overrides, outcomes, &t, crate::cmd::exit_code(status))
});
flush_grouped_task(t, style, in_gha, colorize, footer.as_deref());
} else {
next.push(t);
}
}
Err(e) => {
cleanup_grouped_task(t);
poll_error = Some(e.into());
break;
}
}
}
if let Some(e) = poll_error {
for t in next.into_iter().chain(pending) {
cleanup_grouped_task(t);
}
return Err(e);
}
remaining = next;
if !remaining.is_empty() {
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
Ok(first_failure.unwrap_or(0))
}
fn record_grouped(
overrides: &ResolutionOverrides,
outcomes: &mut Vec<ItemOutcome>,
task: &GroupedTask,
code: i32,
) -> Option<String> {
let elapsed = task.started.elapsed();
outcomes.push(ItemOutcome {
name: task.name.clone(),
status: ItemStatus::Ran { code, elapsed },
});
timing_footer(overrides, elapsed, code)
}
fn flush_grouped_task(
task: GroupedTask,
style: BlockStyle,
in_gha: bool,
colorize: bool,
footer: Option<&str>,
) {
flush_task_group(
&task.name,
style,
in_gha,
colorize,
&task.sink,
task.readers,
footer,
);
}
fn flush_task_group(
name: &str,
style: BlockStyle,
in_gha: bool,
colorize: bool,
sink: &crate::chain::mux::BufferSink,
mut readers: Vec<std::thread::JoinHandle<()>>,
timing_footer: Option<&str>,
) {
use std::io::Write as _;
wait_for_readers(&mut readers, READER_DRAIN_GRACE);
sink.close();
join_finished_readers(&mut readers);
let group = match style {
BlockStyle::Gha => Some(actions_rs::log::group_guard(format!("runner: {name}"))),
BlockStyle::Header => {
let header = format!("runner: {name}");
let header = if colorize {
use colored::Colorize as _;
header
.color(crate::chain::mux::color_for(name))
.bold()
.to_string()
} else {
header
};
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "{header}");
let _ = out.flush();
None
}
BlockStyle::Bare => None,
};
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let _ = sink.replay_to(&mut stdout, &mut stderr, in_gha);
write_timing_footer(timing_footer, colorize);
drop(group);
}
fn timing_footer(
overrides: &ResolutionOverrides,
elapsed: std::time::Duration,
code: i32,
) -> Option<String> {
crate::cmd::timing_enabled(overrides).then(|| crate::cmd::task_timing_summary(elapsed, code))
}
fn write_timing_footer(footer: Option<&str>, colorize: bool) {
use std::io::Write as _;
let Some(footer) = footer else { return };
let line = if colorize {
use colored::Colorize as _;
footer.dimmed().to_string()
} else {
footer.to_string()
};
let mut out = std::io::stdout().lock();
let _ = writeln!(out, "{line}");
let _ = out.flush();
}
fn emit_chain_summary(overrides: &ResolutionOverrides, outcomes: &[ItemOutcome], code: i32) {
use colored::Colorize as _;
if outcomes.len() < 2 || !crate::cmd::timing_enabled(overrides) {
return;
}
let failed: Vec<&ItemOutcome> = outcomes.iter().filter(|o| o.failed()).collect();
let counts = summary_counts(outcomes, failed.len());
let verdict = if failed.is_empty() {
format!("exit {code}")
} else {
format!("exit {code}, first failure")
};
eprintln!(
"{} {} {}",
"·".dimmed(),
format!("summary: {counts}").bold(),
format!("({verdict})").dimmed(),
);
let names: Vec<&str> = outcomes.iter().map(|o| o.name.as_str()).collect();
let width = crate::chain::mux::prefix_width(&names);
for outcome in outcomes {
eprintln!("{} {}", "·".dimmed(), outcome.render(width));
}
if overrides.group_output && actions_rs::env::is_github_actions() {
for outcome in failed {
let ItemStatus::Ran { code, .. } = outcome.status else {
continue;
};
actions_rs::Annotation::new()
.title(format!("runner: {}", outcome.name))
.error(format!("exit {code}"));
}
}
}
fn summary_counts(outcomes: &[ItemOutcome], failed: usize) -> String {
use std::fmt::Write as _;
let skipped = outcomes
.iter()
.filter(|o| matches!(o.status, ItemStatus::Skipped))
.count();
let mut counts = format!(
"{} tasks, {} ok",
outcomes.len(),
outcomes.len() - failed - skipped
);
if failed > 0 {
let _ = write!(counts, ", {failed} failed");
}
if skipped > 0 {
let _ = write!(counts, ", {skipped} skipped");
}
counts
}
impl ItemOutcome {
const fn failed(&self) -> bool {
matches!(self.status, ItemStatus::Ran { code, .. } if code != 0)
}
fn render(&self, width: usize) -> String {
use colored::Colorize as _;
let (mark, detail) = match self.status {
ItemStatus::Ran { code: 0, elapsed } => {
("✓".green(), crate::cmd::format_duration(elapsed))
}
ItemStatus::Ran { code, elapsed } => (
"✗".red(),
format!("{} (exit {code})", crate::cmd::format_duration(elapsed)),
),
ItemStatus::Skipped => ("–".dimmed(), String::from("skipped")),
};
format!(
"{mark} {:<width$} {}",
self.name,
detail.dimmed(),
width = width,
)
}
}
fn kill_and_reap<I: IntoIterator<Item = (String, std::time::Instant, std::process::Child)>>(
children: I,
) {
for (_, _, mut c) in children {
let _ = c.kill();
let _ = c.wait();
}
}
fn cleanup_grouped_task(mut t: GroupedTask) {
let _ = t.child.kill();
let _ = t.child.wait();
t.sink.close();
wait_for_readers(&mut t.readers, READER_DRAIN_GRACE);
}
fn wait_for_readers(readers: &mut Vec<std::thread::JoinHandle<()>>, grace: std::time::Duration) {
let deadline = std::time::Instant::now() + grace;
loop {
join_finished_readers(readers);
if readers.is_empty() {
return;
}
let now = std::time::Instant::now();
if now >= deadline {
return;
}
std::thread::sleep((deadline - now).min(std::time::Duration::from_millis(10)));
}
}
fn join_finished_readers(readers: &mut Vec<std::thread::JoinHandle<()>>) {
let mut index = 0;
while index < readers.len() {
if readers[index].is_finished() {
let handle = readers.swap_remove(index);
let _ = handle.join();
} else {
index += 1;
}
}
}
fn dispatch_item(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
item: &ChainItem,
warnings: &mut HashSet<DetectionWarning>,
) -> Result<i32> {
match &item.kind {
ChainItemKind::Task(name) => {
crate::cmd::run::run(ctx, overrides, name, &item.args, Some(warnings))
}
ChainItemKind::Install { frozen } => {
crate::cmd::install::install_pms(ctx, overrides, *frozen, Some(warnings))
}
}
}