From d67aaba58f593f84fcc57c370e465ec9209274e1 Mon Sep 17 00:00:00 2001 From: h7x4 Date: Mon, 20 Jul 2026 20:49:15 +0900 Subject: [PATCH] rwhod: let users specify user-idle/machine-down timeouts --- src/bin/ruptime.rs | 34 ++++++++++-- src/bin/rwho.rs | 18 +++++- src/server/varlink_api.rs | 114 ++++++++++++++++++++++++++++---------- 3 files changed, 129 insertions(+), 37 deletions(-) diff --git a/src/bin/ruptime.rs b/src/bin/ruptime.rs index 92985cc..bc4f511 100644 --- a/src/bin/ruptime.rs +++ b/src/bin/ruptime.rs @@ -13,14 +13,24 @@ use roowho2_lib::{ /// `ruptime` gives a status line like uptime for each machine on the local network; /// these are formed from packets broadcast by each host on the network once a minute. /// -/// Machines for which no status report has been received for 11 minutes are shown as being down. +/// Machines for which no status report has been received for a while (11 minutes by default, +/// see `--uptime-timeout`) are shown as being down. #[derive(Debug, Parser)] #[command(author = "Programvareverkstedet ", version)] pub struct Args { /// Users idle an hour or more are not counted unless the `-a` flag is given. - #[arg(long, short)] + #[arg(long, short, conflicts_with = "idle_timeout")] all: bool, + /// Only count users idle for less than this many minutes. + /// Cannot be used together with `--all`. + #[arg(long, value_name = "MINUTES", default_value_t = DEFAULT_IDLE_TIMEOUT_MINUTES)] + idle_timeout: u32, + + /// Consider a machine down if no status report has been received from it for this many minutes. + #[arg(long, value_name = "MINUTES", default_value_t = DEFAULT_UPTIME_TIMEOUT_MINUTES)] + uptime_timeout: u32, + /// Sort by load average. #[arg(long, short, conflicts_with = "time", conflicts_with = "users")] load: bool, @@ -51,6 +61,12 @@ pub struct Args { completions: Option, } +/// Users idle for longer than this are not counted by default. +const DEFAULT_IDLE_TIMEOUT_MINUTES: u32 = 60; + +/// Machines that haven't reported in for longer than this are shown as down by default. +const DEFAULT_UPTIME_TIMEOUT_MINUTES: u32 = 11; + #[tokio::main] async fn main() -> anyhow::Result<()> { let args = Args::parse(); @@ -65,12 +81,18 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } + let max_idle_seconds = if args.all { + None + } else { + Some(i64::from(args.idle_timeout) * 60) + }; + let mut conn = zlink::tokio::unix::connect("/run/roowho2/roowho2.varlink") .await .expect("Failed to connect to rwhod server"); let mut reply = conn - .ruptime(args.all) + .ruptime(max_idle_seconds) .await .context("Failed to send rwho request")? .map_err(|e| match e { @@ -96,7 +118,7 @@ async fn main() -> anyhow::Result<()> { // } } else { for entry in &reply { - let line = old_format_machine_entry(entry); + let line = old_format_machine_entry(entry, args.uptime_timeout); println!("{}", line); } } @@ -138,9 +160,9 @@ fn sort_entries( }); } -fn old_format_machine_entry(entry: &WhodStatusUpdate) -> String { +fn old_format_machine_entry(entry: &WhodStatusUpdate, uptime_timeout_minutes: u32) -> String { let time_since_last_ping = Utc::now() - entry.sendtime; - let is_up = time_since_last_ping <= Duration::minutes(11); + let is_up = time_since_last_ping <= Duration::minutes(i64::from(uptime_timeout_minutes)); let uptime = Utc::now() - entry.boot_time; let days = uptime.num_days(); diff --git a/src/bin/rwho.rs b/src/bin/rwho.rs index c6ba790..2beb423 100644 --- a/src/bin/rwho.rs +++ b/src/bin/rwho.rs @@ -19,9 +19,14 @@ use roowho2_lib::{ #[command(author = "Programvareverkstedet ", version)] pub struct Args { /// Print all machines responding even if no one is currently logged in - #[arg(long, short)] + #[arg(long, short, conflicts_with = "idle_timeout")] all: bool, + /// Only show users idle for less than this many minutes. + /// Cannot be used together with `--all`. + #[arg(long, value_name = "MINUTES", default_value_t = DEFAULT_IDLE_TIMEOUT_MINUTES)] + idle_timeout: u32, + /// Print the output with the old formatting #[arg(long, short)] old: bool, @@ -36,6 +41,9 @@ pub struct Args { completions: Option, } +/// Users idle for longer than this are omitted from the output by default. +const DEFAULT_IDLE_TIMEOUT_MINUTES: u32 = 60; + #[tokio::main] async fn main() -> anyhow::Result<()> { let args = Args::parse(); @@ -45,12 +53,18 @@ async fn main() -> anyhow::Result<()> { return Ok(()); } + let max_idle_seconds = if args.all { + None + } else { + Some(i64::from(args.idle_timeout) * 60) + }; + let mut conn = zlink::tokio::unix::connect("/run/roowho2/roowho2.varlink") .await .expect("Failed to connect to rwhod server"); let mut reply = conn - .rwho(args.all) + .rwho(max_idle_seconds) .await .context("Failed to send rwho request")? .map_err(|e| match e { diff --git a/src/server/varlink_api.rs b/src/server/varlink_api.rs index 973902e..6ed6396 100644 --- a/src/server/varlink_api.rs +++ b/src/server/varlink_api.rs @@ -24,14 +24,20 @@ use crate::{ #[zlink::proxy("no.ntnu.pvv.roowho2.rwhod")] pub trait VarlinkRwhodClientProxy { + /// `max_idle_seconds` is the maximum idle time (in seconds) for a user + /// to be included in the response. `None` means no limit (i.e. the old + /// `all: true`). The client is expected to always send an explicit + /// value, defaulting to whatever it considers reasonable if the user + /// didn't ask for a specific limit. async fn rwho( &mut self, - all: bool, + max_idle_seconds: Option, ) -> zlink::Result>; + /// See [`VarlinkRwhodClientProxy::rwho`] for the meaning of `max_idle_seconds`. async fn ruptime( &mut self, - all: bool, + max_idle_seconds: Option, ) -> zlink::Result>; } @@ -40,14 +46,16 @@ pub trait VarlinkRwhodClientProxy { pub enum VarlinkRwhodClientRequest { #[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Rwho")] Rwho { - /// Retrieve all users, even those that have been idle for a long time. - all: bool, + /// Maximum idle time (in seconds) for a user to be included. + /// `None` means no limit (i.e. return all users). + max_idle_seconds: Option, }, #[serde(rename = "no.ntnu.pvv.roowho2.rwhod.Ruptime")] Ruptime { - /// Count all users, even those that have been idle for a long time. - all: bool, + /// Maximum idle time (in seconds) for a user to be counted. + /// `None` means no limit (i.e. return all users). + max_idle_seconds: Option, }, } @@ -165,8 +173,11 @@ impl VarlinkRoowhoo2ClientServer { } impl VarlinkRoowhoo2ClientServer { - async fn handle_rwho_request(&self, all: bool) -> VarlinkRwhoResponse { - tracing::debug!(all, "Handling Rwho request"); + async fn handle_rwho_request( + &self, + max_idle_time: Option, + ) -> VarlinkRwhoResponse { + tracing::debug!(?max_idle_time, "Handling Rwho request"); let store = self.whod_status_store.read().await; let mut all_user_entries = Vec::with_capacity(store.len()); @@ -175,7 +186,7 @@ impl VarlinkRoowhoo2ClientServer { status_update .users .iter() - .filter(|user| all || user.idle_time < chrono::Duration::hours(1)) + .filter(|user| max_idle_time.is_none_or(|max| user.idle_time < max)) .cloned() .map(|user| (status_update.hostname.clone(), user)), ); @@ -184,18 +195,21 @@ impl VarlinkRoowhoo2ClientServer { all_user_entries } - async fn handle_ruptime_request(&self, all: bool) -> VarlinkRuptimeResponse { - tracing::debug!(all, "Handling Ruptime request"); + async fn handle_ruptime_request( + &self, + max_idle_time: Option, + ) -> VarlinkRuptimeResponse { + tracing::debug!(?max_idle_time, "Handling Ruptime request"); let store = self.whod_status_store.read().await; store .values() .cloned() .map(|mut status_update| { - if !all { + if let Some(max_idle_time) = max_idle_time { status_update .users - .retain(|user| user.idle_time < chrono::Duration::hours(1)); + .retain(|user| user.idle_time < max_idle_time); } status_update }) @@ -287,7 +301,7 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { Self::ReplyError<'service>, > { match call.method() { - VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Rwho { all }) => { + VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Rwho { max_idle_seconds }) => { if !self.rwhod_enabled { return ( MethodReply::Error(VarlinkReplyError::Rwhod( @@ -297,19 +311,42 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { ); } - let result = - match timeout(Duration::from_secs(2), self.handle_rwho_request(*all)).await { - Ok(response) => response, - Err(_) => { - tracing::error!("Rwho request timed out after 2 seconds"); - return ( - MethodReply::Error(VarlinkReplyError::Rwhod( - VarlinkRwhodClientError::TimedOut, - )), - Default::default(), - ); - } - }; + let max_idle_time = match max_idle_seconds + .map(|secs| chrono::TimeDelta::try_seconds(secs).ok_or(())) + .transpose() + .map_err(|_| { + tracing::error!( + "Received out-of-range max_idle_seconds: {:?}", + max_idle_seconds + ); + ( + MethodReply::Error(VarlinkReplyError::Rwhod( + VarlinkRwhodClientError::InvalidRequest, + )), + Default::default(), + ) + }) { + Ok(max_idle_time) => max_idle_time, + Err(err) => return err, + }; + + let result = match timeout( + Duration::from_secs(2), + self.handle_rwho_request(max_idle_time), + ) + .await + { + Ok(response) => response, + Err(_) => { + tracing::error!("Rwho request timed out after 2 seconds"); + return ( + MethodReply::Error(VarlinkReplyError::Rwhod( + VarlinkRwhodClientError::TimedOut, + )), + Default::default(), + ); + } + }; ( MethodReply::Single(Some(VarlinkReply::Rwhod( @@ -318,7 +355,7 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { Default::default(), ) } - VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Ruptime { all }) => { + VarlinkMethod::Rwhod(VarlinkRwhodClientRequest::Ruptime { max_idle_seconds }) => { if !self.rwhod_enabled { return ( MethodReply::Error(VarlinkReplyError::Rwhod( @@ -328,9 +365,28 @@ impl zlink::Service for VarlinkRoowhoo2ClientServer { ); } + let max_idle_time = match max_idle_seconds + .map(|secs| chrono::TimeDelta::try_seconds(secs).ok_or(())) + .transpose() + .map_err(|_| { + tracing::error!( + "Received out-of-range max_idle_seconds: {:?}", + max_idle_seconds + ); + ( + MethodReply::Error(VarlinkReplyError::Rwhod( + VarlinkRwhodClientError::InvalidRequest, + )), + Default::default(), + ) + }) { + Ok(max_idle_time) => max_idle_time, + Err(err) => return err, + }; + let result = match timeout( Duration::from_secs(2), - self.handle_ruptime_request(*all), + self.handle_ruptime_request(max_idle_time), ) .await {