Add base subcrates
This commit is contained in:
736
crates/wtfnet-cli/src/main.rs
Normal file
736
crates/wtfnet-cli/src/main.rs
Normal file
@@ -0,0 +1,736 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
use wtfnet_core::{
|
||||
init_logging, CommandEnvelope, CommandInfo, ErrItem, ExitKind, LogFormat, LogLevel,
|
||||
LoggingConfig, Meta,
|
||||
};
|
||||
use wtfnet_platform::{Platform, PlatformError};
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "wtfn",
|
||||
version,
|
||||
about = "WTFnet CLI toolbox",
|
||||
arg_required_else_help = true
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(long)]
|
||||
json: bool,
|
||||
#[arg(long)]
|
||||
pretty: bool,
|
||||
#[arg(long)]
|
||||
no_color: bool,
|
||||
#[arg(long)]
|
||||
quiet: bool,
|
||||
#[arg(short = 'v', action = clap::ArgAction::Count)]
|
||||
verbose: u8,
|
||||
#[arg(long)]
|
||||
log_level: Option<String>,
|
||||
#[arg(long)]
|
||||
log_format: Option<String>,
|
||||
#[arg(long)]
|
||||
log_file: Option<PathBuf>,
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Commands {
|
||||
Sys {
|
||||
#[command(subcommand)]
|
||||
command: SysCommand,
|
||||
},
|
||||
Ports {
|
||||
#[command(subcommand)]
|
||||
command: PortsCommand,
|
||||
},
|
||||
Neigh {
|
||||
#[command(subcommand)]
|
||||
command: NeighCommand,
|
||||
},
|
||||
Cert {
|
||||
#[command(subcommand)]
|
||||
command: CertCommand,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum SysCommand {
|
||||
Ifaces,
|
||||
Ip(SysIpArgs),
|
||||
Route(SysRouteArgs),
|
||||
Dns,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum PortsCommand {
|
||||
Listen(PortsListenArgs),
|
||||
Who(PortsWhoArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum NeighCommand {
|
||||
List(NeighListArgs),
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum CertCommand {
|
||||
Roots,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct SysIpArgs {
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
#[arg(long)]
|
||||
iface: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct SysRouteArgs {
|
||||
#[arg(long)]
|
||||
ipv4: bool,
|
||||
#[arg(long)]
|
||||
ipv6: bool,
|
||||
#[arg(long)]
|
||||
to: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct PortsListenArgs {
|
||||
#[arg(long)]
|
||||
tcp: bool,
|
||||
#[arg(long)]
|
||||
udp: bool,
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct PortsWhoArgs {
|
||||
target: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
struct NeighListArgs {
|
||||
#[arg(long)]
|
||||
ipv4: bool,
|
||||
#[arg(long)]
|
||||
ipv6: bool,
|
||||
#[arg(long)]
|
||||
iface: Option<String>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let cli = Cli::parse();
|
||||
let config = logging_config_from_cli(&cli);
|
||||
if let Err(err) = init_logging(&config) {
|
||||
eprintln!("failed to initialize logging: {err}");
|
||||
std::process::exit(ExitKind::Failed.code());
|
||||
}
|
||||
|
||||
let exit_code = match &cli.command {
|
||||
Commands::Sys {
|
||||
command: SysCommand::Ifaces,
|
||||
} => handle_sys_ifaces(&cli).await,
|
||||
Commands::Sys {
|
||||
command: SysCommand::Ip(args),
|
||||
} => handle_sys_ip(&cli, args.clone()).await,
|
||||
Commands::Sys {
|
||||
command: SysCommand::Route(args),
|
||||
} => handle_sys_route(&cli, args.clone()).await,
|
||||
Commands::Sys {
|
||||
command: SysCommand::Dns,
|
||||
} => handle_sys_dns(&cli).await,
|
||||
Commands::Ports {
|
||||
command: PortsCommand::Listen(args),
|
||||
} => handle_ports_listen(&cli, args.clone()).await,
|
||||
Commands::Ports {
|
||||
command: PortsCommand::Who(args),
|
||||
} => handle_ports_who(&cli, args.clone()).await,
|
||||
Commands::Neigh {
|
||||
command: NeighCommand::List(args),
|
||||
} => handle_neigh_list(&cli, args.clone()).await,
|
||||
Commands::Cert {
|
||||
command: CertCommand::Roots,
|
||||
} => handle_cert_roots(&cli).await,
|
||||
};
|
||||
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
fn platform() -> Platform {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
return wtfnet_platform_windows::platform();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
return wtfnet_platform_linux::platform();
|
||||
}
|
||||
|
||||
#[cfg(not(any(windows, target_os = "linux")))]
|
||||
{
|
||||
panic!("unsupported platform");
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sys_ifaces(cli: &Cli) -> i32 {
|
||||
let result = platform().sys.interfaces().await;
|
||||
match result {
|
||||
Ok(interfaces) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("sys ifaces", Vec::new());
|
||||
let envelope = CommandEnvelope::new(meta, command, interfaces);
|
||||
let json = if cli.pretty {
|
||||
serde_json::to_string_pretty(&envelope)
|
||||
} else {
|
||||
serde_json::to_string(&envelope)
|
||||
};
|
||||
match json {
|
||||
Ok(payload) => {
|
||||
println!("{payload}");
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to serialize json: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for iface in interfaces {
|
||||
println!("{}", iface.name);
|
||||
if let Some(index) = iface.index {
|
||||
println!(" index: {index}");
|
||||
}
|
||||
if let Some(mac) = iface.mac {
|
||||
println!(" mac: {mac}");
|
||||
}
|
||||
if let Some(mtu) = iface.mtu {
|
||||
println!(" mtu: {mtu}");
|
||||
}
|
||||
if let Some(is_up) = iface.is_up {
|
||||
println!(" state: {}", if is_up { "up" } else { "down" });
|
||||
}
|
||||
for addr in iface.addresses {
|
||||
let prefix = addr
|
||||
.prefix_len
|
||||
.map(|value| format!("/{value}"))
|
||||
.unwrap_or_default();
|
||||
if let Some(scope) = addr.scope {
|
||||
println!(" addr: {}{} ({})", addr.ip, prefix, scope);
|
||||
} else {
|
||||
println!(" addr: {}{}", addr.ip, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sys_ip(cli: &Cli, args: SysIpArgs) -> i32 {
|
||||
let result = platform().sys.interfaces().await;
|
||||
match result {
|
||||
Ok(interfaces) => {
|
||||
let filtered = filter_interfaces_for_ip(interfaces, &args);
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let mut command_args = Vec::new();
|
||||
if args.all {
|
||||
command_args.push("--all".to_string());
|
||||
}
|
||||
if let Some(iface) = args.iface.as_ref() {
|
||||
command_args.push("--iface".to_string());
|
||||
command_args.push(iface.clone());
|
||||
}
|
||||
let command = CommandInfo::new("sys ip", command_args);
|
||||
let envelope = CommandEnvelope::new(meta, command, filtered);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for iface in filtered {
|
||||
println!("{}", iface.name);
|
||||
for addr in iface.addresses {
|
||||
let prefix = addr
|
||||
.prefix_len
|
||||
.map(|value| format!("/{value}"))
|
||||
.unwrap_or_default();
|
||||
if let Some(scope) = addr.scope {
|
||||
println!(" addr: {}{} ({})", addr.ip, prefix, scope);
|
||||
} else {
|
||||
println!(" addr: {}{}", addr.ip, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sys_route(cli: &Cli, args: SysRouteArgs) -> i32 {
|
||||
let result = platform().sys.routes().await;
|
||||
match result {
|
||||
Ok(routes) => {
|
||||
let filtered = filter_routes(routes, &args);
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let mut command_args = Vec::new();
|
||||
if args.ipv4 {
|
||||
command_args.push("--ipv4".to_string());
|
||||
}
|
||||
if args.ipv6 {
|
||||
command_args.push("--ipv6".to_string());
|
||||
}
|
||||
if let Some(target) = args.to.as_ref() {
|
||||
command_args.push("--to".to_string());
|
||||
command_args.push(target.clone());
|
||||
}
|
||||
let command = CommandInfo::new("sys route", command_args);
|
||||
let envelope = CommandEnvelope::new(meta, command, filtered);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for route in filtered {
|
||||
let gateway = route.gateway.unwrap_or_else(|| "-".to_string());
|
||||
let iface = route.interface.unwrap_or_else(|| "-".to_string());
|
||||
if let Some(metric) = route.metric {
|
||||
println!(
|
||||
"{} via {} dev {} metric {}",
|
||||
route.destination, gateway, iface, metric
|
||||
);
|
||||
} else {
|
||||
println!("{} via {} dev {}", route.destination, gateway, iface);
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_sys_dns(cli: &Cli) -> i32 {
|
||||
let result = platform().sys.dns_config().await;
|
||||
match result {
|
||||
Ok(snapshot) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("sys dns", Vec::new());
|
||||
let envelope = CommandEnvelope::new(meta, command, snapshot);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
println!("servers:");
|
||||
if snapshot.servers.is_empty() {
|
||||
println!(" -");
|
||||
} else {
|
||||
for server in snapshot.servers {
|
||||
println!(" {server}");
|
||||
}
|
||||
}
|
||||
println!("search:");
|
||||
if snapshot.search_domains.is_empty() {
|
||||
println!(" -");
|
||||
} else {
|
||||
for domain in snapshot.search_domains {
|
||||
println!(" {domain}");
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ports_listen(cli: &Cli, args: PortsListenArgs) -> i32 {
|
||||
let result = platform().ports.listening().await;
|
||||
match result {
|
||||
Ok(sockets) => {
|
||||
let filtered = filter_ports(sockets, &args);
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let mut command_args = Vec::new();
|
||||
if args.tcp {
|
||||
command_args.push("--tcp".to_string());
|
||||
}
|
||||
if args.udp {
|
||||
command_args.push("--udp".to_string());
|
||||
}
|
||||
if let Some(port) = args.port {
|
||||
command_args.push("--port".to_string());
|
||||
command_args.push(port.to_string());
|
||||
}
|
||||
let command = CommandInfo::new("ports listen", command_args);
|
||||
let envelope = CommandEnvelope::new(meta, command, filtered);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for socket in filtered {
|
||||
if let Some(state) = socket.state.as_ref() {
|
||||
println!(
|
||||
"{} {} {} pid={}",
|
||||
socket.proto,
|
||||
socket.local_addr,
|
||||
state,
|
||||
socket.pid.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} {} pid={}",
|
||||
socket.proto,
|
||||
socket.local_addr,
|
||||
socket.pid.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_ports_who(cli: &Cli, args: PortsWhoArgs) -> i32 {
|
||||
let port = match parse_port_arg(&args.target) {
|
||||
Some(port) => port,
|
||||
None => {
|
||||
eprintln!("invalid port: {}", args.target);
|
||||
return ExitKind::Usage.code();
|
||||
}
|
||||
};
|
||||
let result = platform().ports.who_owns(port).await;
|
||||
match result {
|
||||
Ok(sockets) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("ports who", vec![args.target]);
|
||||
let envelope = CommandEnvelope::new(meta, command, sockets);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for socket in sockets {
|
||||
println!(
|
||||
"{} {} pid={}",
|
||||
socket.proto,
|
||||
socket.local_addr,
|
||||
socket.pid.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
|
||||
);
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_neigh_list(cli: &Cli, args: NeighListArgs) -> i32 {
|
||||
let result = platform().neigh.neighbors().await;
|
||||
match result {
|
||||
Ok(neighbors) => {
|
||||
let filtered = filter_neighbors(neighbors, &args);
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let mut command_args = Vec::new();
|
||||
if args.ipv4 {
|
||||
command_args.push("--ipv4".to_string());
|
||||
}
|
||||
if args.ipv6 {
|
||||
command_args.push("--ipv6".to_string());
|
||||
}
|
||||
if let Some(iface) = args.iface.as_ref() {
|
||||
command_args.push("--iface".to_string());
|
||||
command_args.push(iface.clone());
|
||||
}
|
||||
let command = CommandInfo::new("neigh list", command_args);
|
||||
let envelope = CommandEnvelope::new(meta, command, filtered);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for entry in filtered {
|
||||
let mac = entry.mac.unwrap_or_else(|| "-".to_string());
|
||||
let iface = entry.interface.unwrap_or_else(|| "-".to_string());
|
||||
if let Some(state) = entry.state {
|
||||
println!("{} {} {} {}", entry.ip, mac, iface, state);
|
||||
} else {
|
||||
println!("{} {} {}", entry.ip, mac, iface);
|
||||
}
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_cert_roots(cli: &Cli) -> i32 {
|
||||
let result = platform().cert.trusted_roots().await;
|
||||
match result {
|
||||
Ok(roots) => {
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("cert roots", Vec::new());
|
||||
let envelope = CommandEnvelope::new(meta, command, roots);
|
||||
emit_json(cli, &envelope)
|
||||
} else {
|
||||
for root in roots {
|
||||
println!("subject: {}", root.subject);
|
||||
println!("issuer: {}", root.issuer);
|
||||
println!("valid: {} -> {}", root.not_before, root.not_after);
|
||||
println!("sha256: {}", root.sha256);
|
||||
println!("---");
|
||||
}
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
}
|
||||
Err(err) => emit_platform_error(cli, err),
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_interfaces_for_ip(
|
||||
interfaces: Vec<wtfnet_platform::NetInterface>,
|
||||
args: &SysIpArgs,
|
||||
) -> Vec<wtfnet_platform::NetInterface> {
|
||||
let mut filtered = Vec::new();
|
||||
for mut iface in interfaces {
|
||||
if let Some(filter) = args.iface.as_ref() {
|
||||
if iface.name != *filter {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !args.all {
|
||||
iface.addresses.retain(|addr| !is_loopback_ip(&addr.ip));
|
||||
}
|
||||
|
||||
if iface.addresses.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
filtered.push(iface);
|
||||
}
|
||||
filtered
|
||||
}
|
||||
|
||||
fn filter_routes(
|
||||
routes: Vec<wtfnet_platform::RouteEntry>,
|
||||
args: &SysRouteArgs,
|
||||
) -> Vec<wtfnet_platform::RouteEntry> {
|
||||
let mut filtered = Vec::new();
|
||||
for route in routes {
|
||||
if args.ipv4 && !is_ipv4_route(&route.destination) {
|
||||
continue;
|
||||
}
|
||||
if args.ipv6 && !is_ipv6_route(&route.destination) {
|
||||
continue;
|
||||
}
|
||||
if let Some(target) = args.to.as_ref() {
|
||||
if route.destination != *target {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filtered.push(route);
|
||||
}
|
||||
filtered
|
||||
}
|
||||
|
||||
fn is_loopback_ip(value: &str) -> bool {
|
||||
value
|
||||
.parse::<std::net::IpAddr>()
|
||||
.map(|ip| ip.is_loopback())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_ipv4_route(value: &str) -> bool {
|
||||
value.parse::<std::net::Ipv4Addr>().is_ok()
|
||||
}
|
||||
|
||||
fn is_ipv6_route(value: &str) -> bool {
|
||||
value.parse::<std::net::Ipv6Addr>().is_ok()
|
||||
}
|
||||
|
||||
fn filter_ports(
|
||||
sockets: Vec<wtfnet_platform::ListenSocket>,
|
||||
args: &PortsListenArgs,
|
||||
) -> Vec<wtfnet_platform::ListenSocket> {
|
||||
let mut filtered = Vec::new();
|
||||
for socket in sockets {
|
||||
if args.tcp && socket.proto != "tcp" {
|
||||
continue;
|
||||
}
|
||||
if args.udp && socket.proto != "udp" {
|
||||
continue;
|
||||
}
|
||||
if let Some(port) = args.port {
|
||||
if extract_port(&socket.local_addr) != Some(port) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filtered.push(socket);
|
||||
}
|
||||
filtered
|
||||
}
|
||||
|
||||
fn filter_neighbors(
|
||||
neighbors: Vec<wtfnet_platform::NeighborEntry>,
|
||||
args: &NeighListArgs,
|
||||
) -> Vec<wtfnet_platform::NeighborEntry> {
|
||||
let mut filtered = Vec::new();
|
||||
for entry in neighbors {
|
||||
if args.ipv4 && !is_ipv4_addr(&entry.ip) {
|
||||
continue;
|
||||
}
|
||||
if args.ipv6 && !is_ipv6_addr(&entry.ip) {
|
||||
continue;
|
||||
}
|
||||
if let Some(iface) = args.iface.as_ref() {
|
||||
if entry.interface.as_deref() != Some(iface.as_str()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
filtered.push(entry);
|
||||
}
|
||||
filtered
|
||||
}
|
||||
|
||||
fn is_ipv4_addr(value: &str) -> bool {
|
||||
value.parse::<std::net::Ipv4Addr>().is_ok()
|
||||
}
|
||||
|
||||
fn is_ipv6_addr(value: &str) -> bool {
|
||||
value.parse::<std::net::Ipv6Addr>().is_ok()
|
||||
}
|
||||
|
||||
fn extract_port(value: &str) -> Option<u16> {
|
||||
if let Some(pos) = value.rfind(':') {
|
||||
return value[pos + 1..].parse::<u16>().ok();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_port_arg(value: &str) -> Option<u16> {
|
||||
if let Ok(port) = value.parse::<u16>() {
|
||||
return Some(port);
|
||||
}
|
||||
extract_port(value)
|
||||
}
|
||||
|
||||
fn emit_platform_error(cli: &Cli, err: PlatformError) -> i32 {
|
||||
let code = err.code.clone();
|
||||
let message = err.message.clone();
|
||||
if cli.json {
|
||||
let meta = Meta::new("wtfnet", env!("CARGO_PKG_VERSION"), false);
|
||||
let command = CommandInfo::new("sys ifaces", Vec::new());
|
||||
let mut envelope = CommandEnvelope::new(meta, command, serde_json::Value::Null);
|
||||
envelope.errors.push(ErrItem::new(code.clone(), message.clone()));
|
||||
let json = if cli.pretty {
|
||||
serde_json::to_string_pretty(&envelope)
|
||||
} else {
|
||||
serde_json::to_string(&envelope)
|
||||
};
|
||||
if let Ok(payload) = json {
|
||||
println!("{payload}");
|
||||
} else if let Ok(payload) = serde_json::to_string(&envelope) {
|
||||
println!("{payload}");
|
||||
}
|
||||
} else {
|
||||
eprintln!("{message}");
|
||||
}
|
||||
|
||||
match code {
|
||||
wtfnet_core::ErrorCode::PermissionDenied => ExitKind::Permission.code(),
|
||||
wtfnet_core::ErrorCode::Timeout => ExitKind::Timeout.code(),
|
||||
wtfnet_core::ErrorCode::InvalidArgs => ExitKind::Usage.code(),
|
||||
wtfnet_core::ErrorCode::Partial => ExitKind::Partial.code(),
|
||||
_ => ExitKind::Failed.code(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_json<T: serde::Serialize>(cli: &Cli, envelope: &CommandEnvelope<T>) -> i32 {
|
||||
let json = if cli.pretty {
|
||||
serde_json::to_string_pretty(envelope)
|
||||
} else {
|
||||
serde_json::to_string(envelope)
|
||||
};
|
||||
match json {
|
||||
Ok(payload) => {
|
||||
println!("{payload}");
|
||||
ExitKind::Ok.code()
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed to serialize json: {err}");
|
||||
ExitKind::Failed.code()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn logging_config_from_cli(cli: &Cli) -> LoggingConfig {
|
||||
if cli.quiet {
|
||||
return LoggingConfig {
|
||||
level: LogLevel::Error,
|
||||
format: parse_log_format(cli.log_format.as_deref())
|
||||
.or_else(env_log_format)
|
||||
.unwrap_or(LogFormat::Text),
|
||||
log_file: cli.log_file.clone().or_else(env_log_file),
|
||||
};
|
||||
}
|
||||
|
||||
let level = parse_log_level(cli.log_level.as_deref())
|
||||
.or_else(env_log_level)
|
||||
.unwrap_or_else(|| level_from_verbosity(cli.verbose));
|
||||
|
||||
LoggingConfig {
|
||||
level,
|
||||
format: parse_log_format(cli.log_format.as_deref())
|
||||
.or_else(env_log_format)
|
||||
.unwrap_or(LogFormat::Text),
|
||||
log_file: cli.log_file.clone().or_else(env_log_file),
|
||||
}
|
||||
}
|
||||
|
||||
fn level_from_verbosity(count: u8) -> LogLevel {
|
||||
match count {
|
||||
0 => LogLevel::Info,
|
||||
1 => LogLevel::Debug,
|
||||
_ => LogLevel::Trace,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_log_level(value: Option<&str>) -> Option<LogLevel> {
|
||||
match value?.to_ascii_lowercase().as_str() {
|
||||
"error" => Some(LogLevel::Error),
|
||||
"warn" => Some(LogLevel::Warn),
|
||||
"info" => Some(LogLevel::Info),
|
||||
"debug" => Some(LogLevel::Debug),
|
||||
"trace" => Some(LogLevel::Trace),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_log_format(value: Option<&str>) -> Option<LogFormat> {
|
||||
match value?.to_ascii_lowercase().as_str() {
|
||||
"text" => Some(LogFormat::Text),
|
||||
"json" => Some(LogFormat::Json),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn env_log_level() -> Option<LogLevel> {
|
||||
std::env::var("NETTOOL_LOG_LEVEL")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(|value| parse_log_level(Some(value)))
|
||||
}
|
||||
|
||||
fn env_log_format() -> Option<LogFormat> {
|
||||
std::env::var("NETTOOL_LOG_FORMAT")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.and_then(|value| parse_log_format(Some(value)))
|
||||
}
|
||||
|
||||
fn env_log_file() -> Option<PathBuf> {
|
||||
std::env::var("NETTOOL_LOG_FILE").ok().map(PathBuf::from)
|
||||
}
|
||||
Reference in New Issue
Block a user