mod capture;
mod cli;
mod decode;
mod elevation;
mod install;
mod output;
mod platform_guard;
mod resolve;
use anyhow::{Context, Result, bail};
use clap::Parser;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crate::capture::Event;
use crate::output::TextSink;
fn main() -> Result<()> {
platform_guard::check();
let args = cli::Args::parse();
match install::ensure_capture_driver_installed()? {
install::Outcome::AlreadyInstalled => {}
install::Outcome::JustInstalled | install::Outcome::ElevatedChildSucceeded => {
eprintln!("USBPcap installed.");
eprintln!(
"→ Unplug and replug your USB device (or reboot), then re-run this command."
);
return Ok(());
}
}
#[cfg(target_os = "windows")]
require_admin_for_capture()?;
let port = match args.port.as_deref() {
Some(p) => p.to_string(),
None => auto_detect_port()?,
};
let info = resolve::resolve(&port)
.with_context(|| format!("resolving port '{port}'"))?;
eprintln!(
"→ Port {}: bus {} device {} VID:PID {:04x}:{:04x}{}",
port,
info.bus,
info.devnum,
info.vid,
info.pid,
info.interface_number
.map(|n| format!(" interface {n}"))
.unwrap_or_default(),
);
#[cfg(target_os = "linux")]
install::linux_usbmon::ensure_ready(info.bus, args.yes)?;
let decoder = decode::select(
&info,
decode::Options {
ftdi_mps_override: args.ftdi_mps,
},
);
eprintln!(
"→ Decoder: {}{}",
decoder.name(),
match (info.bulk_in_ep, info.bulk_out_ep) {
(Some(i), Some(o)) => format!(" (bulk IN 0x{i:02x}, OUT 0x{o:02x})"),
(Some(i), None) => format!(" (bulk IN 0x{i:02x})"),
(None, Some(o)) => format!(" (bulk OUT 0x{o:02x})"),
_ => String::new(),
}
);
eprintln!("→ Logging to {}", describe_dest(args.output.as_deref(), args.quiet));
let mut sink = TextSink::create(
args.output.as_deref(),
!args.quiet,
args.format,
args.printable_only,
)?;
let options = capture::PassiveOptions {
pcap_path: args.pcap.as_deref(),
usbpcap_override: args.usbpcap.as_deref(),
};
if let Some(p) = options.pcap_path {
eprintln!("→ Pcapng to {}", p.display());
}
eprintln!("→ Press Ctrl-C to stop.");
let written = Arc::new(AtomicUsize::new(0));
install_sigint_handler(written.clone(), args.output.as_deref(), options.pcap_path);
let on_event = {
let written = written.clone();
move |ev: Event| -> Result<()> {
if sink.write_event(&ev)? {
written.fetch_add(1, Ordering::Relaxed);
}
Ok(())
}
};
capture::run_passive(info, decoder, options, on_event)
}
#[cfg(target_os = "windows")]
fn require_admin_for_capture() -> Result<()> {
if elevation::is_elevated()? {
return Ok(());
}
eprintln!("USBPcap requires Administrator privileges to capture.");
eprintln!();
eprintln!("Re-run from an elevated shell:");
eprintln!(r#" • Right-click PowerShell or Windows Terminal → "Run as administrator","#);
eprintln!(" then re-run your command, or");
eprintln!(" • From any PowerShell window:");
eprintln!(" Start-Process powershell -Verb RunAs");
eprintln!();
eprintln!(
r"(USBPcap's installer sets an Administrators-only ACL on \\.\USBPcapN."
);
eprintln!("Capture inherits that, so the binary itself must be elevated.)");
bail!("not elevated")
}
fn auto_detect_port() -> Result<String> {
let listed = resolve::list_ports().context("listing USB serial ports")?;
match listed.len() {
0 => bail!(
"no USB serial devices detected. Plug in a device or pass --port."
),
1 => {
let p = &listed[0];
eprintln!(
"→ Auto-detected port: {} (VID:PID {:04x}:{:04x})",
p.path, p.vid, p.pid
);
Ok(p.path.clone())
}
_ => {
let mut msg = String::from(
"multiple USB serial devices detected; pass --port to choose:\n",
);
for p in &listed {
msg.push_str(&format!(
" {} (VID:PID {:04x}:{:04x})\n",
p.path, p.vid, p.pid
));
}
bail!("{msg}")
}
}
}
fn describe_dest(output: Option<&std::path::Path>, quiet: bool) -> String {
match (output, quiet) {
(Some(p), _) => p.display().to_string(),
(None, false) => "stdout".to_string(),
(None, true) => "(discarded — --quiet without --output)".to_string(),
}
}
fn install_sigint_handler(
written: Arc<AtomicUsize>,
output: Option<&std::path::Path>,
pcap: Option<&std::path::Path>,
) {
let output = output.map(|p| p.to_owned());
let pcap = pcap.map(|p| p.to_owned());
let _ = ctrlc::set_handler(move || {
let n = written.load(Ordering::Relaxed);
eprintln!();
match output.as_ref() {
Some(p) => eprintln!("Stopped. Wrote {n} event(s) to {}.", p.display()),
None => eprintln!("Stopped. Wrote {n} event(s)."),
}
if let Some(p) = pcap.as_ref() {
eprintln!("Pcapng saved to {}.", p.display());
}
std::process::exit(0);
});
}