1use std::ffi::CStr;
6use std::fmt::Write as FmtWrite;
7
8const RUN_LVL: i16 = 1;
10const BOOT_TIME: i16 = 2;
11const NEW_TIME: i16 = 3;
12const OLD_TIME: i16 = 4;
13const INIT_PROCESS: i16 = 5;
14const LOGIN_PROCESS: i16 = 6;
15const USER_PROCESS: i16 = 7;
16const DEAD_PROCESS: i16 = 8;
17
18#[derive(Clone, Debug)]
20pub struct UtmpxEntry {
21 pub ut_type: i16,
22 pub ut_pid: i32,
23 pub ut_line: String,
24 pub ut_id: String,
25 pub ut_user: String,
26 pub ut_host: String,
27 pub ut_tv_sec: i64,
28}
29
30fn guess_pty_name(uid: u32, start_us: u64) -> Option<String> {
36 let start_sec = (start_us / 1_000_000) as i64;
37 let start_nsec = ((start_us % 1_000_000) * 1000) as i64;
38
39 let dir = std::fs::read_dir("/dev/pts").ok()?;
40
41 let mut best_name: Option<String> = None;
42 let mut best_sec: i64 = 0;
43 let mut best_nsec: i64 = 0;
44
45 for entry in dir.flatten() {
46 let name = entry.file_name();
47 let name_str = name.to_string_lossy();
48 if name_str.starts_with('.') || name_str == "ptmx" {
50 continue;
51 }
52
53 let path = format!("/dev/pts/{}", name_str);
54 let c_path = match std::ffi::CString::new(path.as_str()) {
55 Ok(p) => p,
56 Err(_) => continue,
57 };
58
59 let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
60 let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
61 if rc != 0 {
62 continue;
63 }
64
65 if stat_buf.st_uid != uid {
67 continue;
68 }
69
70 let ct_sec = stat_buf.st_ctime;
71 let ct_nsec = stat_buf.st_ctime_nsec;
72
73 if ct_sec < start_sec || (ct_sec == start_sec && ct_nsec < start_nsec) {
75 continue;
76 }
77
78 if best_name.is_none() || ct_sec < best_sec || (ct_sec == best_sec && ct_nsec < best_nsec) {
80 best_name = Some(format!("pts/{}", name_str));
81 best_sec = ct_sec;
82 best_nsec = ct_nsec;
83 }
84 }
85
86 if let Some(ref _name) = best_name {
88 if best_sec > start_sec + 5 || (best_sec == start_sec + 5 && best_nsec > start_nsec) {
89 return None;
90 }
91 }
92
93 best_name
94}
95
96fn read_systemd_sessions(check_pids: bool) -> Vec<UtmpxEntry> {
104 let sessions_dir = std::path::Path::new("/run/systemd/sessions");
105 if !sessions_dir.exists() {
106 return Vec::new();
107 }
108
109 let mut entries = Vec::new();
110
111 let dir = match std::fs::read_dir(sessions_dir) {
112 Ok(d) => d,
113 Err(_) => return Vec::new(),
114 };
115
116 for entry in dir.flatten() {
117 let path = entry.path();
118 if path.extension().is_some() {
120 continue;
121 }
122 let content = match std::fs::read_to_string(&path) {
123 Ok(c) => c,
124 Err(_) => continue,
125 };
126
127 let mut user = String::new();
128 let mut remote_host = String::new();
129 let mut service = String::new();
130 let mut realtime_us: u64 = 0;
131 let mut uid: u32 = 0;
132 let mut leader_pid: i32 = 0;
133 let mut active = false;
134 let mut session_type = String::new();
135 let mut session_class = String::new();
136 let mut session_id = String::new();
137
138 if let Some(fname) = path.file_name() {
140 session_id = fname.to_string_lossy().into_owned();
141 }
142
143 for line in content.lines() {
144 if let Some(val) = line.strip_prefix("USER=") {
145 user = val.to_string();
146 } else if let Some(val) = line.strip_prefix("REMOTE_HOST=") {
147 remote_host = val.to_string();
148 } else if let Some(val) = line.strip_prefix("SERVICE=") {
149 service = val.to_string();
150 } else if let Some(val) = line.strip_prefix("REALTIME=") {
151 realtime_us = val.parse().unwrap_or(0);
152 } else if let Some(val) = line.strip_prefix("UID=") {
153 uid = val.parse().unwrap_or(0);
154 } else if let Some(val) = line.strip_prefix("LEADER=") {
155 leader_pid = val.parse().unwrap_or(0);
156 } else if line == "ACTIVE=1" {
157 active = true;
158 } else if let Some(val) = line.strip_prefix("TYPE=") {
159 session_type = val.to_string();
160 } else if let Some(val) = line.strip_prefix("CLASS=") {
161 session_class = val.to_string();
162 }
163 }
164
165 if !active || user.is_empty() {
167 continue;
168 }
169
170 if check_pids && leader_pid > 0 {
173 let pid_alive = unsafe { libc::kill(leader_pid, 0) };
174 if pid_alive < 0 {
175 let err = std::io::Error::last_os_error();
176 if err.raw_os_error() == Some(libc::ESRCH) {
177 continue; }
179 }
180 }
181
182 let entry_type = if session_class.starts_with("manager") {
184 LOGIN_PROCESS
185 } else {
186 USER_PROCESS
187 };
188
189 if session_class != "user" && !session_class.starts_with("manager") {
191 continue;
192 }
193
194 let tty = if session_type == "tty" {
196 let pty = guess_pty_name(uid, realtime_us);
198 match (service.is_empty(), pty) {
199 (false, Some(pty_name)) => format!("{} {}", service, pty_name),
200 (false, None) => service.clone(),
201 (true, Some(pty_name)) => pty_name,
202 (true, None) => continue, }
204 } else if session_type == "web" {
205 if service.is_empty() {
206 continue;
207 }
208 service.clone()
209 } else {
210 continue; };
212
213 let tv_sec = (realtime_us / 1_000_000) as i64;
214
215 entries.push(UtmpxEntry {
216 ut_type: entry_type,
217 ut_pid: leader_pid,
218 ut_line: tty,
219 ut_id: session_id,
220 ut_user: user,
221 ut_host: remote_host,
222 ut_tv_sec: tv_sec,
223 });
224 }
225
226 entries.sort_by_key(|e| e.ut_tv_sec);
228 entries
229}
230
231pub fn read_utmpx() -> Vec<UtmpxEntry> {
237 let mut entries = Vec::new();
238
239 unsafe {
240 libc::setutxent();
241 loop {
242 let entry = libc::getutxent();
243 if entry.is_null() {
244 break;
245 }
246 let e = &*entry;
247
248 let user = cstr_from_buf(&e.ut_user);
249 let line = cstr_from_buf(&e.ut_line);
250 let host = cstr_from_buf(&e.ut_host);
251 let id = cstr_from_buf(&e.ut_id);
252
253 let tv_sec = e.ut_tv.tv_sec as i64;
254
255 entries.push(UtmpxEntry {
256 ut_type: e.ut_type as i16,
257 ut_pid: e.ut_pid,
258 ut_line: line,
259 ut_id: id,
260 ut_user: user,
261 ut_host: host,
262 ut_tv_sec: tv_sec,
263 });
264 }
265 libc::endutxent();
266 }
267
268 entries
269}
270
271pub fn read_utmpx_with_systemd_fallback_ex(check_pids: bool) -> Vec<UtmpxEntry> {
279 let mut entries = read_utmpx();
280
281 if check_pids {
283 entries.retain(|e| {
284 if e.ut_type == USER_PROCESS && e.ut_pid > 0 {
285 let rc = unsafe { libc::kill(e.ut_pid, 0) };
286 if rc < 0 {
287 let err = std::io::Error::last_os_error();
288 if err.raw_os_error() == Some(libc::ESRCH) {
289 return false;
290 }
291 }
292 }
293 true
294 });
295 }
296
297 let has_user_entries = entries.iter().any(|e| e.ut_type == USER_PROCESS);
298 if !has_user_entries {
299 let systemd_entries = read_systemd_sessions(check_pids);
300 entries.extend(systemd_entries);
301 }
302 entries
303}
304
305pub fn read_utmpx_with_systemd_fallback() -> Vec<UtmpxEntry> {
307 read_utmpx_with_systemd_fallback_ex(true)
308}
309
310pub fn read_utmpx_with_systemd_fallback_no_pid_check() -> Vec<UtmpxEntry> {
312 read_utmpx_with_systemd_fallback_ex(false)
313}
314
315unsafe fn cstr_from_buf(buf: &[libc::c_char]) -> String {
317 let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
319 let bytes: Vec<u8> = buf[..len].iter().map(|&c| c as u8).collect();
320 String::from_utf8_lossy(&bytes).into_owned()
321}
322
323#[derive(Clone, Debug, Default)]
325pub struct WhoConfig {
326 pub show_boot: bool,
327 pub show_dead: bool,
328 pub show_heading: bool,
329 pub show_login: bool,
330 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, }
343
344impl WhoConfig {
345 pub fn apply_all(&mut self) {
347 self.show_boot = true;
348 self.show_dead = true;
349 self.show_login = true;
350 self.show_init_spawn = true;
351 self.show_runlevel = true;
352 self.show_clock_change = true;
353 self.show_mesg = true;
354 self.show_users = true;
355 }
356
357 pub fn is_default_filter(&self) -> bool {
360 !self.show_boot
361 && !self.show_dead
362 && !self.show_login
363 && !self.show_init_spawn
364 && !self.show_runlevel
365 && !self.show_clock_change
366 && !self.show_users
367 }
368}
369
370pub fn format_time(tv_sec: i64) -> String {
372 if tv_sec == 0 {
373 return String::new();
374 }
375 let t = tv_sec as libc::time_t;
376 let tm = unsafe {
377 let mut tm: libc::tm = std::mem::zeroed();
378 libc::localtime_r(&t, &mut tm);
379 tm
380 };
381 format!(
382 "{:04}-{:02}-{:02} {:02}:{:02}",
383 tm.tm_year + 1900,
384 tm.tm_mon + 1,
385 tm.tm_mday,
386 tm.tm_hour,
387 tm.tm_min,
388 )
389}
390
391fn extract_device_path(line: &str) -> Option<String> {
395 if line.is_empty() {
396 return None;
397 }
398 let tty_part = if let Some(idx) = line.find("pts/") {
400 &line[idx..]
401 } else if let Some(idx) = line.find("tty") {
402 &line[idx..]
403 } else if line.starts_with('/') {
404 return Some(line.to_string());
405 } else {
406 line
407 };
408 if tty_part.starts_with('/') {
409 Some(tty_part.to_string())
410 } else {
411 Some(format!("/dev/{}", tty_part))
412 }
413}
414
415fn mesg_status(line: &str) -> char {
418 let dev_path = match extract_device_path(line) {
419 Some(p) => p,
420 None => return '?',
421 };
422
423 let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
424 let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
425 let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
426 if rc != 0 {
427 return '?';
428 }
429 if stat_buf.st_mode & libc::S_IWGRP != 0 {
430 '+'
431 } else {
432 '-'
433 }
434}
435
436fn idle_str(line: &str) -> String {
440 let dev_path = match extract_device_path(line) {
441 Some(p) => p,
442 None => return "?".to_string(),
443 };
444
445 let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
446 let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
447 let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
448 if rc != 0 {
449 return "?".to_string();
450 }
451
452 let now = unsafe { libc::time(std::ptr::null_mut()) };
453 let atime = stat_buf.st_atime;
454 let idle_secs = now - atime;
455
456 if idle_secs < 60 {
457 ".".to_string()
458 } else if idle_secs >= 86400 {
459 "old".to_string()
460 } else {
461 let hours = idle_secs / 3600;
462 let mins = (idle_secs % 3600) / 60;
463 format!("{:02}:{:02}", hours, mins)
464 }
465}
466
467pub fn current_tty() -> Option<String> {
469 unsafe {
470 let name = libc::ttyname(0); if name.is_null() {
472 None
473 } else {
474 let s = CStr::from_ptr(name).to_string_lossy().into_owned();
475 Some(s.strip_prefix("/dev/").unwrap_or(&s).to_string())
477 }
478 }
479}
480
481pub fn should_show(entry: &UtmpxEntry, config: &WhoConfig) -> bool {
483 if config.am_i || config.only_current {
484 if let Some(tty) = current_tty() {
486 return entry.ut_type == USER_PROCESS
488 && (entry.ut_line == tty || entry.ut_line.ends_with(&format!(" {}", tty)));
489 }
490 return false;
491 }
492
493 if config.show_count {
494 return entry.ut_type == USER_PROCESS;
495 }
496
497 if config.is_default_filter() {
498 return entry.ut_type == USER_PROCESS;
499 }
500
501 match entry.ut_type {
502 BOOT_TIME => config.show_boot,
503 DEAD_PROCESS => config.show_dead,
504 LOGIN_PROCESS => config.show_login,
505 INIT_PROCESS => config.show_init_spawn,
506 RUN_LVL => config.show_runlevel,
507 NEW_TIME | OLD_TIME => config.show_clock_change,
508 USER_PROCESS => config.show_users || config.is_default_filter(),
509 _ => false,
510 }
511}
512
513pub fn format_entry(entry: &UtmpxEntry, config: &WhoConfig) -> String {
515 let mut out = String::new();
516
517 let (name, line) = match entry.ut_type {
519 BOOT_TIME => (String::new(), "system boot".to_string()),
520 RUN_LVL => {
521 let current = (entry.ut_pid & 0xFF) as u8 as char;
522 (String::new(), format!("run-level {}", current))
523 }
524 LOGIN_PROCESS => ("LOGIN".to_string(), entry.ut_line.clone()),
525 NEW_TIME => (String::new(), entry.ut_line.clone()),
526 OLD_TIME => (String::new(), entry.ut_line.clone()),
527 _ => (entry.ut_user.clone(), entry.ut_line.clone()),
528 };
529
530 let _ = write!(out, "{:<8}", name);
532
533 if config.show_mesg {
535 let status = if entry.ut_type == USER_PROCESS {
536 mesg_status(&entry.ut_line)
537 } else {
538 ' '
540 };
541 let _ = write!(out, " {}", status);
542 }
543
544 let _ = write!(out, " {:<12}", line);
546
547 let time_str = format_time(entry.ut_tv_sec);
549 let _ = write!(out, " {}", time_str);
550
551 if config.show_users || config.show_all || config.show_login {
553 match entry.ut_type {
554 USER_PROCESS => {
555 let idle = idle_str(&entry.ut_line);
556 let _ = write!(out, " {:>5}", idle);
557 let _ = write!(out, " {:>11}", entry.ut_pid);
558 }
559 LOGIN_PROCESS => {
560 let _ = write!(out, " {:>5} {:>11}", " ", entry.ut_pid);
561 }
562 DEAD_PROCESS => {
563 let _ = write!(out, " {:>10}", entry.ut_pid);
564 }
565 _ => {}
566 }
567 }
568
569 if entry.ut_type == LOGIN_PROCESS {
571 let _ = write!(out, " id={}", entry.ut_id);
572 }
573
574 if !entry.ut_host.is_empty() && entry.ut_type != BOOT_TIME && entry.ut_type != RUN_LVL {
576 if config.show_ips {
577 let _ = write!(out, " ({})", entry.ut_host);
578 } else if config.show_lookup {
579 let resolved = lookup_host(&entry.ut_host);
580 let _ = write!(out, " ({})", resolved);
581 } else {
582 let _ = write!(out, " ({})", entry.ut_host);
583 }
584 }
585
586 out
587}
588
589fn lookup_host(host: &str) -> String {
591 let c_host = match std::ffi::CString::new(host) {
592 Ok(s) => s,
593 Err(_) => return host.to_string(),
594 };
595
596 unsafe {
597 let mut hints: libc::addrinfo = std::mem::zeroed();
598 hints.ai_flags = libc::AI_CANONNAME;
599 hints.ai_family = libc::AF_UNSPEC;
600
601 let mut result: *mut libc::addrinfo = std::ptr::null_mut();
602 let rc = libc::getaddrinfo(c_host.as_ptr(), std::ptr::null(), &hints, &mut result);
603 if rc != 0 || result.is_null() {
604 return host.to_string();
605 }
606
607 let canonical = if !(*result).ai_canonname.is_null() {
608 CStr::from_ptr((*result).ai_canonname)
609 .to_string_lossy()
610 .into_owned()
611 } else {
612 host.to_string()
613 };
614
615 libc::freeaddrinfo(result);
616 canonical
617 }
618}
619
620pub fn format_count(entries: &[UtmpxEntry]) -> String {
622 let users: Vec<&str> = entries
623 .iter()
624 .filter(|e| e.ut_type == USER_PROCESS)
625 .map(|e| e.ut_user.as_str())
626 .collect();
627
628 let mut out = String::new();
629 let _ = writeln!(out, "{}", users.join(" "));
630 let _ = write!(out, "# users={}", users.len());
631 out
632}
633
634pub fn format_heading(config: &WhoConfig) -> String {
636 let mut out = String::new();
637 let _ = write!(out, "{:<8}", "NAME");
638 if config.show_mesg {
639 let _ = write!(out, " S");
640 }
641 let _ = write!(out, " {:<12}", "LINE");
642 let _ = write!(out, " {:<16}", "TIME");
643 if config.show_users || config.show_all {
644 let _ = write!(out, " {:<6}", "IDLE");
645 let _ = write!(out, " {:>10}", "PID");
646 }
647 let _ = write!(out, " {}", "COMMENT");
648 out
649}
650
651#[cfg(target_os = "linux")]
654fn read_boot_time_from_proc() -> Option<i64> {
655 let data = std::fs::read_to_string("/proc/stat").ok()?;
656 for line in data.lines() {
657 if let Some(val) = line.strip_prefix("btime ") {
658 return val.trim().parse::<i64>().ok();
659 }
660 }
661 None
662}
663
664#[cfg(not(target_os = "linux"))]
665fn read_boot_time_from_proc() -> Option<i64> {
666 None
667}
668
669pub fn run_who(config: &WhoConfig) -> String {
671 let mut entries = read_utmpx_with_systemd_fallback();
672
673 if !entries.iter().any(|e| e.ut_type == BOOT_TIME) {
676 if let Some(btime) = read_boot_time_from_proc() {
677 entries.push(UtmpxEntry {
678 ut_type: BOOT_TIME,
679 ut_pid: 0,
680 ut_line: String::new(),
681 ut_id: String::new(),
682 ut_user: String::new(),
683 ut_host: String::new(),
684 ut_tv_sec: btime,
685 });
686 }
687 }
688
689 entries.sort_by_key(|e| e.ut_tv_sec);
691
692 let mut output = String::new();
693
694 if config.show_count {
695 return format_count(&entries);
696 }
697
698 if config.show_heading {
699 let _ = writeln!(output, "{}", format_heading(config));
700 }
701
702 for entry in &entries {
703 if should_show(entry, config) {
704 let _ = writeln!(output, "{}", format_entry(entry, config));
705 }
706 }
707
708 if output.ends_with('\n') {
710 output.pop();
711 }
712
713 output
714}