WIP: Man pages generation with clap_mangen
This commit is contained in:
@@ -8,6 +8,9 @@ license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
# CLI parsing and man page generation
|
||||
clap = { version = "4.5", features = ["derive", "cargo", "wrap_help", "env"] }
|
||||
|
||||
# Tokio: Use minimal features instead of "full" to reduce binary size
|
||||
# Only include: rt-multi-thread (async runtime), net (WebSocket), sync (Mutex/RwLock), macros (#[tokio::test])
|
||||
# Excluded: io, fs, process, signal, time (not needed for this workload)
|
||||
@@ -37,6 +40,8 @@ default = []
|
||||
logging = ["tracing", "tracing-subscriber"]
|
||||
|
||||
[build-dependencies]
|
||||
clap = { version = "4.5", features = ["derive", "cargo", "env"] }
|
||||
clap_mangen = "0.2"
|
||||
prost-build = "0.13"
|
||||
tonic-build = { version = "0.12", default-features = false, optional = true }
|
||||
protoc-bin-vendored = "3"
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
use clap::CommandFactory;
|
||||
use clap_mangen::Man;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
include!("src/cli.rs");
|
||||
|
||||
fn main() {
|
||||
// Vendored protoc for reproducible builds
|
||||
let protoc = protoc_bin_vendored::protoc_bin_path().expect("protoc");
|
||||
|
||||
println!("cargo:rerun-if-changed=proto/processes.proto");
|
||||
println!("cargo:rerun-if-changed=src/cli.rs");
|
||||
|
||||
// Compile protobuf definitions for processes
|
||||
let mut cfg = prost_build::Config::new();
|
||||
@@ -11,4 +19,28 @@ fn main() {
|
||||
// Use local path (ensures file is inside published crate tarball)
|
||||
cfg.compile_protos(&["proto/processes.proto"], &["proto"]) // relative to CARGO_MANIFEST_DIR
|
||||
.expect("compile protos");
|
||||
|
||||
// Generate man page
|
||||
generate_man_page().expect("man page generation failed");
|
||||
}
|
||||
|
||||
fn generate_man_page() -> std::io::Result<()> {
|
||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap());
|
||||
let man_dir = out_dir.join("man");
|
||||
fs::create_dir_all(&man_dir)?;
|
||||
|
||||
// Generate man page for socktop_agent
|
||||
let cmd = Cli::command();
|
||||
let man = Man::new(cmd);
|
||||
let mut buffer = Vec::new();
|
||||
man.render(&mut buffer)?;
|
||||
|
||||
fs::write(man_dir.join("socktop_agent.1"), buffer)?;
|
||||
|
||||
println!(
|
||||
"cargo:warning=Man page generated at {:?}",
|
||||
man_dir.join("socktop_agent.1")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// CLI argument definitions for socktop_agent using clap derive macros.
|
||||
// This file is also included by build.rs for man page generation.
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "socktop_agent",
|
||||
version,
|
||||
author,
|
||||
about = "Lightweight WebSocket server for remote system monitoring",
|
||||
long_about = "socktop_agent is a lightweight Rust-based WebSocket server that provides system \
|
||||
metrics on demand. It serves metrics to socktop clients over WebSocket connections \
|
||||
at the /ws endpoint.\n\n\
|
||||
The agent is request-driven with near-zero CPU usage when idle. It collects metrics \
|
||||
only when clients request them over WebSocket, eliminating the need for background \
|
||||
sampling loops. This design results in minimal resource consumption, making it ideal \
|
||||
for resource-constrained systems like Raspberry Pi.\n\n\
|
||||
Metrics include: CPU (overall and per-core), memory, swap, disk usage, network \
|
||||
throughput, CPU temperatures, top processes, and optional GPU metrics."
|
||||
)]
|
||||
pub struct Cli {
|
||||
/// Port number to listen on
|
||||
///
|
||||
/// Default is 3000 for non-TLS mode and 8443 for TLS mode.
|
||||
/// Can also be set via SOCKTOP_PORT environment variable.
|
||||
#[arg(short = 'p', long = "port", value_name = "PORT", env = "SOCKTOP_PORT")]
|
||||
pub port: Option<u16>,
|
||||
|
||||
/// Enable TLS (secure WebSocket) mode
|
||||
///
|
||||
/// The agent will listen on wss:// instead of ws://.
|
||||
/// On first run with TLS enabled, the agent automatically generates
|
||||
/// a self-signed certificate and private key.
|
||||
/// Can also be enabled via SOCKTOP_ENABLE_SSL=1 environment variable.
|
||||
#[arg(long = "enableSSL", env = "SOCKTOP_ENABLE_SSL", value_parser = parse_bool_env)]
|
||||
pub enable_ssl: bool,
|
||||
}
|
||||
|
||||
/// Parse boolean from environment variable (accepts "1" or "true")
|
||||
fn parse_bool_env(s: &str) -> Result<bool, String> {
|
||||
match s {
|
||||
"1" | "true" | "TRUE" | "True" => Ok(true),
|
||||
"0" | "false" | "FALSE" | "False" => Ok(false),
|
||||
_ => Err(format!("Invalid boolean value: {}", s)),
|
||||
}
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
/// Parse CLI arguments from environment
|
||||
pub fn parse_args() -> Self {
|
||||
Cli::parse()
|
||||
}
|
||||
|
||||
/// Get the port to listen on, with appropriate defaults
|
||||
pub fn get_port(&self) -> u16 {
|
||||
if let Some(port) = self.port {
|
||||
port
|
||||
} else if self.enable_ssl {
|
||||
8443
|
||||
} else {
|
||||
3000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
let cli = Cli::try_parse_from(&["socktop_agent"]).unwrap();
|
||||
assert_eq!(cli.port, None);
|
||||
assert!(!cli.enable_ssl);
|
||||
assert_eq!(cli.get_port(), 3000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_port() {
|
||||
let cli = Cli::try_parse_from(&["socktop_agent", "--port", "8080"]).unwrap();
|
||||
assert_eq!(cli.port, Some(8080));
|
||||
assert_eq!(cli.get_port(), 8080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_short_port() {
|
||||
let cli = Cli::try_parse_from(&["socktop_agent", "-p", "9000"]).unwrap();
|
||||
assert_eq!(cli.port, Some(9000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enable_ssl() {
|
||||
let cli = Cli::try_parse_from(&["socktop_agent", "--enableSSL"]).unwrap();
|
||||
assert!(cli.enable_ssl);
|
||||
assert_eq!(cli.get_port(), 8443); // Default TLS port
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ssl_with_custom_port() {
|
||||
let cli = Cli::try_parse_from(&["socktop_agent", "--enableSSL", "-p", "9443"]).unwrap();
|
||||
assert!(cli.enable_ssl);
|
||||
assert_eq!(cli.get_port(), 9443);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! socktop agent entrypoint: sets up sysinfo handles and serves a WebSocket endpoint at /ws.
|
||||
|
||||
mod cli;
|
||||
mod gpu;
|
||||
mod metrics;
|
||||
mod proto;
|
||||
@@ -14,21 +15,9 @@ use std::str::FromStr;
|
||||
|
||||
mod tls;
|
||||
|
||||
use cli::Cli;
|
||||
use state::AppState;
|
||||
|
||||
fn arg_flag(name: &str) -> bool {
|
||||
std::env::args().any(|a| a == name)
|
||||
}
|
||||
fn arg_value(name: &str) -> Option<String> {
|
||||
let mut it = std::env::args();
|
||||
while let Some(a) = it.next() {
|
||||
if a == name {
|
||||
return it.next();
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
#[cfg(feature = "logging")]
|
||||
tracing_subscriber::fmt::init();
|
||||
@@ -70,11 +59,8 @@ fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
async fn async_main() -> anyhow::Result<()> {
|
||||
// Version flag (print and exit). Keep before heavy initialization.
|
||||
if arg_flag("--version") || arg_flag("-V") {
|
||||
println!("socktop_agent {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
// Parse CLI arguments
|
||||
let cli = Cli::parse_args();
|
||||
|
||||
let state = AppState::new();
|
||||
|
||||
@@ -90,15 +76,8 @@ async fn async_main() -> anyhow::Result<()> {
|
||||
.route("/healthz", get(healthz))
|
||||
.with_state(state.clone());
|
||||
|
||||
let enable_ssl =
|
||||
arg_flag("--enableSSL") || std::env::var("SOCKTOP_ENABLE_SSL").ok().as_deref() == Some("1");
|
||||
if enable_ssl {
|
||||
// Port can be overridden by --port or SOCKTOP_PORT; default to 8443 when SSL
|
||||
let port = arg_value("--port")
|
||||
.or_else(|| arg_value("-p"))
|
||||
.or_else(|| std::env::var("SOCKTOP_PORT").ok())
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
.unwrap_or(8443);
|
||||
if cli.enable_ssl {
|
||||
let port = cli.get_port();
|
||||
|
||||
let (cert_path, key_path) = tls::ensure_self_signed_cert()?;
|
||||
let cfg = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path).await?;
|
||||
@@ -112,11 +91,7 @@ async fn async_main() -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
// Non-TLS HTTP/WS path
|
||||
let port = arg_value("--port")
|
||||
.or_else(|| arg_value("-p"))
|
||||
.or_else(|| std::env::var("SOCKTOP_PORT").ok())
|
||||
.and_then(|s| s.parse::<u16>().ok())
|
||||
.unwrap_or(3000);
|
||||
let port = cli.get_port();
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
println!("socktop_agent: Listening on ws://{addr}/ws");
|
||||
axum_server::bind(addr)
|
||||
|
||||
Reference in New Issue
Block a user