use std::path::PathBuf;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use super::{convert, format_message, interface, legacy, replay, tui};
use crate::{n2kd, server};
#[derive(Debug, Parser)]
#[command(
name = "canboat",
about = "The canboat NMEA 2000 toolkit",
version,
after_help = crate::cli::help_footer()
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Subcommand)]
enum Command {
#[command(long_about = convert::LONG_ABOUT)]
Convert(convert::Args),
FormatMessage(format_message::Args),
Interface(interface::Args),
Server(Box<server::Args>),
Tui(tui::Args),
Replay(replay::Args),
N2kd(Box<n2kd::app::Args>),
InstallShims {
#[arg(long, value_name = "DIR")]
dir: Option<PathBuf>,
},
}
pub fn main() -> ExitCode {
let argv = match legacy::route(crate::cli::canboat_argv()) {
legacy::Route::Alias(result) => return finish(result),
legacy::Route::Canboat(argv) => argv,
};
let cli = Cli::parse_from(argv);
let result = match cli.command {
Command::Convert(args) => convert::run(args),
Command::FormatMessage(args) => format_message::run(args),
Command::Interface(args) => interface::run(args),
Command::Server(args) => {
args.warn_deprecated();
if let Err(e) = args.check() {
return finish(Err(e));
}
let mut config: server::BridgeConfig = (*args).into();
if config.config_dir.is_none() {
config.config_dir = Some(resolve_config_dir());
}
let level = if config.quiet {
"error"
} else if config.verbose {
"debug,nusb=info"
} else {
"info"
};
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(level))
.init();
crate::cli::log_startup(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
run_server(config)
}
Command::Tui(args) => run_tui(args),
Command::Replay(args) => replay::run(args),
Command::N2kd(args) => n2kd::app::run(*args),
Command::InstallShims { dir } => install_shims(dir),
};
finish(result)
}
fn run_server(config: server::BridgeConfig) -> anyhow::Result<()> {
let mut bridge = server::Bridge::new(config)?;
bridge.serve()?;
bridge.spawn()?;
super::stop_signal::install();
while bridge.is_running() && !super::stop_signal::received() {
std::thread::sleep(std::time::Duration::from_millis(100));
}
if super::stop_signal::received() {
log::info!("stopping on signal");
bridge.shutdown();
crate::engine::summarize_incomplete();
} else {
bridge.wait();
}
match bridge.failure() {
Some(why) => anyhow::bail!("{why}"),
None => Ok(()),
}
}
fn finish(result: anyhow::Result<()>) -> ExitCode {
if let Err(e) = result {
eprintln!("canboat: {e:#}");
return ExitCode::from(1);
}
ExitCode::SUCCESS
}
fn install_shims(dir: Option<PathBuf>) -> anyhow::Result<()> {
use anyhow::Context;
let dir = match dir {
Some(d) => d,
None => std::env::current_exe()
.context("locating the canboat executable")?
.parent()
.context("canboat executable has no parent directory")?
.to_path_buf(),
};
legacy::install_shims(&dir)
}
pub(super) fn resolve_config_dir() -> PathBuf {
let system = PathBuf::from("/etc/default/canboat");
if dir_is_writable(&system) {
return system;
}
if let Ok(home) = std::env::var("HOME") {
return PathBuf::from(home).join(".local/canboat");
}
system
}
fn dir_is_writable(dir: &std::path::Path) -> bool {
if std::fs::create_dir_all(dir).is_err() {
return false;
}
let probe = dir.join(".canboat-write-test");
match std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(&probe)
{
Ok(_) => {
let _ = std::fs::remove_file(&probe);
true
}
Err(_) => false,
}
}
fn run_tui(args: tui::Args) -> anyhow::Result<()> {
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()?;
rt.block_on(tui::run(args))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_command_line_is_consistent() {
use clap::CommandFactory;
Cli::command().debug_assert();
}
}