mod app;
mod event_loop;
mod keymap;
mod log_layer;
mod message;
mod terminal;
mod view;
mod worker;
#[cfg(test)]
mod tests;
use std::io::Write as _;
use std::net::Ipv4Addr;
use std::path::Path;
use std::sync::Mutex;
use std::time::Duration;
use ratatui::crossterm::event::EventStream;
use tokio::sync::mpsc;
use self::app::picker::Picker;
use self::app::{App, AppConfig};
use self::event_loop::LoopIo;
use self::log_layer::{LogRing, RingLayer, LOG_CAPACITY};
use self::terminal::Signals;
use self::worker::session::{self, ConnectPlan, TransportKind};
use self::worker::{EventSink, COMMAND_CHANNEL_CAPACITY, EVENT_CHANNEL_CAPACITY};
use crate::args::{Cli, Command};
use crate::core::interfaces::list_ipv4_interfaces;
use crate::transport::TransportArgs;
const STOP_TIMEOUT: Duration = Duration::from_secs(3);
pub(crate) async fn run(cli: Cli) -> crate::CliResult {
if let Err(hint) = terminal::check_terminal() {
eprintln!("{hint}");
std::process::exit(1);
}
let (fps, log_file) = match &cli.command {
Some(Command::Tui { fps, log_file }) => (*fps, log_file.clone()),
_ => (20, None),
};
let kind = if cli.sc {
TransportKind::Sc
} else if cli.ipv6 {
TransportKind::Bip6
} else {
TransportKind::Bip
};
if kind == TransportKind::Sc && !cfg!(feature = "sc-tls") {
eprintln!("Error: BACnet/SC requires the 'sc-tls' feature. Rebuild with: cargo install bacnet-cli --features sc-tls");
std::process::exit(1);
}
let log = LogRing::new(LOG_CAPACITY);
if let Err(error) = init_tracing(cli.verbose, log_file.as_deref(), &log) {
eprintln!("Error: {error}");
std::process::exit(1);
}
let (interface, broadcast, picker) = choose_interface(&cli, kind, &log);
let args = match TransportArgs::from_cli(&cli, interface, broadcast) {
Ok(args) => args,
Err(error) => {
eprintln!("Error: {error}");
std::process::exit(1);
}
};
let plan = ConnectPlan {
kind,
args,
await_interface: picker.is_some(),
};
let color = std::env::var_os("NO_COLOR").is_none_or(|v| v.is_empty());
let mut app = Box::new(App::new(
AppConfig {
transport: kind.label(),
style: kind.style(),
color,
picker,
now: tokio::time::Instant::now().into_std(),
},
log,
));
let signals = Signals::install()?;
let mut terminal = match terminal::enter() {
Ok(terminal) => terminal,
Err(error) => {
eprintln!(
"{}",
terminal::unusable_hint(&format!("raw mode failed: {error}"))
);
std::process::exit(1);
}
};
let (sink, events) = EventSink::channel(EVENT_CHANNEL_CAPACITY);
let dropped = sink.dropped_counter();
let (commands, command_rx) = mpsc::channel(COMMAND_CHANNEL_CAPACITY);
let worker = session::spawn(plan, sink, command_rx);
let mut io = LoopIo {
input: EventStream::new(),
events,
commands,
dropped,
fps,
signals,
alive: terminal::is_active,
};
let result = Box::pin(event_loop::run(&mut terminal, &mut app, &mut io)).await;
terminal::restore();
drop(terminal);
let LoopIo {
input,
events,
commands,
mut signals,
..
} = io;
drop((input, events, commands));
let mut signal = None;
tokio::select! {
joined = tokio::time::timeout(STOP_TIMEOUT, worker) => {
if joined.is_err() {
tracing::warn!("the network worker did not stop in time");
}
}
received = signals.recv() => signal = Some(received),
}
let (error, code) = match (result, signal) {
(Err(error), _) => (Some(error.to_string()), 1),
(Ok(_), Some(signal)) if app.exit_error.is_none() => {
(Some(format!("terminated by {}", signal.name)), signal.code)
}
(Ok(_), _) => (app.exit_error.take(), app.exit_code.unwrap_or(1)),
};
if let Some(error) = error {
let _ = writeln!(std::io::stderr(), "Error: {error}");
std::process::exit(code);
}
Ok(())
}
fn choose_interface(
cli: &Cli,
kind: TransportKind,
log: &LogRing,
) -> (Ipv4Addr, Ipv4Addr, Option<Picker>) {
if let Some(interface) = cli.interface {
return (interface, cli.broadcast, None);
}
if kind != TransportKind::Bip {
return (Ipv4Addr::UNSPECIFIED, cli.broadcast, None);
}
let mut interfaces = list_ipv4_interfaces();
match interfaces.len() {
0 => {
log.info("No network interfaces found; binding to 0.0.0.0.");
(Ipv4Addr::UNSPECIFIED, Ipv4Addr::BROADCAST, None)
}
1 => {
let only = interfaces.remove(0);
log.info(format!(
"Using interface {} ({}, broadcast {}).",
only.name, only.ip, only.broadcast
));
(only.ip, only.broadcast, None)
}
_ => (
Ipv4Addr::UNSPECIFIED,
cli.broadcast,
Some(Picker::new(interfaces)),
),
}
}
fn init_tracing(verbosity: u8, log_file: Option<&Path>, ring: &LogRing) -> Result<(), String> {
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::EnvFilter;
let level = match verbosity {
0 => "info",
1 => "debug",
_ => "trace",
};
let file_layer = match log_file {
Some(path) => {
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| format!("cannot open --log-file {}: {e}", path.display()))?;
Some(
tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_target(false)
.with_writer(Mutex::new(file)),
)
}
None => None,
};
tracing_subscriber::registry()
.with(EnvFilter::new(level))
.with(RingLayer::new(ring.clone()))
.with(file_layer)
.try_init()
.map_err(|e| e.to_string())
}