summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2024-03-18 21:01:37 +0100
committerRaindropsSys <raindrops@equestria.dev>2024-03-18 21:01:37 +0100
commit68c7b35722065d6d2855b73e3f4a9321864a2c1c (patch)
treea1a4d7a32c70856d05d85e0a89863b22a9f59b6f
parent23496dd23edf3662ad705c123086f1cb8e647135 (diff)
downloadwhere-rs-68c7b35722065d6d2855b73e3f4a9321864a2c1c.tar.gz
where-rs-68c7b35722065d6d2855b73e3f4a9321864a2c1c.tar.bz2
where-rs-68c7b35722065d6d2855b73e3f4a9321864a2c1c.zip
I don't even know what I did
-rw-r--r--Cargo.lock130
-rw-r--r--where-rs/Cargo.toml7
-rw-r--r--where-rs/default_config.toml (renamed from config.toml)2
-rw-r--r--where-rs/src/args.rs9
-rw-r--r--where-rs/src/config.rs129
-rw-r--r--where-rs/src/main.rs182
-rw-r--r--where-rs/src/servers.rs66
-rw-r--r--where-rs/src/ui.rs75
-rw-r--r--whered/services/dev.equestria.whered.plist22
-rw-r--r--whered/services/whered12
-rw-r--r--whered/services/whered.service14
11 files changed, 475 insertions, 173 deletions
diff --git a/Cargo.lock b/Cargo.lock
index b943e8d..36aab30 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -27,6 +27,54 @@ dependencies = [
]
[[package]]
+name = "anstream"
+version = "0.6.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -82,6 +130,52 @@ dependencies = [
]
[[package]]
+name = "clap"
+version = "4.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.52",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
name = "const_fn"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -124,6 +218,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -413,6 +513,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
+[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -474,9 +580,9 @@ dependencies = [
[[package]]
name = "toml"
-version = "0.8.11"
+version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af06656561d28735e9c1cd63dfd57132c8155426aa6af24f36a00a351f88c48e"
+checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
dependencies = [
"serde",
"serde_spanned",
@@ -495,9 +601,9 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.22.7"
+version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18769cd1cec395d70860ceb4d932812a0b4d06b1a4bb336745a4d21b9496e992"
+checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd"
dependencies = [
"indexmap",
"serde",
@@ -513,6 +619,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -577,6 +689,7 @@ name = "where-rs"
version = "0.1.0"
dependencies = [
"chrono",
+ "clap",
"serde",
"toml",
"where-shared",
@@ -628,6 +741,15 @@ dependencies = [
]
[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
name = "windows-targets"
version = "0.52.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/where-rs/Cargo.toml b/where-rs/Cargo.toml
index 690d02b..9e1b2c5 100644
--- a/where-rs/Cargo.toml
+++ b/where-rs/Cargo.toml
@@ -2,8 +2,8 @@
name = "where-rs"
version = "0.1.0"
edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+description = "A small Rust client for the WHRD/UDP protocol, to access a list of logged in users on multiple systems at once."
+authors = ["Raindrops", "ryze132"]
[[bin]]
name = "where"
@@ -12,5 +12,6 @@ path = "src/main.rs"
[dependencies]
where-shared = { path = "../where-shared" }
chrono = "0.4.35"
-toml = "0.8.11"
+toml = "0.8.12"
serde = { version = "1.0.197", features = ["derive"] }
+clap = { version = "4.5.3", features = ["derive"] }
diff --git a/config.toml b/where-rs/default_config.toml
index 44fb53a..c0c65a9 100644
--- a/config.toml
+++ b/where-rs/default_config.toml
@@ -1,4 +1,4 @@
-# where-rs: .where.toml, v1.0 2024/03/13
+# where-rs: where.toml, v1.0 2024/03/18
# This is the where-rs configuration file. Documentation is provided in-line.
diff --git a/where-rs/src/args.rs b/where-rs/src/args.rs
new file mode 100644
index 0000000..fd08f36
--- /dev/null
+++ b/where-rs/src/args.rs
@@ -0,0 +1,9 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(name = "where", version, about)]
+pub struct Args {
+ /// Generate a config file when none is available
+ #[arg(short = 'c', long)]
+ pub generate_config: bool,
+}
diff --git a/where-rs/src/config.rs b/where-rs/src/config.rs
new file mode 100644
index 0000000..c45ba93
--- /dev/null
+++ b/where-rs/src/config.rs
@@ -0,0 +1,129 @@
+use std::{env, fs};
+use std::path::PathBuf;
+use serde::Deserialize;
+use crate::args::Args;
+
+const TIMEOUT: u64 = 2000;
+const MAX_SEND_RETRIES: usize = 3;
+const CONFIG_FILENAME: &str = "where.toml";
+
+#[derive(Deserialize, Debug, Default)]
+#[serde(default)]
+pub struct Config {
+ pub global: GlobalConfig,
+ pub server: Vec<Server>
+}
+
+#[derive(Deserialize, Debug, Clone)]
+#[serde(default)]
+pub struct GlobalConfig {
+ pub timeout: u64,
+ pub max_retries: usize,
+ pub include_inactive: bool,
+ pub port: u16,
+ pub source: String
+}
+
+#[derive(Deserialize, Debug)]
+pub struct Server {
+ pub endpoint: String,
+ pub label: Option<String>,
+ pub timeout: Option<u64>,
+ pub max_retries: Option<usize>,
+ pub failsafe: Option<bool>
+}
+
+impl Default for GlobalConfig {
+ fn default() -> Self {
+ Self {
+ timeout: TIMEOUT,
+ max_retries: MAX_SEND_RETRIES,
+ include_inactive: true,
+ port: 15,
+ source: "Local".to_string()
+ }
+ }
+}
+
+impl Config {
+ fn get_config_locations() -> Vec<PathBuf> {
+ vec![
+ {
+ let mut path = PathBuf::new();
+
+ if let Ok(home) = env::var("XDG_CONFIG_HOME") {
+ path.push(home);
+ } else if let Ok(home) = env::var("HOME") {
+ path.push(home);
+ path.push(".config");
+ } else {
+ path.push("/");
+ }
+
+ path.push(CONFIG_FILENAME);
+ path
+ },
+ {
+ let mut path = PathBuf::new();
+
+ path.push("/etc");
+ path.push(CONFIG_FILENAME);
+ path
+ }
+ ]
+ }
+
+ pub fn build(args: Args) -> Self {
+ let config: Option<Config> = Self::get_config_locations()
+ .iter()
+ .flat_map(|path| fs::read_to_string(path).ok())
+ .map(|str| toml::from_str(&str).unwrap_or_else(|e| {
+ eprintln!("where: Failed to parse configuration file: {e}");
+ std::process::exit(1);
+ }))
+ .next();
+
+ if args.generate_config {
+ let default_config = include_str!("../default_config.toml");
+
+ let mut saved_path: Option<&PathBuf> = None;
+ let mut save_locations = Self::get_config_locations();
+ save_locations.reverse();
+
+ let res: Option<()> = save_locations
+ .iter()
+ .flat_map(|path| {
+ let res = fs::write(path, default_config).ok();
+
+ if res.is_some() {
+ saved_path = Some(path);
+ }
+
+ res
+ })
+ .next();
+
+ if res.is_some() {
+ println!("where: Generated default configuration file at {}. Please edit it and run 'where' again.", saved_path.unwrap().to_str().unwrap());
+ std::process::exit(0);
+ } else {
+ let save_locations_strings: Vec<String> = save_locations
+ .into_iter()
+ .map(|path| path.to_str().unwrap().to_string())
+ .collect();
+ println!("where: Failed to generate the default configuration file, tried: {}", save_locations_strings.join(", "));
+ std::process::exit(1);
+ }
+ } else {
+ let locations_strings: Vec<String> = Self::get_config_locations()
+ .into_iter()
+ .map(|path| path.to_str().unwrap().to_string())
+ .collect();
+
+ config.unwrap_or_else(|| {
+ eprintln!("where: Valid configuration file found nowhere, tried: {}\nPass -c to generate a default config file.", locations_strings.join(", "));
+ std::process::exit(1);
+ })
+ }
+ }
+}
diff --git a/where-rs/src/main.rs b/where-rs/src/main.rs
index 4391c52..ce6a8bc 100644
--- a/where-rs/src/main.rs
+++ b/where-rs/src/main.rs
@@ -1,38 +1,12 @@
-use std::fs;
-use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
-use std::io::ErrorKind;
-use std::time::Duration;
-use where_shared::error::{WhereError, WhereResult};
-use where_shared::{Session, SessionCollection, MAX_PAYLOAD_LENGTH, WHERED_MAGIC};
-use chrono::prelude::*;
-use serde::Deserialize;
+mod config;
+mod servers;
+mod ui;
+mod args;
-pub const TIMEOUT: u64 = 2000;
-pub const MAX_SEND_RETRIES: usize = 3;
-
-#[derive(Deserialize, Debug)]
-struct Config {
- global: Option<GlobalConfig>,
- server: Vec<ServerConfig>
-}
-
-#[derive(Deserialize, Debug)]
-struct ServerConfig {
- endpoint: String,
- label: Option<String>,
- timeout: Option<u64>,
- max_retries: Option<usize>,
- failsafe: Option<bool>
-}
-
-#[derive(Deserialize, Debug, Clone, Default)]
-struct GlobalConfig {
- timeout: Option<u64>,
- max_retries: Option<usize>,
- include_inactive: Option<bool>,
- port: Option<u16>,
- source: Option<String>
-}
+use clap::Parser;
+use args::Args;
+use where_shared::error::WhereResult;
+use config::{Config, Server};
fn main() {
if let Err(e) = start_client() {
@@ -42,28 +16,17 @@ fn main() {
}
fn start_client() -> WhereResult<()> {
- // TODO: Make it load from an actual path: /etc/where.toml, or ~/.where.toml if it exists
- let config_path = "./config.toml";
-
- let config: Config = toml::from_str(&fs::read_to_string(config_path).unwrap_or_else(|e| {
- eprintln!("where: Failed to open configuration file: {e}");
- std::process::exit(1);
- })).unwrap_or_else(|e| {
- eprintln!("where: Failed to parse configuration file: {e}");
- std::process::exit(1);
- });
+ let args = Args::parse();
+ let config = Config::build(args);
+ let global_config = config.global;
- println!("{:?}", config);
- let global_config = config.global.unwrap_or_default();
-
- let servers: Vec<ServerConfig> = config.server;
+ let servers: Vec<Server> = config.server;
let mut sessions = vec![];
for server in servers {
- // I know using .clone() sucks!
- let res = match process_server(&server, global_config.clone()) {
- Ok(data) => {
- data
+ let res = match server.process(&global_config) {
+ Ok(collection) => {
+ collection
}
Err(e) => {
eprintln!("where: {e}");
@@ -72,124 +35,13 @@ fn start_client() -> WhereResult<()> {
std::process::exit(1);
}
- SessionCollection::get_empty()
+ continue
}
};
sessions.extend(res.into_vec());
}
- print_summary(sessions, global_config);
+ ui::print_summary(sessions, global_config);
Ok(())
}
-
-fn process_server(server: &ServerConfig, config: GlobalConfig) -> WhereResult<SessionCollection> {
- let label = server.label.clone().unwrap_or(server.endpoint.to_owned());
- let timeout = Duration::from_millis(server.timeout.unwrap_or(config.timeout.unwrap_or(TIMEOUT)));
- let retries = server.max_retries.unwrap_or(config.max_retries.unwrap_or(MAX_SEND_RETRIES));
-
- let address: SocketAddr = match server.endpoint.to_socket_addrs() {
- Ok(addr) => addr.as_slice()[0],
- Err(_) => {
- let mut endpoint = server.endpoint.clone();
- endpoint.push_str(&format!(":{}", config.port.unwrap_or(15)));
- endpoint.to_socket_addrs()?.as_slice()[0]
- }
- };
-
- let socket = UdpSocket::bind(if address.is_ipv4() {
- "0.0.0.0:0"
- } else {
- "[::]:0"
- })?;
- socket.set_read_timeout(Some(timeout))?;
-
- let mut buf = [0; MAX_PAYLOAD_LENGTH];
-
- for _ in 0..retries {
- socket.send_to(&WHERED_MAGIC, address)?;
-
- match socket.recv_from(&mut buf) {
- Ok(_) => {
- let collection = SessionCollection::from_udp_payload(buf, &label)?;
- return Ok(collection);
- },
- Err(e) if e.kind() == ErrorKind::TimedOut || e.kind() == ErrorKind::WouldBlock => continue,
- Err(e) => return Err(WhereError::from(e)),
- }
- }
-
- Err(WhereError::TimedOut(server.endpoint.to_string(), address.to_string(), retries, timeout))
-}
-
-fn print_summary(mut sessions: Vec<Session>, config: GlobalConfig) {
- fn max_key_with_min<T, F>(sessions: &[Session], get_key: F, floor: T) -> T
- where
- T: Ord + Default,
- F: Fn(&Session) -> T
- {
- sessions.iter()
- .max_by_key(|s| get_key(s))
- .map(get_key)
- .unwrap_or_default()
- .max(floor)
- }
-
-
- sessions.sort_unstable_by_key(|s| s.login_time);
- sessions.sort_by_key(|s| !s.active); // We want active first
-
- const ACTIVE_PADDING: usize = 2;
- let host_padding = max_key_with_min(&sessions, |s| s.host.as_deref().map_or(0, |str| str.len()), 5);
- let remote_padding = max_key_with_min(&sessions, |s| s.remote.as_deref().map_or(0, |str| str.len()), 7);
- let username_padding = max_key_with_min(&sessions, |s| s.user.len(), 5);
- let tty_padding = max_key_with_min(&sessions, |s| s.tty.len(), 4);
- let pid_padding = max_key_with_min(&sessions, |s| s.pid.abs().checked_ilog10().unwrap_or_default() + 1 + (s.pid < 0) as u32, 4);
-
- println!("{:pad_0$} {:<pad_1$} {:<pad_2$} {:<pad_3$} {:<pad_4$} {:<pad_5$} Since",
- "Act",
- "Host",
- "Source",
- "User",
- "TTY",
- "PID",
- pad_0 = ACTIVE_PADDING,
- pad_1 = host_padding,
- pad_2 = remote_padding,
- pad_3 = username_padding,
- pad_4 = tty_padding,
- pad_5 = pid_padding as usize);
-
- for session in sessions {
- if !config.include_inactive.unwrap_or(true) && !session.active {
- continue;
- }
-
- let active = if session.active {
- '*'
- } else {
- ' '
- };
-
- let host = session.host.unwrap_or_else(|| ' '.to_string());
- let remote = session.remote.unwrap_or_else(|| config.source.clone().unwrap_or("Local".to_string()));
-
- let datetime = DateTime::from_timestamp(session.login_time, 0).unwrap();
- let time = datetime.format("%Y-%m-%d %H:%M:%S");
-
- println!(" {:<pad_0$} {:<pad_1$} {:<pad_2$} {:<pad_3$} {:<pad_4$} {:<pad_5$} {}",
- active,
- host,
- remote,
- session.tty,
- session.user,
- session.pid,
- time,
- pad_0 = ACTIVE_PADDING,
- pad_1 = host_padding,
- pad_2 = remote_padding,
- pad_3 = username_padding,
- pad_4 = tty_padding,
- pad_5 = pid_padding as usize);
- }
-}
diff --git a/where-rs/src/servers.rs b/where-rs/src/servers.rs
new file mode 100644
index 0000000..aab4322
--- /dev/null
+++ b/where-rs/src/servers.rs
@@ -0,0 +1,66 @@
+use std::io::ErrorKind;
+use std::net::{SocketAddr, UdpSocket};
+use std::time::Duration;
+use where_shared::error::{WhereError, WhereResult};
+use where_shared::{MAX_PAYLOAD_LENGTH, SessionCollection, WHERED_MAGIC};
+use crate::config::{GlobalConfig, Server};
+
+impl Server {
+ fn get_address(&self, config: &GlobalConfig) -> WhereResult<SocketAddr> {
+ let res: SocketAddr = match self.endpoint.parse() {
+ Ok(addr) => addr,
+ Err(_) => {
+ let mut endpoint = self.endpoint.clone();
+ let port = config.port.to_string();
+
+ endpoint.push(':');
+ endpoint.push_str(&port);
+ endpoint.parse()?
+ }
+ };
+
+ Ok(res)
+ }
+
+ fn create_socket(&self, address: &SocketAddr, timeout: Duration) -> WhereResult<UdpSocket> {
+ let socket = UdpSocket::bind(if address.is_ipv4() {
+ "0.0.0.0:0"
+ } else {
+ "[::]:0"
+ })?;
+ socket.set_read_timeout(Some(timeout))?;
+
+ Ok(socket)
+ }
+
+ fn attempt_fetch(socket: &UdpSocket, address: &SocketAddr, mut buf: [u8; MAX_PAYLOAD_LENGTH], label: &str) -> WhereResult<Option<SessionCollection>> {
+ socket.send_to(&WHERED_MAGIC, address)?;
+
+ match socket.recv_from(&mut buf) {
+ Ok(_) => {
+ let collection = SessionCollection::from_udp_payload(buf, &label)?;
+ Ok(Some(collection))
+ },
+ Err(e) if e.kind() == ErrorKind::TimedOut || e.kind() == ErrorKind::WouldBlock => Ok(None),
+ Err(e) => Err(WhereError::from(e)),
+ }
+ }
+
+ pub fn process(&self, config: &GlobalConfig) -> WhereResult<SessionCollection> {
+ let label = self.label.clone().unwrap_or(self.endpoint.to_owned());
+ let retries = self.max_retries.unwrap_or(config.max_retries);
+ let address = self.get_address(config)?;
+ let timeout = Duration::from_millis(self.timeout.unwrap_or(config.timeout));
+ let socket = self.create_socket(&address, timeout)?;
+ let buf = [0; MAX_PAYLOAD_LENGTH];
+
+ for _ in 0..retries {
+ match Self::attempt_fetch(&socket, &address, buf, &label)? {
+ Some(c) => return Ok(c),
+ None => continue
+ };
+ }
+
+ Err(WhereError::TimedOut(self.endpoint.to_string(), address.to_string(), retries, timeout))
+ }
+}
diff --git a/where-rs/src/ui.rs b/where-rs/src/ui.rs
new file mode 100644
index 0000000..d2c1826
--- /dev/null
+++ b/where-rs/src/ui.rs
@@ -0,0 +1,75 @@
+use chrono::DateTime;
+use where_shared::Session;
+use crate::config::GlobalConfig;
+
+pub fn print_summary(mut sessions: Vec<Session>, config: GlobalConfig) {
+ fn max_key_with_min<T, F>(sessions: &[Session], get_key: F, floor: T) -> T
+ where
+ T: Ord + Default,
+ F: Fn(&Session) -> T
+ {
+ sessions.iter()
+ .max_by_key(|s| get_key(s))
+ .map(get_key)
+ .unwrap_or_default()
+ .max(floor)
+ }
+
+
+ sessions.sort_unstable_by_key(|s| s.login_time);
+ sessions.sort_by_key(|s| !s.active); // We want active first
+
+ const ACTIVE_PADDING: usize = 2;
+ let host_padding = max_key_with_min(&sessions, |s| s.host.as_deref().map_or(0, |str| str.len()), 5);
+ let remote_padding = max_key_with_min(&sessions, |s| s.remote.as_deref().map_or(0, |str| str.len()), 7);
+ let username_padding = max_key_with_min(&sessions, |s| s.user.len(), 5);
+ let tty_padding = max_key_with_min(&sessions, |s| s.tty.len(), 4);
+ let pid_padding = max_key_with_min(&sessions, |s| s.pid.abs().checked_ilog10().unwrap_or_default() + 1 + (s.pid < 0) as u32, 4);
+
+ println!("{:pad_0$} {:<pad_1$} {:<pad_2$} {:<pad_3$} {:<pad_4$} {:<pad_5$} Since",
+ "Act",
+ "Host",
+ "Source",
+ "User",
+ "TTY",
+ "PID",
+ pad_0 = ACTIVE_PADDING,
+ pad_1 = host_padding,
+ pad_2 = remote_padding,
+ pad_3 = username_padding,
+ pad_4 = tty_padding,
+ pad_5 = pid_padding as usize);
+
+ for session in sessions {
+ if !config.include_inactive && !session.active {
+ continue;
+ }
+
+ let active = if session.active {
+ '*'
+ } else {
+ ' '
+ };
+
+ let host = session.host.unwrap_or_else(|| ' '.to_string());
+ let remote = session.remote.unwrap_or_else(|| config.source.clone());
+
+ let datetime = DateTime::from_timestamp(session.login_time, 0).unwrap();
+ let time = datetime.format("%Y-%m-%d %H:%M:%S");
+
+ println!(" {:<pad_0$} {:<pad_1$} {:<pad_2$} {:<pad_3$} {:<pad_4$} {:<pad_5$} {}",
+ active,
+ host,
+ remote,
+ session.tty,
+ session.user,
+ session.pid,
+ time,
+ pad_0 = ACTIVE_PADDING,
+ pad_1 = host_padding,
+ pad_2 = remote_padding,
+ pad_3 = username_padding,
+ pad_4 = tty_padding,
+ pad_5 = pid_padding as usize);
+ }
+}
diff --git a/whered/services/dev.equestria.whered.plist b/whered/services/dev.equestria.whered.plist
new file mode 100644
index 0000000..72fdb2c
--- /dev/null
+++ b/whered/services/dev.equestria.whered.plist
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>dev.equestria.whered</string>
+
+ <key>ProgramArguments</key>
+ <array>
+ <string>/usr/local/bin/whered</string>
+ </array>
+
+ <key>KeepAlive</key>
+ <true/>
+
+ <key>RunAtLoad</key>
+ <true/>
+
+ <key>StandardOutPath</key>
+ <string>/Library/Logs/whered.log</string>
+</dict>
+</plist>
diff --git a/whered/services/whered b/whered/services/whered
new file mode 100644
index 0000000..a38a950
--- /dev/null
+++ b/whered/services/whered
@@ -0,0 +1,12 @@
+#!/sbin/openrc-run
+
+name="whered"
+description="WHRD/UDP Protocol Server"
+command="/usr/bin/whered"
+command_args="${service_args}"
+command_user="whered:whered"
+
+depend() {
+ need net
+ use logger
+}
diff --git a/whered/services/whered.service b/whered/services/whered.service
new file mode 100644
index 0000000..cee24eb
--- /dev/null
+++ b/whered/services/whered.service
@@ -0,0 +1,14 @@
+[Unit]
+Description=WHRD/UDP Protocol Server
+After=network.target
+StartLimitIntervalSec=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+User=whered
+ExecStart=/usr/bin/whered
+
+[Install]
+WantedBy=multi-user.target