diff --git a/README.md b/README.md index e34a2e4..7c78d4f 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,12 @@ First time you specify a new `--profile/-P` name together with a URL (and option socktop --profile prod ws://prod-host:3000/ws # With TLS pinning: socktop --profile prod-tls --tls-ca /path/to/cert.pem wss://prod-host:8443/ws + +You can also set custom intervals (milliseconds): + +```bash +socktop --profile prod --metrics-interval-ms 750 --processes-interval-ms 3000 ws://prod-host:3000/ws +``` ``` If a profile already exists you will be prompted before overwriting: @@ -214,7 +220,7 @@ socktop --profile prod socktop -P prod-tls # short flag ``` -The stored URL (and TLS CA path, if any) will be used. TLS auto-upgrade still applies if a CA path is stored alongside a ws:// URL. +The stored URL (and TLS CA path, if any) plus any saved intervals will be used. TLS auto-upgrade still applies if a CA path is stored alongside a ws:// URL. ### Interactive selection (no args) @@ -238,7 +244,12 @@ An example `profiles.json` (pretty‑printed): { "profiles": { "prod": { "url": "ws://prod-host:3000/ws" }, - "prod-tls": { "url": "wss://prod-host:8443/ws", "tls_ca": "/home/user/certs/prod-cert.pem" } + "prod-tls": { + "url": "wss://prod-host:8443/ws", + "tls_ca": "/home/user/certs/prod-cert.pem", + "metrics_interval_ms": 500, + "processes_interval_ms": 2000 + } }, "version": 0 } @@ -248,6 +259,7 @@ Notes: - The `tls_ca` path is stored as given; if you move or rotate the certificate update the profile by re-running with `--profile NAME --save`. - Deleting a profile: edit the JSON file and remove the entry (TUI does not yet have an in-app delete command). - Profiles are client-side convenience only; they do not affect the agent. +- Intervals: `metrics_interval_ms` controls the fast metrics poll (default 500 ms). `processes_interval_ms` controls process list polling (default 2000 ms). Values below 100 ms (metrics) or 200 ms (processes) are clamped. --- diff --git a/socktop/src/app.rs b/socktop/src/app.rs index be65c6b..46707e6 100644 --- a/socktop/src/app.rs +++ b/socktop/src/app.rs @@ -63,6 +63,7 @@ pub struct App { last_disks_poll: Instant, procs_interval: Duration, disks_interval: Duration, + metrics_interval: Duration, // For reconnects ws_url: String, @@ -94,10 +95,17 @@ impl App { .unwrap_or_else(Instant::now), procs_interval: Duration::from_secs(2), disks_interval: Duration::from_secs(5), + metrics_interval: Duration::from_millis(500), ws_url: String::new(), } } + pub fn with_intervals(mut self, metrics_ms: Option, procs_ms: Option) -> Self { + if let Some(m) = metrics_ms { self.metrics_interval = Duration::from_millis(m.max(100)); } + if let Some(p) = procs_ms { self.procs_interval = Duration::from_millis(p.max(200)); } + self + } + pub async fn run( &mut self, url: &str, @@ -284,7 +292,7 @@ impl App { terminal.draw(|f| self.draw(f))?; // Tick rate - sleep(Duration::from_millis(500)).await; + sleep(self.metrics_interval).await; } Ok(()) @@ -471,6 +479,7 @@ impl Default for App { .unwrap_or_else(Instant::now), procs_interval: Duration::from_secs(2), disks_interval: Duration::from_secs(5), + metrics_interval: Duration::from_millis(500), ws_url: String::new(), } } diff --git a/socktop/src/main.rs b/socktop/src/main.rs index c9204b2..62b07ca 100644 --- a/socktop/src/main.rs +++ b/socktop/src/main.rs @@ -19,6 +19,8 @@ pub(crate) struct ParsedArgs { save: bool, demo: bool, dry_run: bool, // hidden test helper: skip connecting + metrics_interval_ms: Option, + processes_interval_ms: Option, } pub(crate) fn parse_args>(args: I) -> Result { @@ -30,10 +32,12 @@ pub(crate) fn parse_args>(args: I) -> Result = None; + let mut processes_interval_ms: Option = None; while let Some(arg) = it.next() { match arg.as_str() { "-h" | "--help" => { - return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [ws://HOST:PORT/ws]\n")); + return Err(format!("Usage: {prog} [--tls-ca CERT_PEM|-t CERT_PEM] [--profile NAME|-P NAME] [--save] [--demo] [--metrics-interval-ms N] [--processes-interval-ms N] [ws://HOST:PORT/ws]\n")); } "--tls-ca" | "-t" => { tls_ca = it.next(); @@ -51,6 +55,8 @@ pub(crate) fn parse_args>(args: I) -> Result { metrics_interval_ms = it.next().and_then(|v| v.parse().ok()); } + "--processes-interval-ms" => { processes_interval_ms = it.next().and_then(|v| v.parse().ok()); } _ if arg.starts_with("--tls-ca=") => { if let Some((_, v)) = arg.split_once('=') { if !v.is_empty() { @@ -65,6 +71,8 @@ pub(crate) fn parse_args>(args: I) -> Result Result<(), Box> { return run_demo_mode(parsed.tls_ca.as_deref()).await; } if let Some(entry) = profiles_mut.profiles.get(name) { - (entry.url.clone(), entry.tls_ca.clone()) + (entry.url.clone(), entry.tls_ca.clone(), entry.metrics_interval_ms, entry.processes_interval_ms) } else { return Ok(()); } @@ -191,22 +214,25 @@ async fn main() -> Result<(), Box> { } else { Some(ca.trim().to_string()) }; - profiles_mut.profiles.insert( + let (mi, pi) = gather_intervals(parsed.metrics_interval_ms, parsed.processes_interval_ms)?; + profiles_mut.profiles.insert( name.clone(), ProfileEntry { url: url.trim().to_string(), tls_ca: ca_opt.clone(), + metrics_interval_ms: mi, + processes_interval_ms: pi, }, ); let _ = save_profiles(&profiles_mut); - (url.trim().to_string(), ca_opt) + (url.trim().to_string(), ca_opt, mi, pi) } ResolveProfile::None => { eprintln!("No URL provided and no profiles to select."); return Ok(()); } }; - let mut app = App::new(); + let mut app = App::new().with_intervals(metrics_interval_ms, processes_interval_ms); if parsed.dry_run { return Ok(()); } @@ -231,6 +257,28 @@ fn prompt_string(prompt: &str) -> io::Result { Ok(line) } +fn gather_intervals(arg_metrics: Option, arg_procs: Option) -> Result<(Option, Option), Box> { + let default_metrics = 500u64; + let default_procs = 2000u64; + let metrics = match arg_metrics { + Some(v) => Some(v), + None => { + let inp = prompt_string(&format!("Metrics interval ms (default {default_metrics}, Enter for default): "))?; + let t = inp.trim(); + if t.is_empty() { Some(default_metrics) } else { Some(t.parse()?) } + } + }; + let procs = match arg_procs { + Some(v) => Some(v), + None => { + let inp = prompt_string(&format!("Processes interval ms (default {default_procs}, Enter for default): "))?; + let t = inp.trim(); + if t.is_empty() { Some(default_procs) } else { Some(t.parse()?) } + } + }; + Ok((metrics, procs)) +} + // Demo mode implementation async fn run_demo_mode(_tls_ca: Option<&str>) -> Result<(), Box> { let port = 3231; diff --git a/socktop/src/profiles.rs b/socktop/src/profiles.rs index 8dcc682..4086f97 100644 --- a/socktop/src/profiles.rs +++ b/socktop/src/profiles.rs @@ -9,6 +9,10 @@ pub struct ProfileEntry { pub url: String, #[serde(skip_serializing_if = "Option::is_none")] pub tls_ca: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metrics_interval_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub processes_interval_ms: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)]