use std::fmt::Write as _;
use std::io::{self, BufWriter, Write};
use std::path::Path;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use super::Port;
use crate::error::{Error, Result};
pub(crate) struct Trace {
out: Mutex<Box<dyn Write + Send>>,
}
impl Trace {
pub(crate) fn new(out: Box<dyn Write + Send>) -> Self {
Self {
out: Mutex::new(out),
}
}
fn line(&self, port: &str, what: &str, bytes: &[u8]) {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let mut line = format!("{:.6} {port} {what}", now.as_secs_f64());
if !bytes.is_empty() {
let _ = write!(line, " {}", bytes.len());
for byte in bytes {
let _ = write!(line, " {byte:02x}");
}
let text: String = bytes
.iter()
.map(|&b| {
if b.is_ascii_graphic() || b == b' ' {
b as char
} else {
'.'
}
})
.collect();
let _ = write!(line, " |{text}|");
}
line.push('\n');
let _ = self.out.lock().unwrap().write_all(line.as_bytes());
}
fn flush(&self) {
let _ = self.out.lock().unwrap().flush();
}
}
pub(crate) fn wrap(path: &Path, port: Result<Box<dyn Port>>) -> Result<Box<dyn Port>> {
static TRACE: OnceLock<Option<std::result::Result<Arc<Trace>, String>>> = OnceLock::new();
let trace =
TRACE.get_or_init(|| std::env::var_os("ANYBOX_TRACE").map(|file| create(Path::new(&file))));
match trace {
None => port,
Some(Ok(trace)) => traced(path, port, trace.clone()),
Some(Err(message)) => Err(Error::Io(io::Error::other(message.clone()))),
}
}
pub(crate) fn create(file: &Path) -> std::result::Result<Arc<Trace>, String> {
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
std::os::unix::fs::OpenOptionsExt::mode(&mut options, 0o600);
let opened = options.open(file);
#[cfg(unix)]
let opened = opened.and_then(|f| {
use std::os::unix::fs::PermissionsExt;
f.set_permissions(std::fs::Permissions::from_mode(0o600))?;
Ok(f)
});
opened
.map(|f| Arc::new(Trace::new(Box::new(BufWriter::with_capacity(1 << 20, f)))))
.map_err(|error| format!("couldn't create the trace file {}: {error}", file.display()))
}
pub(crate) fn traced(
path: &Path,
port: Result<Box<dyn Port>>,
trace: Arc<Trace>,
) -> Result<Box<dyn Port>> {
let path = path.display().to_string();
match port {
Ok(port) => {
trace.line(&path, "open", &[]);
Ok(Box::new(Traced { port, path, trace }))
}
Err(error) => {
trace.line(&path, &format!("open failed {error}"), &[]);
trace.flush();
Err(error)
}
}
}
struct Traced {
port: Box<dyn Port>,
path: String,
trace: Arc<Trace>,
}
impl Port for Traced {
fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
let read = self.port.read(buffer);
match &read {
Ok(0) => self.trace.line(&self.path, "eof", &[]),
Ok(n) => self.trace.line(&self.path, "read", &buffer[..*n]),
Err(error) if error.kind() == io::ErrorKind::TimedOut => {}
Err(error) => self
.trace
.line(&self.path, &format!("read error {error}"), &[]),
}
read
}
fn write_all(&self, bytes: &[u8]) -> io::Result<()> {
self.trace.line(&self.path, "write", bytes);
let written = self.port.write_all(bytes);
match &written {
Ok(()) => self.trace.line(&self.path, "write ok", &[]),
Err(error) => self
.trace
.line(&self.path, &format!("write failed {error}"), &[]),
}
written
}
fn wake(&self) {
self.port.wake();
}
fn retire(&self) {
self.port.retire();
}
}
impl Drop for Traced {
fn drop(&mut self) {
self.trace.line(&self.path, "close", &[]);
self.trace.flush();
}
}
#[cfg(test)]
mod tests;