use std::io::{self, Read};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use crate::domain::{CommitOwnership, MemberId};
use crate::error::{Error, Result};
use crate::integrity::{IntegrityStatus, VerifiedTransaction};
use crate::transaction::TransactionReceipt;
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
const DEFAULT_OUTPUT_LIMIT: usize = 64 * 1024;
#[derive(Debug, Clone)]
pub struct CommandSpec {
program: PathBuf,
args: Vec<String>,
current_dir: Option<PathBuf>,
timeout: Duration,
max_output_bytes: usize,
environment: Vec<(String, String)>,
}
impl CommandSpec {
pub fn new(program: impl Into<PathBuf>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
current_dir: None,
timeout: DEFAULT_TIMEOUT,
max_output_bytes: DEFAULT_OUTPUT_LIMIT,
environment: Vec::new(),
}
}
pub fn arg(mut self, argument: impl Into<String>) -> Self {
self.args.push(argument.into());
self
}
pub fn args<I, S>(mut self, arguments: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.args.extend(arguments.into_iter().map(Into::into));
self
}
pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.current_dir = Some(path.into());
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn max_output_bytes(mut self, limit: usize) -> Self {
self.max_output_bytes = limit;
self
}
pub fn environment(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.environment.push((name.into(), value.into()));
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandOutput {
exit_code: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
timed_out: bool,
output_limited: bool,
}
impl CommandOutput {
pub fn exit_code(&self) -> Option<i32> {
self.exit_code
}
pub fn stdout(&self) -> &[u8] {
&self.stdout
}
pub fn stderr(&self) -> &[u8] {
&self.stderr
}
pub fn timed_out(&self) -> bool {
self.timed_out
}
pub fn output_limited(&self) -> bool {
self.output_limited
}
pub fn success(&self) -> bool {
!self.timed_out && !self.output_limited && self.exit_code == Some(0)
}
}
pub fn run_bounded(spec: &CommandSpec) -> Result<CommandOutput> {
let mut command = Command::new(&spec.program);
command
.args(&spec.args)
.env_clear()
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if let Some(current_dir) = &spec.current_dir {
command.current_dir(current_dir);
}
for (name, value) in &spec.environment {
command.env(name, value);
}
let mut child = command
.spawn()
.map_err(|source| Error::io("spawning bounded candidate", source))?;
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::CandidateExecution("missing stdout pipe".into()))?;
let stderr = child
.stderr
.take()
.ok_or_else(|| Error::CandidateExecution("missing stderr pipe".into()))?;
let (stdout_tx, stdout_rx) = mpsc::channel();
let (stderr_tx, stderr_rx) = mpsc::channel();
let limit = spec.max_output_bytes;
let stdout_thread = thread::spawn(move || {
let _ = stdout_tx.send(read_limited(stdout, limit));
});
let stderr_thread = thread::spawn(move || {
let _ = stderr_tx.send(read_limited(stderr, limit));
});
let deadline = Instant::now() + spec.timeout;
let mut timed_out = false;
let mut output_limited = false;
let mut status = None;
let mut stdout_result = None;
let mut stderr_result = None;
loop {
if stdout_result.is_none() {
if let Ok(result) = stdout_rx.try_recv() {
output_limited |= result.is_err();
stdout_result = Some(result.unwrap_or_default());
}
}
if stderr_result.is_none() {
if let Ok(result) = stderr_rx.try_recv() {
output_limited |= result.is_err();
stderr_result = Some(result.unwrap_or_default());
}
}
if status.is_none() {
status = child
.try_wait()
.map_err(|source| Error::io("polling bounded candidate", source))?;
}
if output_limited {
let _ = child.kill();
status = child.wait().ok();
} else if status.is_none() && Instant::now() >= deadline {
timed_out = true;
let _ = child.kill();
status = child.wait().ok();
}
if ((timed_out || output_limited) && status.is_some())
|| (status.is_some() && stdout_result.is_some() && stderr_result.is_some())
{
break;
}
thread::sleep(Duration::from_millis(2));
}
if !timed_out && !output_limited {
let _ = stdout_thread.join();
let _ = stderr_thread.join();
}
Ok(CommandOutput {
exit_code: status.and_then(|status| status.code()),
stdout: stdout_result.unwrap_or_default(),
stderr: stderr_result.unwrap_or_default(),
timed_out,
output_limited,
})
}
fn read_limited(mut reader: impl Read, limit: usize) -> io::Result<Vec<u8>> {
let mut output = Vec::new();
let mut buffer = [0_u8; 4096];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
return Ok(output);
}
if output.len().saturating_add(count) > limit {
return Err(io::Error::other("candidate output limit exceeded"));
}
output.extend_from_slice(&buffer[..count]);
}
}
pub trait CandidateValidator {
fn validate(&self, transaction: &VerifiedTransaction) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct ExactIdentityValidator {
checks: Vec<IdentityCheck>,
timeout: Duration,
max_output_bytes: usize,
}
#[derive(Debug, Clone)]
struct IdentityCheck {
member: MemberId,
args: Vec<String>,
expected_output: String,
}
impl ExactIdentityValidator {
pub fn new(member: MemberId, expected_output: impl Into<String>) -> Self {
Self {
checks: vec![IdentityCheck {
member,
args: Vec::new(),
expected_output: expected_output.into(),
}],
timeout: DEFAULT_TIMEOUT,
max_output_bytes: DEFAULT_OUTPUT_LIMIT,
}
}
pub fn for_members<I>(members: I, expected_output: impl Into<String>) -> Self
where
I: IntoIterator<Item = MemberId>,
{
let expected_output = expected_output.into();
let checks = members
.into_iter()
.map(|member| IdentityCheck {
member,
args: Vec::new(),
expected_output: expected_output.clone(),
})
.collect();
Self {
checks,
timeout: DEFAULT_TIMEOUT,
max_output_bytes: DEFAULT_OUTPUT_LIMIT,
}
}
pub fn args<I, S>(mut self, args: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
for check in &mut self.checks {
check.args = args.clone();
}
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn max_output_bytes(mut self, limit: usize) -> Self {
self.max_output_bytes = limit;
self
}
}
impl CandidateValidator for ExactIdentityValidator {
fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
for check in &self.checks {
let program = transaction.staged_path(&check.member)?;
let output = run_bounded(
&CommandSpec::new(program)
.args(check.args.clone())
.current_dir(transaction.stage_root())
.timeout(self.timeout)
.max_output_bytes(self.max_output_bytes),
)?;
if !output.success()
|| !output.stderr().is_empty()
|| output.stdout() != check.expected_output.as_bytes()
{
return Err(Error::CandidateExecution(format!(
"member {} did not produce the exact expected identity",
check.member
)));
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct CrossMemberAgreementValidator(ExactIdentityValidator);
impl CrossMemberAgreementValidator {
pub fn new<I>(members: I, expected_output: impl Into<String>) -> Self
where
I: IntoIterator<Item = MemberId>,
{
Self(ExactIdentityValidator::for_members(
members,
expected_output,
))
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.0 = self.0.timeout(timeout);
self
}
pub fn max_output_bytes(mut self, limit: usize) -> Self {
self.0 = self.0.max_output_bytes(limit);
self
}
}
impl CandidateValidator for CrossMemberAgreementValidator {
fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
self.0.validate(transaction)
}
}
pub struct AllValidators<'a> {
validators: Vec<&'a dyn CandidateValidator>,
}
impl<'a> AllValidators<'a> {
pub fn new() -> Self {
Self {
validators: Vec::new(),
}
}
pub fn push(mut self, validator: &'a dyn CandidateValidator) -> Self {
self.validators.push(validator);
self
}
}
impl Default for AllValidators<'_> {
fn default() -> Self {
Self::new()
}
}
impl CandidateValidator for AllValidators<'_> {
fn validate(&self, transaction: &VerifiedTransaction) -> Result<()> {
for validator in &self.validators {
validator.validate(transaction)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ValidatedTransaction {
pub(crate) verified: VerifiedTransaction,
}
impl VerifiedTransaction {
pub fn validate(self, validator: &dyn CandidateValidator) -> Result<ValidatedTransaction> {
if self.artifacts().iter().any(|member| {
self.integrity(member.id())
.is_none_or(|result| result.status() != IntegrityStatus::Verified)
}) {
return Err(Error::VerificationFailed(
"candidate execution requires verified integrity evidence for every member".into(),
));
}
validator.validate(&self)?;
Ok(ValidatedTransaction { verified: self })
}
}
impl ValidatedTransaction {
pub fn artifacts(&self) -> &crate::domain::ArtifactSet {
self.verified.artifacts()
}
pub fn staged_path(&self, member: &MemberId) -> Result<std::path::PathBuf> {
self.verified.staged_path(member)
}
pub fn commit(self, ownership: CommitOwnership<'_>) -> Result<TransactionReceipt> {
let digests = self.verified.verified_digests();
self.verified
.prepared
.commit_inner(ownership, &digests, None)
}
#[cfg(test)]
pub(crate) fn commit_with_fault(
self,
ownership: CommitOwnership<'_>,
fault: crate::transaction::CommitFault,
) -> Result<TransactionReceipt> {
let digests = self.verified.verified_digests();
self.verified
.prepared
.commit_inner(ownership, &digests, Some(fault))
}
}