WIP: Man pages generation with clap_mangen
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled

This commit is contained in:
2025-11-20 23:35:29 -08:00
parent 518ae8c2bf
commit f82a5903b8
13 changed files with 1496 additions and 154 deletions
+5
View File
@@ -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"
+32
View File
@@ -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(())
}
+105
View File
@@ -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);
}
}
+7 -32
View File
@@ -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)