mod app;
mod bg_indexer;
mod clipboard;
mod color;
mod completion;
mod cursor;
mod event;
mod filter_apply;
mod keys;
mod live;
#[doc(hidden)]
pub mod loader;
mod owned_packet;
mod state;
mod stats_collect;
mod stream;
mod tree;
mod ui;
mod widgets;
use std::path::PathBuf;
use packet_dissector::registry::DissectorRegistry;
use crate::decode_as;
use crate::error::{DsctError, Result};
fn cache_dir() -> Option<PathBuf> {
if let Ok(cache) = std::env::var("XDG_CACHE_HOME") {
return Some(PathBuf::from(cache).join("dsct"));
}
if let Ok(home) = std::env::var("HOME") {
return Some(PathBuf::from(home).join(".cache/dsct"));
}
None
}
pub fn run(file: PathBuf, decode_as_args: Vec<String>) -> Result<()> {
let mut registry = DissectorRegistry::default();
decode_as::parse_and_apply(&mut registry, &decode_as_args)?;
let capture = loader::open_and_mmap(&file)?;
let total_bytes = capture.as_bytes().len();
let bg_indexer = bg_indexer::BackgroundIndexer::spawn(&file, total_bytes)?;
let indices = Vec::new();
let mut app = app::App::new(capture, indices, registry, &file);
app.bg_indexer = Some(bg_indexer);
let mut terminal = event::init_terminal()?;
let result = event::run_event_loop(&mut terminal, app);
let _ = event::restore_terminal(&mut terminal);
result
}
pub fn run_live(decode_as_args: Vec<String>) -> Result<()> {
let mut registry = DissectorRegistry::default();
decode_as::parse_and_apply(&mut registry, &decode_as_args)?;
let temp_file = if let Some(dir) = cache_dir() {
std::fs::create_dir_all(&dir)?;
tempfile::NamedTempFile::new_in(dir)?
} else {
tempfile::NamedTempFile::new()?
};
let file = temp_file.as_file().try_clone()?;
let pipe_read = dup_stdin()?;
let copier = live::StdinCopier::spawn(pipe_read, &file)?;
eprint!("Waiting for capture data on stdin...");
match wait_for_first_data(&copier) {
WaitResult::Data => eprintln!(" ready."),
WaitResult::Timeout => {
eprintln!(" starting (data will appear when the upstream program flushes).");
}
WaitResult::Eof => {
eprintln!();
return Err(DsctError::msg(
"stdin closed before any capture data was received",
));
}
}
redirect_stdin_to_tty()?;
let mut terminal = event::init_terminal()?;
let result = (|| {
let capture = state::CaptureMap::new_live(file)?;
let indices = Vec::new();
let app = app::App::new_live(capture, indices, registry, copier);
event::run_event_loop(&mut terminal, app)
})();
let _ = event::restore_terminal(&mut terminal);
drop(temp_file);
result
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WaitResult {
Data,
Timeout,
Eof,
}
const FIRST_DATA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
fn wait_for_first_data(copier: &live::StdinCopier) -> WaitResult {
use std::sync::atomic::Ordering;
use std::time::Instant;
let deadline = Instant::now() + FIRST_DATA_TIMEOUT;
loop {
if copier.bytes_written.load(Ordering::Acquire) > 0 {
return WaitResult::Data;
}
if copier.eof.load(Ordering::Acquire) {
return WaitResult::Eof;
}
if Instant::now() >= deadline {
return WaitResult::Timeout;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[cfg(unix)]
fn dup_stdin() -> Result<std::fs::File> {
let pipe_fd = rustix::io::dup(rustix::stdio::stdin())?;
Ok(std::fs::File::from(pipe_fd))
}
#[cfg(not(unix))]
fn dup_stdin() -> Result<std::fs::File> {
Err(DsctError::msg(
"reading from stdin pipe is not supported on this platform",
))
}
#[cfg(unix)]
fn redirect_stdin_to_tty() -> Result<()> {
let tty = std::fs::File::open("/dev/tty")?;
rustix::stdio::dup2_stdin(&tty)?;
Ok(())
}
#[cfg(not(unix))]
fn redirect_stdin_to_tty() -> Result<()> {
Err(DsctError::msg(
"reading from stdin pipe is not supported on this platform",
))
}
#[cfg(test)]
#[allow(unsafe_code)]
mod tests {
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
fn env_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn fake_copier(bytes: u64, eof: bool) -> live::StdinCopier {
let bw = Arc::new(AtomicU64::new(bytes));
let done = Arc::new(AtomicBool::new(eof));
live::StdinCopier {
bytes_written: bw,
eof: done,
handle: None,
}
}
#[test]
fn wait_returns_data_when_bytes_present() {
let copier = fake_copier(100, false);
assert_eq!(wait_for_first_data(&copier), WaitResult::Data);
}
#[test]
fn wait_returns_eof_without_data() {
let copier = fake_copier(0, true);
assert_eq!(wait_for_first_data(&copier), WaitResult::Eof);
}
#[test]
fn wait_unblocks_when_data_arrives() {
let copier = fake_copier(0, false);
let bw = Arc::clone(&copier.bytes_written);
let handle = std::thread::spawn(move || wait_for_first_data(&copier));
std::thread::sleep(std::time::Duration::from_millis(100));
bw.store(24, Ordering::Release);
assert_eq!(handle.join().unwrap(), WaitResult::Data);
}
#[test]
fn cache_dir_prefers_xdg_cache_home() {
let _lock = env_lock();
unsafe {
std::env::set_var("XDG_CACHE_HOME", "/tmp/xdg-test-cache");
std::env::set_var("HOME", "/home/dummy");
}
let dir = cache_dir();
assert_eq!(dir, Some(PathBuf::from("/tmp/xdg-test-cache/dsct")));
unsafe {
std::env::remove_var("XDG_CACHE_HOME");
std::env::remove_var("HOME");
}
}
#[test]
fn cache_dir_falls_back_to_home() {
let _lock = env_lock();
unsafe {
std::env::remove_var("XDG_CACHE_HOME");
std::env::set_var("HOME", "/home/dummy");
}
let dir = cache_dir();
assert_eq!(dir, Some(PathBuf::from("/home/dummy/.cache/dsct")));
unsafe {
std::env::remove_var("HOME");
}
}
#[test]
fn cache_dir_returns_none_without_env() {
let _lock = env_lock();
unsafe {
std::env::remove_var("XDG_CACHE_HOME");
std::env::remove_var("HOME");
}
let dir = cache_dir();
assert_eq!(dir, None);
}
#[test]
fn wait_returns_timeout_when_no_data() {
let copier = fake_copier(0, false);
let handle = std::thread::spawn(move || {
let start = std::time::Instant::now();
let result = wait_for_first_data(&copier);
(result, start.elapsed())
});
let (result, elapsed) = handle.join().unwrap();
assert_eq!(result, WaitResult::Timeout);
assert!(
elapsed < std::time::Duration::from_secs(10),
"waited too long: {elapsed:?}"
);
}
}