1use std::ffi::CStr;
16use std::mem::MaybeUninit;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19use crate::error::{UResult, USimpleError};
20
21mod ffi {
23 use std::ffi::c_char;
24 use std::os::raw::{c_int, c_uint};
25
26 #[link(name = "systemd")]
27 unsafe extern "C" {
28 pub fn sd_get_sessions(sessions: *mut *mut *mut c_char) -> c_int;
29 pub fn sd_session_get_uid(session: *const c_char, uid: *mut c_uint) -> c_int;
30 pub fn sd_session_get_start_time(session: *const c_char, usec: *mut u64) -> c_int;
31 pub fn sd_session_get_tty(session: *const c_char, tty: *mut *mut c_char) -> c_int;
32 pub fn sd_session_get_remote_host(
33 session: *const c_char,
34 remote_host: *mut *mut c_char,
35 ) -> c_int;
36 pub fn sd_session_get_display(session: *const c_char, display: *mut *mut c_char) -> c_int;
37 pub fn sd_session_get_type(session: *const c_char, session_type: *mut *mut c_char)
38 -> c_int;
39 pub fn sd_session_get_seat(session: *const c_char, seat: *mut *mut c_char) -> c_int;
40
41 }
42}
43
44mod login {
46 use super::ffi;
47 use std::ffi::{CStr, CString};
48 use std::ptr;
49 use std::time::SystemTime;
50
51 pub fn get_sessions() -> Result<Vec<String>, Box<dyn std::error::Error>> {
53 let mut sessions_ptr: *mut *mut libc::c_char = ptr::null_mut();
54
55 let result = unsafe { ffi::sd_get_sessions(&raw mut sessions_ptr) };
56
57 if result < 0 {
58 return Err(format!("sd_get_sessions failed: {result}").into());
59 }
60
61 let mut sessions = Vec::new();
62 if !sessions_ptr.is_null() {
63 let mut i = 0;
64 loop {
65 let session_ptr = unsafe { *sessions_ptr.add(i) };
66 if session_ptr.is_null() {
67 break;
68 }
69
70 let session_cstr = unsafe { CStr::from_ptr(session_ptr) };
71 sessions.push(session_cstr.to_string_lossy().into_owned());
72
73 unsafe { libc::free(session_ptr.cast()) };
74 i += 1;
75 }
76
77 unsafe { libc::free(sessions_ptr.cast()) };
78 }
79
80 Ok(sessions)
81 }
82
83 pub fn get_session_uid(session_id: &str) -> Result<u32, Box<dyn std::error::Error>> {
85 let session_cstring = CString::new(session_id)?;
86 let mut uid: std::os::raw::c_uint = 0;
87
88 let result = unsafe { ffi::sd_session_get_uid(session_cstring.as_ptr(), &raw mut uid) };
89
90 if result < 0 {
91 return Err(
92 format!("sd_session_get_uid failed for session '{session_id}': {result}").into(),
93 );
94 }
95
96 Ok(uid)
97 }
98
99 pub fn get_session_start_time(session_id: &str) -> Result<u64, Box<dyn std::error::Error>> {
101 let session_cstring = CString::new(session_id)?;
102 let mut usec: u64 = 0;
103
104 let result =
105 unsafe { ffi::sd_session_get_start_time(session_cstring.as_ptr(), &raw mut usec) };
106
107 if result < 0 {
108 return Err(format!(
109 "sd_session_get_start_time failed for session '{session_id}': {result}",
110 )
111 .into());
112 }
113
114 Ok(usec)
115 }
116
117 pub fn get_session_tty(session_id: &str) -> Result<Option<String>, Box<dyn std::error::Error>> {
119 let session_cstring = CString::new(session_id)?;
120 let mut tty_ptr: *mut libc::c_char = ptr::null_mut();
121
122 let result = unsafe { ffi::sd_session_get_tty(session_cstring.as_ptr(), &raw mut tty_ptr) };
123
124 if result < 0 {
125 return Err(
126 format!("sd_session_get_tty failed for session '{session_id}': {result}").into(),
127 );
128 }
129
130 if tty_ptr.is_null() {
131 return Ok(None);
132 }
133
134 let tty_cstr = unsafe { CStr::from_ptr(tty_ptr) };
135 let tty_string = tty_cstr.to_string_lossy().into_owned();
136
137 unsafe { libc::free(tty_ptr.cast()) };
138
139 Ok(Some(tty_string))
140 }
141
142 pub fn get_session_remote_host(
144 session_id: &str,
145 ) -> Result<Option<String>, Box<dyn std::error::Error>> {
146 let session_cstring = CString::new(session_id)?;
147 let mut host_ptr: *mut libc::c_char = ptr::null_mut();
148
149 let result =
150 unsafe { ffi::sd_session_get_remote_host(session_cstring.as_ptr(), &raw mut host_ptr) };
151
152 if result < 0 {
153 return Err(format!(
154 "sd_session_get_remote_host failed for session '{session_id}': {result}",
155 )
156 .into());
157 }
158
159 if host_ptr.is_null() {
160 return Ok(None);
161 }
162
163 let host_cstr = unsafe { CStr::from_ptr(host_ptr) };
164 let host_string = host_cstr.to_string_lossy().into_owned();
165
166 unsafe { libc::free(host_ptr.cast()) };
167
168 Ok(Some(host_string))
169 }
170
171 pub fn get_session_display(
173 session_id: &str,
174 ) -> Result<Option<String>, Box<dyn std::error::Error>> {
175 let session_cstring = CString::new(session_id)?;
176 let mut display_ptr: *mut libc::c_char = ptr::null_mut();
177
178 let result =
179 unsafe { ffi::sd_session_get_display(session_cstring.as_ptr(), &raw mut display_ptr) };
180
181 if result < 0 {
182 return Err(format!(
183 "sd_session_get_display failed for session '{session_id}': {result}",
184 )
185 .into());
186 }
187
188 if display_ptr.is_null() {
189 return Ok(None);
190 }
191
192 let display_cstr = unsafe { CStr::from_ptr(display_ptr) };
193 let display_string = display_cstr.to_string_lossy().into_owned();
194
195 unsafe { libc::free(display_ptr.cast()) };
196
197 Ok(Some(display_string))
198 }
199
200 pub fn get_session_type(
202 session_id: &str,
203 ) -> Result<Option<String>, Box<dyn std::error::Error>> {
204 let session_cstring = CString::new(session_id)?;
205 let mut type_ptr: *mut libc::c_char = ptr::null_mut();
206
207 let result =
208 unsafe { ffi::sd_session_get_type(session_cstring.as_ptr(), &raw mut type_ptr) };
209
210 if result < 0 {
211 return Err(
212 format!("sd_session_get_type failed for session '{session_id}': {result}").into(),
213 );
214 }
215
216 if type_ptr.is_null() {
217 return Ok(None);
218 }
219
220 let type_cstr = unsafe { CStr::from_ptr(type_ptr) };
221 let type_string = type_cstr.to_string_lossy().into_owned();
222
223 unsafe { libc::free(type_ptr.cast()) };
224
225 Ok(Some(type_string))
226 }
227
228 pub fn get_session_seat(
230 session_id: &str,
231 ) -> Result<Option<String>, Box<dyn std::error::Error>> {
232 let session_cstring = CString::new(session_id)?;
233 let mut seat_ptr: *mut libc::c_char = ptr::null_mut();
234
235 let result =
236 unsafe { ffi::sd_session_get_seat(session_cstring.as_ptr(), &raw mut seat_ptr) };
237
238 if result < 0 {
239 return Err(
240 format!("sd_session_get_seat failed for session '{session_id}': {result}").into(),
241 );
242 }
243
244 if seat_ptr.is_null() {
245 return Ok(None);
246 }
247
248 let seat_cstr = unsafe { CStr::from_ptr(seat_ptr) };
249 let seat_string = seat_cstr.to_string_lossy().into_owned();
250
251 unsafe { libc::free(seat_ptr.cast()) };
252
253 Ok(Some(seat_string))
254 }
255
256 pub fn get_boot_time() -> Result<SystemTime, Box<dyn std::error::Error>> {
267 use std::fs;
268
269 let metadata = fs::metadata("/var/lib/systemd/random-seed")
270 .map_err(|e| format!("Failed to read /var/lib/systemd/random-seed: {e}"))?;
271
272 metadata
273 .modified()
274 .map_err(|e| format!("Failed to get modification time: {e}").into())
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct SystemdLoginRecord {
281 pub user: String,
282 pub session_id: String,
283 pub seat_or_tty: String,
284 pub raw_device: String,
285 pub host: String,
286 pub login_time: SystemTime,
287 pub pid: u32,
288 pub session_leader_pid: u32,
289 pub record_type: SystemdRecordType,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq)]
293pub enum SystemdRecordType {
294 UserProcess = 7, LoginProcess = 6, BootTime = 2, }
298
299impl SystemdLoginRecord {
300 pub fn is_user_process(&self) -> bool {
302 !self.user.is_empty() && self.record_type == SystemdRecordType::UserProcess
303 }
304
305 pub fn login_time_offset(&self) -> time::OffsetDateTime {
307 let duration = self
308 .login_time
309 .duration_since(UNIX_EPOCH)
310 .unwrap_or_default();
311 let ts_nanos: i128 = (duration.as_nanos()).try_into().unwrap_or(0);
312 let local_offset = time::OffsetDateTime::now_local()
313 .map_or_else(|_| time::UtcOffset::UTC, time::OffsetDateTime::offset);
314 time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos)
315 .unwrap_or_else(|_| {
316 time::OffsetDateTime::now_local()
317 .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
318 })
319 .to_offset(local_offset)
320 }
321}
322
323pub fn read_login_records() -> UResult<Vec<SystemdLoginRecord>> {
326 let mut records = Vec::new();
327
328 if let Ok(boot_time) = login::get_boot_time() {
330 let boot_record = SystemdLoginRecord {
331 user: "reboot".to_string(),
332 session_id: "boot".to_string(),
333 seat_or_tty: "~".to_string(), raw_device: String::new(),
335 host: String::new(),
336 login_time: boot_time,
337 pid: 0,
338 session_leader_pid: 0,
339 record_type: SystemdRecordType::BootTime,
340 };
341 records.push(boot_record);
342 }
343
344 let mut sessions = login::get_sessions()
346 .map_err(|e| USimpleError::new(1, format!("Failed to get systemd sessions: {e}")))?;
347
348 sessions.sort();
350 sessions.reverse();
351
352 for session_id in sessions {
354 let Ok(uid) = login::get_session_uid(&session_id) else {
356 continue;
357 };
358
359 let user = unsafe {
361 let mut passwd = MaybeUninit::<libc::passwd>::uninit();
362
363 let buf_size = {
365 let size = libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX);
366 if size == -1 {
367 16384 } else {
369 size as usize
370 }
371 };
372 let mut buf = vec![0u8; buf_size];
373 let mut result: *mut libc::passwd = std::ptr::null_mut();
374
375 let ret = libc::getpwuid_r(
376 uid,
377 passwd.as_mut_ptr(),
378 buf.as_mut_ptr().cast(),
379 buf.len(),
380 &raw mut result,
381 );
382
383 if ret == 0 && !result.is_null() {
384 let passwd = passwd.assume_init();
385 CStr::from_ptr(passwd.pw_name)
386 .to_string_lossy()
387 .into_owned()
388 } else {
389 format!("{uid}") }
391 };
392
393 let start_time = login::get_session_start_time(&session_id).map_or(UNIX_EPOCH, |usec| {
395 UNIX_EPOCH + std::time::Duration::from_micros(usec)
396 });
397
398 let mut tty = login::get_session_tty(&session_id)
400 .ok()
401 .flatten()
402 .unwrap_or_default();
403
404 let mut seat = login::get_session_seat(&session_id)
406 .ok()
407 .flatten()
408 .unwrap_or_default();
409
410 if tty.starts_with('?') {
412 tty = tty[1..].to_string();
413 }
414 if seat.starts_with('?') {
415 seat = seat[1..].to_string();
416 }
417
418 let remote_host = login::get_session_remote_host(&session_id)
420 .ok()
421 .flatten()
422 .unwrap_or_default();
423
424 let display = login::get_session_display(&session_id)
426 .ok()
427 .flatten()
428 .unwrap_or_default();
429
430 let _session_type = login::get_session_type(&session_id)
432 .ok()
433 .flatten()
434 .unwrap_or_default();
435
436 let host = if remote_host.is_empty() {
439 display.clone()
440 } else {
441 remote_host
442 };
443
444 if tty.is_empty() && seat.is_empty() && display.is_empty() {
446 continue;
447 }
448
449 let create_record = |seat_or_tty: String,
456 raw_device: String,
457 user: String,
458 session_id: String,
459 host: String| {
460 SystemdLoginRecord {
461 user,
462 session_id,
463 seat_or_tty,
464 raw_device,
465 host,
466 login_time: start_time,
467 pid: 0, session_leader_pid: 0,
469 record_type: SystemdRecordType::UserProcess,
470 }
471 };
472
473 if !seat.is_empty() && !tty.is_empty() {
475 let seat_formatted = format!("?{seat}");
478 records.push(create_record(
479 seat_formatted,
480 seat,
481 user.clone(),
482 session_id.clone(),
483 host.clone(),
484 ));
485
486 let tty_formatted = if tty.starts_with("tty") {
487 format!("*{tty}")
488 } else {
489 tty.clone()
490 };
491 records.push(create_record(tty_formatted, tty, user, session_id, host)); } else if !seat.is_empty() {
493 let seat_formatted = format!("?{seat}");
495 records.push(create_record(seat_formatted, seat, user, session_id, host));
496 } else if !tty.is_empty() {
497 let tty_formatted = if tty.starts_with("tty") {
499 format!("*{tty}")
500 } else {
501 tty.clone()
502 };
503 records.push(create_record(tty_formatted, tty, user, session_id, host));
504 } else if !display.is_empty() {
505 records.push(create_record(
508 display,
509 String::new(),
510 user,
511 session_id,
512 host,
513 ));
514 }
515 }
516
517 Ok(records)
518}
519
520pub struct SystemdUtmpxCompat {
522 record: SystemdLoginRecord,
523}
524
525impl SystemdUtmpxCompat {
526 pub fn new(record: SystemdLoginRecord) -> Self {
528 Self { record }
529 }
530
531 pub fn record_type(&self) -> i16 {
533 self.record.record_type as i16
534 }
535
536 pub fn pid(&self) -> i32 {
538 self.record.pid as i32
539 }
540
541 pub fn terminal_suffix(&self) -> String {
543 self.record.session_id.clone()
545 }
546
547 pub fn user(&self) -> String {
549 self.record.user.clone()
550 }
551
552 pub fn host(&self) -> String {
554 self.record.host.clone()
555 }
556
557 pub fn tty_device(&self) -> String {
559 if self.record.raw_device.is_empty() {
561 self.record.seat_or_tty.clone()
562 } else {
563 self.record.raw_device.clone()
564 }
565 }
566
567 pub fn login_time(&self) -> time::OffsetDateTime {
569 self.record.login_time_offset()
570 }
571
572 pub fn exit_status(&self) -> (i16, i16) {
574 (0, 0) }
576
577 pub fn is_user_process(&self) -> bool {
579 self.record.is_user_process()
580 }
581
582 pub fn canon_host(&self) -> String {
584 self.record.host.clone()
587 }
588}
589
590pub struct SystemdUtmpxIter {
592 records: Vec<SystemdLoginRecord>,
593 current_index: usize,
594}
595
596impl SystemdUtmpxIter {
597 pub fn new() -> UResult<Self> {
599 let records = read_login_records()?;
600 Ok(Self {
601 records,
602 current_index: 0,
603 })
604 }
605
606 pub fn empty() -> Self {
608 Self {
609 records: Vec::new(),
610 current_index: 0,
611 }
612 }
613
614 pub fn next_record(&mut self) -> Option<SystemdUtmpxCompat> {
616 if self.current_index >= self.records.len() {
617 return None;
618 }
619
620 let record = self.records[self.current_index].clone();
621 self.current_index += 1;
622
623 Some(SystemdUtmpxCompat::new(record))
624 }
625
626 pub fn get_all_records(&self) -> Vec<SystemdUtmpxCompat> {
628 self.records
629 .iter()
630 .cloned()
631 .map(SystemdUtmpxCompat::new)
632 .collect()
633 }
634
635 pub fn reset(&mut self) {
637 self.current_index = 0;
638 }
639
640 pub fn len(&self) -> usize {
642 self.records.len()
643 }
644
645 pub fn is_empty(&self) -> bool {
647 self.records.is_empty()
648 }
649}
650
651impl Iterator for SystemdUtmpxIter {
652 type Item = SystemdUtmpxCompat;
653
654 fn next(&mut self) -> Option<Self::Item> {
655 self.next_record()
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_empty_iterator() {
665 let mut iter = SystemdUtmpxIter::empty();
666
667 assert_eq!(iter.len(), 0);
668 assert!(iter.is_empty());
669 assert!(iter.next().is_none());
670 assert!(iter.next_record().is_none());
671 }
672
673 #[test]
674 fn test_iterator_with_mock_data() {
675 let mock_records = vec![
677 SystemdLoginRecord {
678 session_id: "session1".to_string(),
679 user: "user1".to_string(),
680 seat_or_tty: "tty1".to_string(),
681 raw_device: "tty1".to_string(),
682 host: "host1".to_string(),
683 login_time: UNIX_EPOCH,
684 pid: 1234,
685 session_leader_pid: 1234,
686 record_type: SystemdRecordType::UserProcess,
687 },
688 SystemdLoginRecord {
689 session_id: "session2".to_string(),
690 user: "user2".to_string(),
691 seat_or_tty: "pts/0".to_string(),
692 raw_device: "pts/0".to_string(),
693 host: "host2".to_string(),
694 login_time: UNIX_EPOCH,
695 pid: 5678,
696 session_leader_pid: 5678,
697 record_type: SystemdRecordType::UserProcess,
698 },
699 ];
700
701 let mut iter = SystemdUtmpxIter {
702 records: mock_records,
703 current_index: 0,
704 };
705
706 assert_eq!(iter.len(), 2);
707 assert!(!iter.is_empty());
708
709 let first = iter.next();
711 assert!(first.is_some());
712
713 let second = iter.next();
714 assert!(second.is_some());
715
716 let third = iter.next();
717 assert!(third.is_none());
718
719 assert!(iter.next().is_none());
721 }
722
723 #[test]
724 fn test_get_all_records() {
725 let mock_records = vec![SystemdLoginRecord {
726 session_id: "session1".to_string(),
727 user: "user1".to_string(),
728 seat_or_tty: "tty1".to_string(),
729 raw_device: "tty1".to_string(),
730 host: "host1".to_string(),
731 login_time: UNIX_EPOCH,
732 pid: 1234,
733 session_leader_pid: 1234,
734 record_type: SystemdRecordType::UserProcess,
735 }];
736
737 let iter = SystemdUtmpxIter {
738 records: mock_records,
739 current_index: 0,
740 };
741
742 let all_records = iter.get_all_records();
743 assert_eq!(all_records.len(), 1);
744 }
745
746 #[test]
747 fn test_systemd_record_conversion() {
748 let record = SystemdLoginRecord {
750 session_id: "c1".to_string(),
751 user: "testuser".to_string(),
752 seat_or_tty: "seat0".to_string(),
753 raw_device: "seat0".to_string(),
754 host: "localhost".to_string(),
755 login_time: UNIX_EPOCH + std::time::Duration::from_secs(1000),
756 pid: 9999,
757 session_leader_pid: 9999,
758 record_type: SystemdRecordType::UserProcess,
759 };
760
761 let compat = SystemdUtmpxCompat::new(record);
762
763 assert_eq!(compat.user(), "testuser");
765 assert_eq!(compat.tty_device().as_str(), "seat0");
766 assert_eq!(compat.host(), "localhost");
767 }
768}