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
30pub fn read_utmpx() -> Vec<UtmpxEntry> {
36 let mut entries = Vec::new();
37
38 unsafe {
39 libc::setutxent();
40 loop {
41 let entry = libc::getutxent();
42 if entry.is_null() {
43 break;
44 }
45 let e = &*entry;
46
47 let user = cstr_from_buf(&e.ut_user);
48 let line = cstr_from_buf(&e.ut_line);
49 let host = cstr_from_buf(&e.ut_host);
50 let id = cstr_from_buf(&e.ut_id);
51
52 let tv_sec = e.ut_tv.tv_sec as i64;
53
54 entries.push(UtmpxEntry {
55 ut_type: e.ut_type as i16,
56 ut_pid: e.ut_pid,
57 ut_line: line,
58 ut_id: id,
59 ut_user: user,
60 ut_host: host,
61 ut_tv_sec: tv_sec,
62 });
63 }
64 libc::endutxent();
65 }
66
67 entries
68}
69
70unsafe fn cstr_from_buf(buf: &[libc::c_char]) -> String {
72 let len = buf.iter().position(|&c| c == 0).unwrap_or(buf.len());
74 let bytes: Vec<u8> = buf[..len].iter().map(|&c| c as u8).collect();
75 String::from_utf8_lossy(&bytes).into_owned()
76}
77
78#[derive(Clone, Debug, Default)]
80pub struct WhoConfig {
81 pub show_boot: bool,
82 pub show_dead: bool,
83 pub show_heading: bool,
84 pub show_login: bool,
85 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, }
98
99impl WhoConfig {
100 pub fn apply_all(&mut self) {
102 self.show_boot = true;
103 self.show_dead = true;
104 self.show_login = true;
105 self.show_init_spawn = true;
106 self.show_runlevel = true;
107 self.show_clock_change = true;
108 self.show_mesg = true;
109 self.show_users = true;
110 }
111
112 pub fn is_default_filter(&self) -> bool {
115 !self.show_boot
116 && !self.show_dead
117 && !self.show_login
118 && !self.show_init_spawn
119 && !self.show_runlevel
120 && !self.show_clock_change
121 && !self.show_users
122 }
123}
124
125pub fn format_time(tv_sec: i64) -> String {
127 if tv_sec == 0 {
128 return String::new();
129 }
130 let t = tv_sec as libc::time_t;
131 let tm = unsafe {
132 let mut tm: libc::tm = std::mem::zeroed();
133 libc::localtime_r(&t, &mut tm);
134 tm
135 };
136 format!(
137 "{:04}-{:02}-{:02} {:02}:{:02}",
138 tm.tm_year + 1900,
139 tm.tm_mon + 1,
140 tm.tm_mday,
141 tm.tm_hour,
142 tm.tm_min,
143 )
144}
145
146fn mesg_status(line: &str) -> char {
149 if line.is_empty() {
150 return '?';
151 }
152 let dev_path = if line.starts_with('/') {
153 line.to_string()
154 } else {
155 format!("/dev/{}", line)
156 };
157
158 let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
159 let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
160 let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
161 if rc != 0 {
162 return '?';
163 }
164 if stat_buf.st_mode & libc::S_IWGRP != 0 {
165 '+'
166 } else {
167 '-'
168 }
169}
170
171fn idle_str(line: &str) -> String {
175 if line.is_empty() {
176 return "?".to_string();
177 }
178 let dev_path = if line.starts_with('/') {
179 line.to_string()
180 } else {
181 format!("/dev/{}", line)
182 };
183
184 let mut stat_buf: libc::stat = unsafe { std::mem::zeroed() };
185 let c_path = std::ffi::CString::new(dev_path).unwrap_or_default();
186 let rc = unsafe { libc::stat(c_path.as_ptr(), &mut stat_buf) };
187 if rc != 0 {
188 return "?".to_string();
189 }
190
191 let now = unsafe { libc::time(std::ptr::null_mut()) };
192 let atime = stat_buf.st_atime;
193 let idle_secs = now - atime;
194
195 if idle_secs < 60 {
196 ".".to_string()
197 } else if idle_secs >= 86400 {
198 "old".to_string()
199 } else {
200 let hours = idle_secs / 3600;
201 let mins = (idle_secs % 3600) / 60;
202 format!("{:02}:{:02}", hours, mins)
203 }
204}
205
206pub fn current_tty() -> Option<String> {
208 unsafe {
209 let name = libc::ttyname(0); if name.is_null() {
211 None
212 } else {
213 let s = CStr::from_ptr(name).to_string_lossy().into_owned();
214 Some(s.strip_prefix("/dev/").unwrap_or(&s).to_string())
216 }
217 }
218}
219
220pub fn should_show(entry: &UtmpxEntry, config: &WhoConfig) -> bool {
222 if config.am_i || config.only_current {
223 if let Some(tty) = current_tty() {
225 return entry.ut_type == USER_PROCESS && entry.ut_line == tty;
226 }
227 return false;
228 }
229
230 if config.show_count {
231 return entry.ut_type == USER_PROCESS;
232 }
233
234 if config.is_default_filter() {
235 return entry.ut_type == USER_PROCESS;
236 }
237
238 match entry.ut_type {
239 BOOT_TIME => config.show_boot,
240 DEAD_PROCESS => config.show_dead,
241 LOGIN_PROCESS => config.show_login,
242 INIT_PROCESS => config.show_init_spawn,
243 RUN_LVL => config.show_runlevel,
244 NEW_TIME | OLD_TIME => config.show_clock_change,
245 USER_PROCESS => config.show_users || config.is_default_filter(),
246 _ => false,
247 }
248}
249
250pub fn format_entry(entry: &UtmpxEntry, config: &WhoConfig) -> String {
252 let mut out = String::new();
253
254 let (name, line) = match entry.ut_type {
256 BOOT_TIME => (String::new(), "system boot".to_string()),
257 RUN_LVL => {
258 let current = (entry.ut_pid & 0xFF) as u8 as char;
259 (String::new(), format!("run-level {}", current))
260 }
261 LOGIN_PROCESS => ("LOGIN".to_string(), entry.ut_line.clone()),
262 NEW_TIME => (String::new(), entry.ut_line.clone()),
263 OLD_TIME => (String::new(), entry.ut_line.clone()),
264 _ => (entry.ut_user.clone(), entry.ut_line.clone()),
265 };
266
267 let _ = write!(out, "{:<8}", name);
269
270 if config.show_mesg {
272 let status = if entry.ut_type == USER_PROCESS {
273 mesg_status(&entry.ut_line)
274 } else {
275 '?'
276 };
277 let _ = write!(out, " {}", status);
278 }
279
280 let _ = write!(out, " {:<12}", line);
282
283 let time_str = format_time(entry.ut_tv_sec);
285 let _ = write!(out, " {}", time_str);
286
287 if config.show_users || config.show_all {
289 match entry.ut_type {
290 USER_PROCESS => {
291 let idle = idle_str(&entry.ut_line);
292 let _ = write!(out, " {:>5}", idle);
293 let _ = write!(out, " {:>10}", entry.ut_pid);
294 }
295 LOGIN_PROCESS => {
296 let _ = write!(out, " ? {:>10}", entry.ut_pid);
297 }
298 DEAD_PROCESS => {
299 let _ = write!(out, " {:>10}", entry.ut_pid);
300 }
301 _ => {}
302 }
303 }
304
305 if entry.ut_type == LOGIN_PROCESS {
307 if !(config.show_users || config.show_all) {
308 let _ = write!(out, " {:>5}", entry.ut_pid);
310 }
311 let _ = write!(out, " id={}", entry.ut_id);
312 }
313
314 if !entry.ut_host.is_empty() {
316 if config.show_ips {
317 let _ = write!(out, " ({})", entry.ut_host);
318 } else if config.show_lookup {
319 let resolved = lookup_host(&entry.ut_host);
320 let _ = write!(out, " ({})", resolved);
321 } else {
322 let _ = write!(out, " ({})", entry.ut_host);
323 }
324 }
325
326 out
327}
328
329fn lookup_host(host: &str) -> String {
331 let c_host = match std::ffi::CString::new(host) {
332 Ok(s) => s,
333 Err(_) => return host.to_string(),
334 };
335
336 unsafe {
337 let mut hints: libc::addrinfo = std::mem::zeroed();
338 hints.ai_flags = libc::AI_CANONNAME;
339 hints.ai_family = libc::AF_UNSPEC;
340
341 let mut result: *mut libc::addrinfo = std::ptr::null_mut();
342 let rc = libc::getaddrinfo(c_host.as_ptr(), std::ptr::null(), &hints, &mut result);
343 if rc != 0 || result.is_null() {
344 return host.to_string();
345 }
346
347 let canonical = if !(*result).ai_canonname.is_null() {
348 CStr::from_ptr((*result).ai_canonname)
349 .to_string_lossy()
350 .into_owned()
351 } else {
352 host.to_string()
353 };
354
355 libc::freeaddrinfo(result);
356 canonical
357 }
358}
359
360pub fn format_count(entries: &[UtmpxEntry]) -> String {
362 let users: Vec<&str> = entries
363 .iter()
364 .filter(|e| e.ut_type == USER_PROCESS)
365 .map(|e| e.ut_user.as_str())
366 .collect();
367
368 let mut out = String::new();
369 let _ = writeln!(out, "{}", users.join(" "));
370 let _ = write!(out, "# users={}", users.len());
371 out
372}
373
374pub fn format_heading(config: &WhoConfig) -> String {
376 let mut out = String::new();
377 let _ = write!(out, "{:<8}", "NAME");
378 if config.show_mesg {
379 let _ = write!(out, " S");
380 }
381 let _ = write!(out, " {:<12}", "LINE");
382 let _ = write!(out, " {:<16}", "TIME");
383 if config.show_users || config.show_all {
384 let _ = write!(out, " {:<6}", "IDLE");
385 let _ = write!(out, " {:>10}", "PID");
386 }
387 let _ = write!(out, " {}", "COMMENT");
388 out
389}
390
391pub fn run_who(config: &WhoConfig) -> String {
393 let entries = read_utmpx();
394 let mut output = String::new();
395
396 if config.show_count {
397 return format_count(&entries);
398 }
399
400 if config.show_heading {
401 let _ = writeln!(output, "{}", format_heading(config));
402 }
403
404 for entry in &entries {
405 if should_show(entry, config) {
406 let _ = writeln!(output, "{}", format_entry(entry, config));
407 }
408 }
409
410 if output.ends_with('\n') {
412 output.pop();
413 }
414
415 output
416}