use std::ffi::CStr;
use std::fmt::Write as FmtWrite;
const RUN_LVL: i16 = 1;
const BOOT_TIME: i16 = 2;
const NEW_TIME: i16 = 3;
const OLD_TIME: i16 = 4;
const INIT_PROCESS: i16 = 5;
const LOGIN_PROCESS: i16 = 6;
const USER_PROCESS: i16 = 7;
const DEAD_PROCESS: i16 = 8;
#[derive(Clone, Debug)]
pub struct UtmpxEntry {
pub ut_type: i16,
pub ut_pid: i32,
pub ut_line: String,
pub ut_id: String,
pub ut_user: String,
pub ut_host: String,
pub ut_tv_sec: i64,
}
fn guess_pty_name(uid: u32, start_us: u64) -> Option<String> {
let start_sec = (start_us / 1_000_000) as i64;
let start_nsec = ((start_us % 1_000_000) * 1000) as i64;
let dir = std::fs::read_dir("/dev/pts").ok()?;
let mut best_name: Option<String> = None;
let mut best_sec: i64 = 0;
let mut best_nsec: i64 = 0;
for entry in dir.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with('.') || name_str == "ptmx" {
continue;
}
let path = format!("/dev/pts/{}", name_str);
let c_path = match std::ffi::CString::new(path.as_str()) {
Ok(p) => p,
Err(_) => continue,
};
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
if rc != 0 {
continue;
}
if stat_buf.st_uid != uid {
continue;
}
let ct_sec = stat_buf.st_ctime;
let ct_nsec = stat_buf.st_ctime_nsec;
if ct_sec < start_sec || (ct_sec == start_sec && ct_nsec < start_nsec) {
continue;
}
if best_name.is_none() || ct_sec < best_sec || (ct_sec == best_sec && ct_nsec < best_nsec) {
best_name = Some(format!("pts/{}", name_str));
best_sec = ct_sec;
best_nsec = ct_nsec;
}
}
if let Some(ref _name) = best_name {
if best_sec > start_sec + 5 || (best_sec == start_sec + 5 && best_nsec > start_nsec) {
return None;
}
}
best_name
}
fn read_systemd_sessions(check_pids: bool) -> Vec<UtmpxEntry> {
let sessions_dir = std::path::Path::new("/run/systemd/sessions");
if !sessions_dir.exists() {
return Vec::new();
}
let mut entries = Vec::new();
let dir = match std::fs::read_dir(sessions_dir) {
Ok(d) => d,
Err(_) => return Vec::new(),
};
for entry in dir.flatten() {
let path = entry.path();
if path.extension().is_some() {
continue;
}
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(_) => continue,
};
let mut user = String::new();
let mut remote_host = String::new();
let mut service = String::new();
let mut realtime_us: u64 = 0;
let mut uid: u32 = 0;
let mut leader_pid: i32 = 0;
let mut active = false;
let mut session_type = String::new();
let mut session_class = String::new();
let mut session_id = String::new();
if let Some(fname) = path.file_name() {
session_id = fname.to_string_lossy().into_owned();
}
for line in content.lines() {
if let Some(val) = line.strip_prefix("USER=") {
user = val.to_string();
} else if let Some(val) = line.strip_prefix("REMOTE_HOST=") {
remote_host = val.to_string();
} else if let Some(val) = line.strip_prefix("SERVICE=") {
service = val.to_string();
} else if let Some(val) = line.strip_prefix("REALTIME=") {
realtime_us = val.parse().unwrap_or(0);
} else if let Some(val) = line.strip_prefix("UID=") {
uid = val.parse().unwrap_or(0);
} else if let Some(val) = line.strip_prefix("LEADER=") {
leader_pid = val.parse().unwrap_or(0);
} else if line == "ACTIVE=1" {
active = true;
} else if let Some(val) = line.strip_prefix("TYPE=") {
session_type = val.to_string();
} else if let Some(val) = line.strip_prefix("CLASS=") {
session_class = val.to_string();
}
}
if !active || user.is_empty() {
continue;
}
if check_pids && leader_pid > 0 {
let pid_alive = unsafe { libc::kill(leader_pid, 0) };
if pid_alive < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ESRCH) {
continue; }
}
}
let entry_type = if session_class.starts_with("manager") {
LOGIN_PROCESS
} else {
USER_PROCESS
};
if session_class != "user" && !session_class.starts_with("manager") {
continue;
}
let tty = if session_type == "tty" {
let pty = guess_pty_name(uid, realtime_us);
match (service.is_empty(), pty) {
(false, Some(pty_name)) => format!("{} {}", service, pty_name),
(false, None) => service.clone(),
(true, Some(pty_name)) => pty_name,
(true, None) => continue, }
} else if session_type == "web" {
if service.is_empty() {
continue;
}
service.clone()
} else {
continue; };
let tv_sec = (realtime_us / 1_000_000) as i64;
entries.push(UtmpxEntry {
ut_type: entry_type,
ut_pid: leader_pid,
ut_line: tty,
ut_id: session_id,
ut_user: user,
ut_host: remote_host,
ut_tv_sec: tv_sec,
});
}
entries.sort_by_key(|e| e.ut_tv_sec);
entries
}
pub fn read_utmpx() -> Vec<UtmpxEntry> {
let mut entries = Vec::new();
unsafe {
libc::setutxent();
loop {
let entry = libc::getutxent();
if entry.is_null() {
break;
}
let e = &*entry;
let user = cstr_from_buf(&e.ut_user);
let line = cstr_from_buf(&e.ut_line);
let host = cstr_from_buf(&e.ut_host);
let id = cstr_from_buf(&e.ut_id);
let tv_sec = e.ut_tv.tv_sec as i64;
entries.push(UtmpxEntry {
ut_type: e.ut_type as i16,
ut_pid: e.ut_pid,
ut_line: line,
ut_id: id,
ut_user: user,
ut_host: host,
ut_tv_sec: tv_sec,
});
}
libc::endutxent();
}
entries
}
pub fn read_utmpx_with_systemd_fallback_ex(check_pids: bool) -> Vec<UtmpxEntry> {
let mut entries = read_utmpx();
if check_pids {
entries.retain(|e| {
if e.ut_type == USER_PROCESS && e.ut_pid > 0 {
let rc = unsafe { libc::kill(e.ut_pid, 0) };
if rc < 0 {
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ESRCH) {
return false;
}
}
}
true
});
}
let has_user_entries = entries.iter().any(|e| e.ut_type == USER_PROCESS);
if !has_user_entries {
let systemd_entries = read_systemd_sessions(check_pids);
entries.extend(systemd_entries);
}
entries
}
pub fn read_utmpx_with_systemd_fallback() -> Vec<UtmpxEntry> {
read_utmpx_with_systemd_fallback_ex(true)
}
pub fn read_utmpx_with_systemd_fallback_no_pid_check() -> Vec<UtmpxEntry> {
read_utmpx_with_systemd_fallback_ex(false)
}
unsafe fn cstr_from_buf(buf: &[libc::c_char]) -> String {
let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
let bytes: Vec<u8> = buf[..len].iter().map(|&c| c as u8).collect();
String::from_utf8_lossy(&bytes).into_owned()
}
#[derive(Clone, Debug, Default)]
pub struct WhoConfig {
pub show_boot: bool,
pub show_dead: bool,
pub show_heading: bool,
pub show_login: bool,
pub only_current: bool, pub show_init_spawn: bool, pub show_count: bool, pub show_runlevel: bool, pub short_format: bool, pub show_clock_change: bool, pub show_mesg: bool, pub show_users: bool, pub show_all: bool, pub show_ips: bool, pub show_lookup: bool, pub am_i: bool, }
impl WhoConfig {
pub fn apply_all(&mut self) {
self.show_boot = true;
self.show_dead = true;
self.show_login = true;
self.show_init_spawn = true;
self.show_runlevel = true;
self.show_clock_change = true;
self.show_mesg = true;
self.show_users = true;
}
pub fn is_default_filter(&self) -> bool {
!self.show_boot
&& !self.show_dead
&& !self.show_login
&& !self.show_init_spawn
&& !self.show_runlevel
&& !self.show_clock_change
&& !self.show_users
}
}
pub fn format_time(tv_sec: i64) -> String {
if tv_sec == 0 {
return String::new();
}
let t = tv_sec as libc::time_t;
let tm = unsafe {
let mut tm: libc::tm = std::mem::zeroed();
libc::localtime_r(&t, &mut tm);
tm
};
format!(
"{:04}-{:02}-{:02} {:02}:{:02}",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
)
}
fn extract_device_path(line: &str) -> Option<String> {
if line.is_empty() {
return None;
}
let tty_part = if let Some(idx) = line.find("pts/") {
&line[idx..]
} else if let Some(idx) = line.find("tty") {
&line[idx..]
} else if line.starts_with('/') {
return Some(line.to_string());
} else {
line
};
if tty_part.starts_with('/') {
Some(tty_part.to_string())
} else {
Some(format!("/dev/{}", tty_part))
}
}
fn mesg_status(line: &str) -> char {
let dev_path = match extract_device_path(line) {
Some(p) => p,
None => return '?',
};
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
if rc != 0 {
return '?';
}
if stat_buf.st_mode & libc::S_IWGRP != 0 {
'+'
} else {
'-'
}
}
fn idle_str(line: &str) -> String {
let dev_path = match extract_device_path(line) {
Some(p) => p,
None => return "?".to_string(),
};
let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
if rc != 0 {
return "?".to_string();
}
let now = unsafe { libc::time(std::ptr::null_mut()) };
let atime = stat_buf.st_atime;
let idle_secs = now - atime;
if idle_secs < 60 {
".".to_string()
} else if idle_secs >= 86400 {
"old".to_string()
} else {
let hours = idle_secs / 3600;
let mins = (idle_secs % 3600) / 60;
format!("{:02}:{:02}", hours, mins)
}
}
pub fn current_tty() -> Option<String> {
unsafe {
let name = libc::ttyname(0); if name.is_null() {
None
} else {
let s = CStr::from_ptr(name).to_string_lossy().into_owned();
Some(s.strip_prefix("/dev/").unwrap_or(&s).to_string())
}
}
}
pub fn should_show(entry: &UtmpxEntry, config: &WhoConfig) -> bool {
if config.am_i || config.only_current {
if let Some(tty) = current_tty() {
return entry.ut_type == USER_PROCESS
&& (entry.ut_line == tty || entry.ut_line.ends_with(&format!(" {}", tty)));
}
return false;
}
if config.show_count {
return entry.ut_type == USER_PROCESS;
}
if config.is_default_filter() {
return entry.ut_type == USER_PROCESS;
}
match entry.ut_type {
BOOT_TIME => config.show_boot,
DEAD_PROCESS => config.show_dead,
LOGIN_PROCESS => config.show_login,
INIT_PROCESS => config.show_init_spawn,
RUN_LVL => config.show_runlevel,
NEW_TIME | OLD_TIME => config.show_clock_change,
USER_PROCESS => config.show_users || config.is_default_filter(),
_ => false,
}
}
pub fn format_entry(entry: &UtmpxEntry, config: &WhoConfig) -> String {
let mut out = String::new();
let (name, line) = match entry.ut_type {
BOOT_TIME => (String::new(), "system boot".to_string()),
RUN_LVL => {
let current = (entry.ut_pid & 0xFF) as u8 as char;
(String::new(), format!("run-level {}", current))
}
LOGIN_PROCESS => ("LOGIN".to_string(), entry.ut_line.clone()),
NEW_TIME => (String::new(), entry.ut_line.clone()),
OLD_TIME => (String::new(), entry.ut_line.clone()),
_ => (entry.ut_user.clone(), entry.ut_line.clone()),
};
let _ = write!(out, "{:<8}", name);
if config.show_mesg {
let status = if entry.ut_type == USER_PROCESS {
mesg_status(&entry.ut_line)
} else {
' '
};
let _ = write!(out, " {}", status);
}
let _ = write!(out, " {:<12}", line);
let time_str = format_time(entry.ut_tv_sec);
let _ = write!(out, " {}", time_str);
if config.show_users || config.show_all || config.show_login {
match entry.ut_type {
USER_PROCESS => {
let idle = idle_str(&entry.ut_line);
let _ = write!(out, " {:>5}", idle);
let _ = write!(out, " {:>11}", entry.ut_pid);
}
LOGIN_PROCESS => {
let _ = write!(out, " {:>5} {:>11}", " ", entry.ut_pid);
}
DEAD_PROCESS => {
let _ = write!(out, " {:>10}", entry.ut_pid);
}
_ => {}
}
}
if entry.ut_type == LOGIN_PROCESS {
let _ = write!(out, " id={}", entry.ut_id);
}
if !entry.ut_host.is_empty() && entry.ut_type != BOOT_TIME && entry.ut_type != RUN_LVL {
if config.show_ips {
let _ = write!(out, " ({})", entry.ut_host);
} else if config.show_lookup {
let resolved = lookup_host(&entry.ut_host);
let _ = write!(out, " ({})", resolved);
} else {
let _ = write!(out, " ({})", entry.ut_host);
}
}
out
}
fn lookup_host(host: &str) -> String {
let c_host = match std::ffi::CString::new(host) {
Ok(s) => s,
Err(_) => return host.to_string(),
};
unsafe {
let mut hints: libc::addrinfo = std::mem::zeroed();
hints.ai_flags = libc::AI_CANONNAME;
hints.ai_family = libc::AF_UNSPEC;
let mut result: *mut libc::addrinfo = std::ptr::null_mut();
let rc = libc::getaddrinfo(c_host.as_ptr(), std::ptr::null(), &hints, &mut result);
if rc != 0 || result.is_null() {
return host.to_string();
}
let canonical = if !(*result).ai_canonname.is_null() {
CStr::from_ptr((*result).ai_canonname)
.to_string_lossy()
.into_owned()
} else {
host.to_string()
};
libc::freeaddrinfo(result);
canonical
}
}
pub fn format_count(entries: &[UtmpxEntry]) -> String {
let users: Vec<&str> = entries
.iter()
.filter(|e| e.ut_type == USER_PROCESS)
.map(|e| e.ut_user.as_str())
.collect();
let mut out = String::new();
let _ = writeln!(out, "{}", users.join(" "));
let _ = write!(out, "# users={}", users.len());
out
}
pub fn format_heading(config: &WhoConfig) -> String {
let mut out = String::new();
if config.show_mesg {
let _ = write!(out, "{:<10}", "NAME");
} else {
let _ = write!(out, "{:<8}", "NAME");
}
let _ = write!(out, " {:<12}", "LINE");
let _ = write!(out, " {:<16}", "TIME");
if config.show_users || config.show_all {
let _ = write!(out, " {:<6}", "IDLE");
let _ = write!(out, " {:>10}", "PID");
}
let _ = write!(out, " {:<8}", "COMMENT");
if config.show_all || config.show_dead {
let _ = write!(out, " {}", "EXIT");
}
out.trim_end().to_string()
}
#[cfg(target_os = "linux")]
fn read_boot_time_from_proc() -> Option<i64> {
if let Some(ts) = read_boot_time_from_systemd() {
return Some(ts);
}
let data = std::fs::read_to_string("/proc/stat").ok()?;
for line in data.lines() {
if let Some(val) = line.strip_prefix("btime ") {
return val.trim().parse::<i64>().ok();
}
}
None
}
#[cfg(target_os = "linux")]
fn read_boot_time_from_systemd() -> Option<i64> {
use std::ffi::CString;
use std::mem::MaybeUninit;
let c_path = CString::new("/run/systemd/").ok()?;
unsafe {
let mut statx_buf: libc::statx = MaybeUninit::zeroed().assume_init();
let rc = libc::statx(
libc::AT_FDCWD,
c_path.as_ptr(),
0,
libc::STATX_BTIME,
&mut statx_buf,
);
if rc == 0 && (statx_buf.stx_mask & libc::STATX_BTIME) != 0 {
return Some(statx_buf.stx_btime.tv_sec);
}
}
None
}
#[cfg(not(target_os = "linux"))]
fn read_boot_time_from_proc() -> Option<i64> {
None
}
pub fn run_who(config: &WhoConfig) -> String {
let mut entries = read_utmpx_with_systemd_fallback();
if !entries.iter().any(|e| e.ut_type == BOOT_TIME) {
if let Some(btime) = read_boot_time_from_proc() {
entries.push(UtmpxEntry {
ut_type: BOOT_TIME,
ut_pid: 0,
ut_line: String::new(),
ut_id: String::new(),
ut_user: String::new(),
ut_host: String::new(),
ut_tv_sec: btime,
});
}
}
entries.sort_by_key(|e| e.ut_tv_sec);
let mut output = String::new();
if config.show_count {
return format_count(&entries);
}
if config.show_heading {
let _ = writeln!(output, "{}", format_heading(config));
}
for entry in &entries {
if should_show(entry, config) {
let _ = writeln!(output, "{}", format_entry(entry, config));
}
}
if output.ends_with('\n') {
output.pop();
}
output
}