use core::time::Duration;
use std::io::{self, Read};
use std::process::{Command, Output, Stdio};
use std::sync::mpsc::{self, Receiver, SyncSender};
use std::thread::{self, JoinHandle};
use std::time::Instant;
use cargo_gamma_process::{MemoryRequest, ProcessTree, prepare};
use super::super::cargo_options::BuildLimits;
use super::super::events::Events;
#[cfg(test)]
use super::super::faults::{self, Fault};
use super::super::workspace::Workspace;
use super::messages::cargo_message;
use crate::Result;
use crate::discover::Plan;
use crate::error::{Error, error};
use crate::report::encode_controls;
pub(super) fn spawn_failure(program: &str, work: &Workspace, cause: io::Error) -> Error {
let program = encode_controls(program);
let root = encode_controls(work.root.as_str());
if !work.root.as_std_path().is_dir() {
return error!("the scratch tree at `{root}` disappeared while it was being built").caused_by(cause);
}
error!(
"could not run `{program}` in `{root}`. Cargo is taken from the `CARGO` environment variable when it is set, and from `PATH` otherwise"
)
.caused_by(cause)
}
pub(super) fn compile(work: &Workspace, args: &[String], budget: Option<Duration>, events: &mut dyn Events) -> Result<Option<Output>> {
let mut command = work.cargo();
let _command = command
.env("CARGO_TERM_PROGRESS_WHEN", "always")
.env("CARGO_TERM_PROGRESS_WIDTH", PROGRESS_WIDTH.to_string())
.args(args)
.stderr(Stdio::piped())
.stdout(Stdio::piped());
supervise(command, work, budget, events)
}
pub(super) fn supervise(command: Command, work: &Workspace, budget: Option<Duration>, events: &mut dyn Events) -> Result<Option<Output>> {
supervise_with_limits(command, work, budget, events, OUTPUT_LIMITS)
}
pub(super) fn supervise_with_limits(
command: Command,
work: &Workspace,
budget: Option<Duration>,
events: &mut dyn Events,
limits: OutputLimits,
) -> Result<Option<Output>> {
let program = command.get_program().to_string_lossy().into_owned();
let root = encode_controls(work.root.as_str());
let prepared = prepare(command, MemoryRequest::default()).map_err(|reason| {
let raw_reason = reason.to_string();
let reason = encode_controls(&raw_reason);
error!("the cargo build in `{root}` could not be contained: {reason}")
})?;
let spawned = prepared.spawn().map_err(|failure| {
let (cause, _prepared) = failure.into_parts();
spawn_failure(&program, work, cause)
})?;
let mut subtree = match ProcessTree::adopt(spawned) {
Ok(subtree) => subtree,
Err(reason) => {
events.build_finished();
let raw_reason = reason.to_string();
let reason = encode_controls(&raw_reason);
return Err(error!("the cargo build in `{root}` could not be contained: {reason}"));
}
};
let (sender, lines) = mpsc::sync_channel(limits.backlog);
let stdout = subtree
.take_stdout()
.map(|pipe| read_pipe_with_limits(pipe, Stream::Json, &sender, limits));
let stderr = subtree
.take_stderr()
.map(|pipe| read_pipe_with_limits(pipe, Stream::Prose, &sender, limits));
drop(sender);
let deadline = budget.map(|budget| Instant::now() + budget);
let outcome = loop {
let _narrated = narrate(&lines, events);
match subtree.observe() {
Ok(Some(status)) => {
break Ok(Some(status));
}
Ok(None) => {
if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
collect(&mut subtree);
break Ok(None);
}
thread::sleep(BUILD_POLL_INTERVAL);
}
Err(cause) => {
collect(&mut subtree);
break Err(error!("could not wait for cargo in `{root}`").caused_by(cause));
}
}
};
debug_assert!(subtree.released(), "the containment is released before the output is drained");
let grace = Instant::now() + DRAIN_GRACE;
let (said, printed) = finish_readers(stdout, stderr, &lines, events, grace);
events.build_finished();
let Some(status) = outcome? else {
return Ok(None);
};
let (Some(stdout), Some(stderr)) = (said, printed) else {
return Err(error!(
"cargo in `{root}` finished, but its output could not be read to the end, so what it built could not be read"
));
};
if !stdout.complete || !stderr.complete {
return Err(error!(
"cargo in `{root}` finished, but its output could not be read to the end, so what it built could not be read"
));
}
if !stdout.within_limits || !stderr.within_limits {
return Err(error!(
"cargo in `{root}` exceeded the configured {}-byte retained or {}-byte per-line build-output limit, so its truncated output could not be trusted",
limits.retained, limits.line
));
}
Ok(Some(Output {
status,
stdout: stdout.text,
stderr: stderr.text,
}))
}
fn collect(subtree: &mut ProcessTree) {
#[cfg(unix)]
debug_assert!(
!subtree.released(),
"the subtree is signalled while it still holds its leader and its watch slot"
);
let _reaped = subtree.terminate();
}
pub(super) const PROGRESS_WIDTH: usize = 100;
pub(super) fn narrate(lines: &Receiver<(Stream, String)>, events: &mut dyn Events) -> usize {
let wanted = events.wants_build_output();
let mut narrated = 0;
while narrated < NARRATION_BATCH {
let Ok((stream, line)) = lines.try_recv() else {
break;
};
narrated += 1;
match stream {
Stream::Prose if is_progress(&line) => events.build_progress(line.trim_end()),
Stream::Prose => events.build_output(line.trim_end()),
Stream::Json => {
if !wanted {
continue;
}
if let Some(rendered) = rendered_diagnostic(&line) {
events.build_output(rendered.trim_end());
}
}
}
}
narrated
}
pub(super) fn is_progress(line: &str) -> bool {
let plain = crate::report::unstyled(line);
let trimmed = plain.trim_start();
trimmed.starts_with("Building [") || trimmed.starts_with("Compiling [")
}
pub(super) fn rendered_diagnostic(line: &str) -> Option<String> {
let message = cargo_message(line)?;
if message.reason != "compiler-message" {
return None;
}
let rendered = message.message?.rendered?;
if rendered.trim().is_empty() {
None
} else {
Some(rendered.into_owned())
}
}
pub(super) const BUILD_POLL_INTERVAL: Duration = Duration::from_millis(20);
#[derive(Clone, Copy, Debug)]
pub(super) struct OutputLimits {
pub(super) retained: usize,
pub(super) line: usize,
pub(super) backlog: usize,
}
const OUTPUT_LIMITS: OutputLimits = OutputLimits {
retained: 4 * 1024 * 1024,
line: 64 * 1024,
backlog: 64,
};
const NARRATION_BATCH: usize = 128;
#[derive(Debug)]
pub(super) struct Pipe {
pub(super) text: Vec<u8>,
pub(super) complete: bool,
pub(super) within_limits: bool,
}
#[cfg(test)]
pub(super) fn read_pipe<R: Read + Send + 'static>(
pipe: R,
stream: Stream,
sink: &SyncSender<(Stream, String)>,
) -> io::Result<JoinHandle<Pipe>> {
read_pipe_with_limits(pipe, stream, sink, OUTPUT_LIMITS)
}
pub(super) fn read_pipe_with_limits<R: Read + Send + 'static>(
mut pipe: R,
stream: Stream,
sink: &SyncSender<(Stream, String)>,
limits: OutputLimits,
) -> io::Result<JoinHandle<Pipe>> {
let sink = sink.clone();
#[cfg(test)]
if faults::fired(Fault::Thread) {
return Err(io::Error::other("the reader thread a test asked to fail"));
}
thread::Builder::new().name("cargo-gamma-build-output".to_owned()).spawn(move || {
let mut text = Vec::with_capacity(limits.retained);
let mut buffer = [0_u8; 8192];
let mut line = Vec::with_capacity(limits.line);
let mut complete = true;
let mut within_limits = true;
let mut line_limited = false;
loop {
let read = match pipe.read(&mut buffer) {
Ok(0) => break,
Ok(read) => read,
Err(cause) => {
if cause.kind() == io::ErrorKind::Interrupted {
continue;
}
complete = false;
break;
}
};
let room = limits.retained.saturating_sub(text.len());
let kept = read.min(room);
text.extend_from_slice(&buffer[..kept]);
if kept < read {
within_limits = false;
}
for byte in &buffer[..read] {
if matches!(*byte, b'\n' | b'\r') {
if !line_limited
&& !line.is_empty()
&& let Ok(line) = str::from_utf8(&line)
{
let _sent = sink.send((stream, line.to_owned()));
}
line.clear();
line_limited = false;
continue;
}
if line.len() < limits.line {
line.push(*byte);
} else {
line_limited = true;
within_limits = false;
}
}
}
if !line_limited
&& !line.is_empty()
&& let Ok(line) = str::from_utf8(&line)
{
let _ = sink.send((stream, line.to_owned()));
}
Pipe {
text,
complete,
within_limits,
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Stream {
Json,
Prose,
}
#[cfg(test)]
pub(super) fn drained(handle: Option<JoinHandle<Pipe>>, deadline: Instant) -> Option<Vec<u8>> {
let Some(handle) = handle else {
return Some(Vec::new());
};
while !handle.is_finished() {
if Instant::now() >= deadline {
return None;
}
thread::sleep(BUILD_POLL_INTERVAL);
}
let Ok(pipe) = handle.join() else {
return None;
};
(pipe.complete && pipe.within_limits).then_some(pipe.text)
}
pub(super) fn finish_readers(
stdout: Option<io::Result<JoinHandle<Pipe>>>,
stderr: Option<io::Result<JoinHandle<Pipe>>>,
lines: &Receiver<(Stream, String)>,
events: &mut dyn Events,
deadline: Instant,
) -> (Option<Pipe>, Option<Pipe>) {
let finished = |reader: &Option<io::Result<JoinHandle<Pipe>>>| {
reader
.as_ref()
.is_none_or(|reader| reader.as_ref().is_err() || reader.as_ref().is_ok_and(JoinHandle::is_finished))
};
while !finished(&stdout) || !finished(&stderr) {
let _narrated = narrate(lines, events);
if Instant::now() >= deadline {
return (None, None);
}
thread::sleep(BUILD_POLL_INTERVAL);
}
while narrate(lines, events) == NARRATION_BATCH {}
let stdout = stdout.map(|handle| {
let handle = handle.ok()?;
handle.join().ok()
});
let stderr = stderr.map(|handle| {
let handle = handle.ok()?;
handle.join().ok()
});
(
stdout.unwrap_or_else(|| {
Some(Pipe {
text: Vec::new(),
complete: true,
within_limits: true,
})
}),
stderr.unwrap_or_else(|| {
Some(Pipe {
text: Vec::new(),
complete: true,
within_limits: true,
})
}),
)
}
const DRAIN_GRACE: Duration = Duration::from_secs(5);
#[derive(Debug)]
pub(super) struct Compiled {
pub(super) succeeded: bool,
pub(super) stdout: Option<String>,
pub(super) stderr: String,
}
pub(super) fn run_cargo(
work: &Workspace,
plan: &Plan,
verb: &[&str],
select: Option<&[String]>,
limits: BuildLimits,
first_round: Option<Duration>,
events: &mut dyn Events,
) -> Result<Compiled> {
let mut args: Vec<String> = verb.iter().map(|arg| (*arg).to_owned()).collect();
args.push("--message-format=json".to_owned());
match select {
Some(packages) => {
for package in packages {
args.push("--package".to_owned());
args.push(plan.spec(&work.root, package));
}
}
None => args.push("--workspace".to_owned()),
}
work.cargo.extend_build_args(&mut args);
let Some(output) = compile(work, &args, limits.budget(first_round), events)? else {
return Ok(Compiled {
succeeded: false,
stdout: None,
stderr: String::new(),
});
};
let stdout = match String::from_utf8(output.stdout) {
Ok(text) => text,
Err(invalid) => String::from_utf8_lossy(invalid.as_bytes()).into_owned(),
};
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
Ok(Compiled {
succeeded: output.status.success(),
stdout: Some(stdout),
stderr,
})
}