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();
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),
ChainMode::Parallel => run_parallel(ctx, overrides, chain, &mut warnings),
};
crate::cmd::emit_collected_warnings(&warnings, overrides);
result
}
fn run_sequential(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
) -> Result<i32> {
let keep_going = matches!(chain.failure, FailurePolicy::KeepGoing);
let mut first_failure: Option<i32> = None;
for item in &chain.items {
let started = std::time::Instant::now();
let code = dispatch_item(ctx, overrides, item, warnings)?;
crate::cmd::emit_task_timing(overrides, item.display_name(), started.elapsed(), code);
if code != 0 {
first_failure.get_or_insert(code);
if !keep_going {
return Ok(code);
}
}
}
Ok(first_failure.unwrap_or(0))
}
fn run_parallel(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
) -> 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 gha_syntax = in_gha;
run_parallel_grouped(ctx, overrides, chain, warnings, gha_syntax)
} else {
run_parallel_streaming(ctx, overrides, chain, warnings)
}
}
fn run_parallel_streaming(
ctx: &ProjectContext,
overrides: &ResolutionOverrides,
chain: &Chain,
warnings: &mut HashSet<DetectionWarning>,
) -> 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);
crate::cmd::emit_task_timing(overrides, &name, started.elapsed(), code);
if code != 0 {
first_failure.get_or_insert(code);
}
}
Ok(None) => {
if kill_on_fail && first_failure.is_some() {
let _ = child.kill();
if let Ok(status) = child.wait() {
crate::cmd::emit_task_timing(
overrides,
&name,
started.elapsed(),
crate::cmd::exit_code(status),
);
}
} 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))
}
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>,
gha_syntax: 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 = timing_footer(overrides, t.started.elapsed(), code);
flush_grouped_task(t, gha_syntax, colorize, footer.as_deref());
}
Ok(None) => {
if kill_on_fail && first_failure.is_some() {
let _ = t.child.kill();
let elapsed = t.started.elapsed();
let footer = t.child.wait().ok().and_then(|status| {
timing_footer(overrides, elapsed, crate::cmd::exit_code(status))
});
flush_grouped_task(t, gha_syntax, 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 flush_grouped_task(task: GroupedTask, gha_syntax: bool, colorize: bool, footer: Option<&str>) {
flush_task_group(
&task.name,
gha_syntax,
colorize,
&task.sink,
task.readers,
footer,
);
}
fn flush_task_group(
name: &str,
gha_syntax: 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);
if gha_syntax {
let _group = actions_rs::log::group_guard(format!("runner: {name}"));
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let _ = sink.replay_to(&mut stdout, &mut stderr, true);
write_timing_footer(timing_footer, colorize);
} else {
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();
}
let mut stdout = std::io::stdout();
let mut stderr = std::io::stderr();
let _ = sink.replay_to(&mut stdout, &mut stderr, false);
write_timing_footer(timing_footer, colorize);
}
}
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 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))
}
}
}