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 if entry.ut_type == LOGIN_PROCESS || entry.ut_type == DEAD_PROCESS {
538 '?'
539 } else {
540 ' '
542 };
543 let _ = write!(out, " {}", status);
544 }
545
546 let _ = write!(out, " {:<12}", line);
548
549 let time_str = format_time(entry.ut_tv_sec);
551 let _ = write!(out, " {}", time_str);
552
553 if config.show_users || config.show_all {
555 match entry.ut_type {
556 USER_PROCESS => {
557 let idle = idle_str(&entry.ut_line);
558 let _ = write!(out, " {:>5}", idle);
559 let _ = write!(out, " {:>11}", entry.ut_pid);
560 }
561 LOGIN_PROCESS => {
562 let _ = write!(out, " ? {:>10}", entry.ut_pid);
563 }
564 DEAD_PROCESS => {
565 let _ = write!(out, " {:>10}", entry.ut_pid);
566 }
567 _ => {}
568 }
569 }
570
571 if entry.ut_type == LOGIN_PROCESS {
573 if !(config.show_users || config.show_all) {
574 let _ = write!(out, " {:>5}", entry.ut_pid);
576 }
577 let _ = write!(out, " id={}", entry.ut_id);
578 }
579
580 if !entry.ut_host.is_empty() {
582 if config.show_ips {
583 let _ = write!(out, " ({})", entry.ut_host);
584 } else if config.show_lookup {
585 let resolved = lookup_host(&entry.ut_host);
586 let _ = write!(out, " ({})", resolved);
587 } else {
588 let _ = write!(out, " ({})", entry.ut_host);
589 }
590 }
591
592 out
593}
594
595fn lookup_host(host: &str) -> String {
597 let c_host = match std::ffi::CString::new(host) {
598 Ok(s) => s,
599 Err(_) => return host.to_string(),
600 };
601
602 unsafe {
603 let mut hints: libc::addrinfo = std::mem::zeroed();
604 hints.ai_flags = libc::AI_CANONNAME;
605 hints.ai_family = libc::AF_UNSPEC;
606
607 let mut result: *mut libc::addrinfo = std::ptr::null_mut();
608 let rc = libc::getaddrinfo(c_host.as_ptr(), std::ptr::null(), &hints, &mut result);
609 if rc != 0 || result.is_null() {
610 return host.to_string();
611 }
612
613 let canonical = if !(*result).ai_canonname.is_null() {
614 CStr::from_ptr((*result).ai_canonname)
615 .to_string_lossy()
616 .into_owned()
617 } else {
618 host.to_string()
619 };
620
621 libc::freeaddrinfo(result);
622 canonical
623 }
624}
625
626pub fn format_count(entries: &[UtmpxEntry]) -> String {
628 let users: Vec<&str> = entries
629 .iter()
630 .filter(|e| e.ut_type == USER_PROCESS)
631 .map(|e| e.ut_user.as_str())
632 .collect();
633
634 let mut out = String::new();
635 let _ = writeln!(out, "{}", users.join(" "));
636 let _ = write!(out, "# users={}", users.len());
637 out
638}
639
640pub fn format_heading(config: &WhoConfig) -> String {
642 let mut out = String::new();
643 let _ = write!(out, "{:<8}", "NAME");
644 if config.show_mesg {
645 let _ = write!(out, " S");
646 }
647 let _ = write!(out, " {:<12}", "LINE");
648 let _ = write!(out, " {:<16}", "TIME");
649 if config.show_users || config.show_all {
650 let _ = write!(out, " {:<6}", "IDLE");
651 let _ = write!(out, " {:>10}", "PID");
652 }
653 let _ = write!(out, " {}", "COMMENT");
654 out
655}
656
657#[cfg(target_os = "linux")]
660fn read_boot_time_from_proc() -> Option<i64> {
661 let data = std::fs::read_to_string("/proc/stat").ok()?;
662 for line in data.lines() {
663 if let Some(val) = line.strip_prefix("btime ") {
664 return val.trim().parse::<i64>().ok();
665 }
666 }
667 None
668}
669
670#[cfg(not(target_os = "linux"))]
671fn read_boot_time_from_proc() -> Option<i64> {
672 None
673}
674
675pub fn run_who(config: &WhoConfig) -> String {
677 let mut entries = read_utmpx_with_systemd_fallback();
678
679 if !entries.iter().any(|e| e.ut_type == BOOT_TIME) {
682 if let Some(btime) = read_boot_time_from_proc() {
683 entries.push(UtmpxEntry {
684 ut_type: BOOT_TIME,
685 ut_pid: 0,
686 ut_line: String::new(),
687 ut_id: String::new(),
688 ut_user: String::new(),
689 ut_host: String::new(),
690 ut_tv_sec: btime,
691 });
692 }
693 }
694
695 entries.sort_by_key(|e| e.ut_tv_sec);
697
698 let mut output = String::new();
699
700 if config.show_count {
701 return format_count(&entries);
702 }
703
704 if config.show_heading {
705 let _ = writeln!(output, "{}", format_heading(config));
706 }
707
708 for entry in &entries {
709 if should_show(entry, config) {
710 let _ = writeln!(output, "{}", format_entry(entry, config));
711 }
712 }
713
714 if output.ends_with('\n') {
716 output.pop();
717 }
718
719 output
720}