mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-10 00:26:59 +00:00
Add a Unix domain socket interface for querying node state at runtime. A spawned tokio task accepts connections and communicates with the main event loop via mpsc/oneshot channels, keeping all Node access single-threaded. Includes: - src/control/ module with socket lifecycle, JSON protocol, and 11 query handlers (status, peers, links, tree, sessions, bloom, mmp, cache, connections, transports, routing) - Separate fipsctl binary for CLI queries (fipsctl show <command>) - ControlConfig in node configuration (enabled, socket_path) - Integration into the main select! event loop
166 lines
4.5 KiB
Rust
166 lines
4.5 KiB
Rust
//! fipsctl — FIPS control client
|
|
//!
|
|
//! Connects to the FIPS daemon's Unix domain control socket, sends a
|
|
//! query command, and pretty-prints the JSON response.
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use std::io::{BufRead, BufReader, Write};
|
|
use std::os::unix::net::UnixStream;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
/// FIPS control client
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "fipsctl", version, about = "Query a running FIPS daemon")]
|
|
struct Cli {
|
|
/// Control socket path override
|
|
#[arg(short = 's', long)]
|
|
socket: Option<PathBuf>,
|
|
|
|
#[command(subcommand)]
|
|
command: Commands,
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum Commands {
|
|
/// Show node information
|
|
Show {
|
|
#[command(subcommand)]
|
|
what: ShowCommands,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand, Debug)]
|
|
enum ShowCommands {
|
|
/// Node status overview
|
|
Status,
|
|
/// Authenticated peers
|
|
Peers,
|
|
/// Active links
|
|
Links,
|
|
/// Spanning tree state
|
|
Tree,
|
|
/// End-to-end sessions
|
|
Sessions,
|
|
/// Bloom filter state
|
|
Bloom,
|
|
/// MMP metrics summary
|
|
Mmp,
|
|
/// Coordinate cache stats
|
|
Cache,
|
|
/// Pending handshake connections
|
|
Connections,
|
|
/// Transport instances
|
|
Transports,
|
|
/// Routing table summary
|
|
Routing,
|
|
}
|
|
|
|
impl ShowCommands {
|
|
fn command_name(&self) -> &'static str {
|
|
match self {
|
|
ShowCommands::Status => "show_status",
|
|
ShowCommands::Peers => "show_peers",
|
|
ShowCommands::Links => "show_links",
|
|
ShowCommands::Tree => "show_tree",
|
|
ShowCommands::Sessions => "show_sessions",
|
|
ShowCommands::Bloom => "show_bloom",
|
|
ShowCommands::Mmp => "show_mmp",
|
|
ShowCommands::Cache => "show_cache",
|
|
ShowCommands::Connections => "show_connections",
|
|
ShowCommands::Transports => "show_transports",
|
|
ShowCommands::Routing => "show_routing",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Determine the default socket path.
|
|
fn default_socket_path() -> PathBuf {
|
|
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
|
PathBuf::from(format!("{}/fips/control.sock", runtime_dir))
|
|
} else {
|
|
PathBuf::from("/tmp/fips-control.sock")
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let cli = Cli::parse();
|
|
|
|
let socket_path = cli.socket.unwrap_or_else(default_socket_path);
|
|
let command_name = match &cli.command {
|
|
Commands::Show { what } => what.command_name(),
|
|
};
|
|
|
|
// Connect to the control socket
|
|
let mut stream = match UnixStream::connect(&socket_path) {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
eprintln!(
|
|
"error: cannot connect to {}: {}",
|
|
socket_path.display(),
|
|
e
|
|
);
|
|
eprintln!("Is the FIPS daemon running?");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Set timeouts
|
|
let timeout = Duration::from_secs(2);
|
|
let _ = stream.set_read_timeout(Some(timeout));
|
|
let _ = stream.set_write_timeout(Some(timeout));
|
|
|
|
// Send request
|
|
let request = format!("{{\"command\":\"{}\"}}\n", command_name);
|
|
if let Err(e) = stream.write_all(request.as_bytes()) {
|
|
eprintln!("error: failed to send request: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Shutdown write half to signal end of request
|
|
let _ = stream.shutdown(std::net::Shutdown::Write);
|
|
|
|
// Read response
|
|
let reader = BufReader::new(&stream);
|
|
let response_line = match reader.lines().next() {
|
|
Some(Ok(line)) => line,
|
|
Some(Err(e)) => {
|
|
eprintln!("error: failed to read response: {}", e);
|
|
std::process::exit(1);
|
|
}
|
|
None => {
|
|
eprintln!("error: no response from daemon");
|
|
std::process::exit(1);
|
|
}
|
|
};
|
|
|
|
// Parse and pretty-print
|
|
let Ok(value) = serde_json::from_str::<serde_json::Value>(&response_line) else {
|
|
// Not JSON, print raw
|
|
println!("{}", response_line);
|
|
return;
|
|
};
|
|
|
|
let status = value
|
|
.get("status")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown");
|
|
|
|
if status == "error" {
|
|
let msg = value
|
|
.get("message")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("unknown error");
|
|
eprintln!("error: {}", msg);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
// Pretty-print the data field (or whole response if no data field)
|
|
let output = if let Some(data) = value.get("data") {
|
|
serde_json::to_string_pretty(data).unwrap_or(response_line)
|
|
} else {
|
|
serde_json::to_string_pretty(&value).unwrap_or(response_line)
|
|
};
|
|
println!("{}", output);
|
|
}
|