pub mod xml;
use serde::Deserialize;
use std::ffi::OsString;
use std::net::{SocketAddr, TcpListener};
use std::process::Command;
use std::{env, fmt, io::Read};
use std::{ffi::OsStr, process::Stdio};
const STACK_OVERFLOW: &str = "main thread stack using the --main-stacksize= flag";
#[derive(Debug)]
pub enum Error {
ValgrindNotInstalled,
SocketConnection,
ProcessFailed,
ValgrindFailure(String),
StackOverflow(String),
ProcessSignal(i32, xml::Output),
MalformedOutput(serde_xml_rs::Error, Vec<u8>),
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::ValgrindNotInstalled => write!(f, "valgrind executable not found"),
Self::SocketConnection => write!(f, "local TCP I/O error"),
Self::ProcessFailed => write!(f, "cannot start valgrind process"),
Self::ProcessSignal(nr, _) => write!(f, "program exited with signal {nr}"),
Self::StackOverflow(stderr) => write!(f, "stack overflow detected: {stderr}"),
Self::ValgrindFailure(s) => write!(f, "invalid valgrind usage: {s}"),
Self::MalformedOutput(e, _) => write!(f, "unexpected valgrind output: {e}"),
}
}
}
pub fn execute<S, I>(command: I) -> Result<xml::Output, Error>
where
S: AsRef<OsStr>,
I: IntoIterator<Item = S>,
{
let address: SocketAddr = ([127, 0, 0, 1], 0).into();
let listener = TcpListener::bind(address).map_err(|_| Error::SocketConnection)?;
let address = listener.local_addr().map_err(|_| Error::SocketConnection)?;
let mut valgrind = Command::new("valgrind");
if let Ok(additional_args) = env::var("VALGRINDFLAGS") {
valgrind.args(additional_args.split(' '));
}
let suppressions = temp_file::TempFile::with_prefix("valgrind-suppressions")
.expect("could not create temporary suppression file")
.with_contents(SUPPRESSIONS.as_bytes())
.expect("could not write to temporary suppression file");
valgrind.arg({
let mut option = OsString::from("--suppressions=");
option.push(suppressions.path());
option
});
let cargo = valgrind
.arg("--xml=yes")
.arg(format!("--xml-socket={}:{}", address.ip(), address.port()))
.args(command)
.stderr(Stdio::piped())
.spawn()
.map_err(|_| Error::ValgrindNotInstalled)?;
let xml = std::thread::spawn(move || {
let (mut listener, _) = listener.accept().map_err(|_| Error::SocketConnection)?;
let mut output = Vec::new();
listener
.read_to_end(&mut output)
.map_err(|_| Error::SocketConnection)?;
let xml: xml::Output = xml::Output::deserialize(
&mut serde_xml_rs::Deserializer::new_from_reader(&*output)
.non_contiguous_seq_elements(true),
)
.map(|output_: xml::Output| {
let mut output = output_;
if let Some(err) = output.errors {
let new_err: Vec<xml::Error> = err
.into_iter()
.filter(|e| {
!e.kind.is_leak() || e.resources.bytes > 0 || e.resources.blocks > 0
})
.collect();
if new_err.is_empty() {
output.errors = None;
} else {
output.errors = Some(new_err);
}
}
output
})
.map_err(|e| Error::MalformedOutput(e, output))?;
Ok(xml)
});
let output = cargo.wait_with_output().map_err(|_| Error::ProcessFailed)?;
if output.status.success() {
let xml = xml.join().expect("Reader-thread panicked")?;
Ok(xml)
} else if let Some(signal_nr) = is_terminated_by_signal(output.status) {
let xml = xml.join().expect("Reader-thread panicked")?;
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains(STACK_OVERFLOW) {
Err(Error::StackOverflow(stderr.to_string()))
} else {
Err(Error::ProcessSignal(signal_nr, xml))
}
} else {
drop(xml);
Err(Error::ValgrindFailure(
String::from_utf8_lossy(&output.stderr).to_string(),
))
}
}
#[cfg(unix)] fn is_terminated_by_signal(exit_status: std::process::ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
exit_status.signal()
}
#[cfg(not(unix))] fn is_terminated_by_signal(_exit_status: std::process::ExitStatus) -> Option<i32> {
None
}
include!(concat!(env!("OUT_DIR"), "/suppressions.rs"));