use std::ffi::CStr;
use std::mem::MaybeUninit;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{UResult, USimpleError};
use crate::utmpx;
mod ffi {
use std::ffi::c_char;
use std::os::raw::{c_int, c_uint};
#[link(name = "systemd")]
unsafe extern "C" {
pub fn sd_get_sessions(sessions: *mut *mut *mut c_char) -> c_int;
pub fn sd_session_get_uid(session: *const c_char, uid: *mut c_uint) -> c_int;
pub fn sd_session_get_start_time(session: *const c_char, usec: *mut u64) -> c_int;
pub fn sd_session_get_tty(session: *const c_char, tty: *mut *mut c_char) -> c_int;
pub fn sd_session_get_remote_host(
session: *const c_char,
remote_host: *mut *mut c_char,
) -> c_int;
pub fn sd_session_get_display(session: *const c_char, display: *mut *mut c_char) -> c_int;
pub fn sd_session_get_type(session: *const c_char, session_type: *mut *mut c_char)
-> c_int;
pub fn sd_session_get_seat(session: *const c_char, seat: *mut *mut c_char) -> c_int;
}
}
mod login {
use super::ffi;
use std::ffi::{CStr, CString};
use std::ptr;
use std::time::SystemTime;
pub fn get_sessions() -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut sessions_ptr: *mut *mut libc::c_char = ptr::null_mut();
let result = unsafe { ffi::sd_get_sessions(&mut sessions_ptr) };
if result < 0 {
return Err(format!("sd_get_sessions failed: {result}").into());
}
let mut sessions = Vec::new();
if !sessions_ptr.is_null() {
let mut i = 0;
loop {
let session_ptr = unsafe { *sessions_ptr.add(i) };
if session_ptr.is_null() {
break;
}
let session_cstr = unsafe { CStr::from_ptr(session_ptr) };
sessions.push(session_cstr.to_string_lossy().into_owned());
unsafe { libc::free(session_ptr as *mut libc::c_void) };
i += 1;
}
unsafe { libc::free(sessions_ptr as *mut libc::c_void) };
}
Ok(sessions)
}
pub fn get_session_uid(session_id: &str) -> Result<u32, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut uid: std::os::raw::c_uint = 0;
let result = unsafe { ffi::sd_session_get_uid(session_cstring.as_ptr(), &mut uid) };
if result < 0 {
return Err(
format!("sd_session_get_uid failed for session '{session_id}': {result}",).into(),
);
}
Ok(uid)
}
pub fn get_session_start_time(session_id: &str) -> Result<u64, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut usec: u64 = 0;
let result = unsafe { ffi::sd_session_get_start_time(session_cstring.as_ptr(), &mut usec) };
if result < 0 {
return Err(format!(
"sd_session_get_start_time failed for session '{session_id}': {result}",
)
.into());
}
Ok(usec)
}
pub fn get_session_tty(session_id: &str) -> Result<Option<String>, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut tty_ptr: *mut libc::c_char = ptr::null_mut();
let result = unsafe { ffi::sd_session_get_tty(session_cstring.as_ptr(), &mut tty_ptr) };
if result < 0 {
return Err(
format!("sd_session_get_tty failed for session '{session_id}': {result}",).into(),
);
}
if tty_ptr.is_null() {
return Ok(None);
}
let tty_cstr = unsafe { CStr::from_ptr(tty_ptr) };
let tty_string = tty_cstr.to_string_lossy().into_owned();
unsafe { libc::free(tty_ptr as *mut libc::c_void) };
Ok(Some(tty_string))
}
pub fn get_session_remote_host(
session_id: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut host_ptr: *mut libc::c_char = ptr::null_mut();
let result =
unsafe { ffi::sd_session_get_remote_host(session_cstring.as_ptr(), &mut host_ptr) };
if result < 0 {
return Err(format!(
"sd_session_get_remote_host failed for session '{session_id}': {result}",
)
.into());
}
if host_ptr.is_null() {
return Ok(None);
}
let host_cstr = unsafe { CStr::from_ptr(host_ptr) };
let host_string = host_cstr.to_string_lossy().into_owned();
unsafe { libc::free(host_ptr as *mut libc::c_void) };
Ok(Some(host_string))
}
pub fn get_session_display(
session_id: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut display_ptr: *mut libc::c_char = ptr::null_mut();
let result =
unsafe { ffi::sd_session_get_display(session_cstring.as_ptr(), &mut display_ptr) };
if result < 0 {
return Err(format!(
"sd_session_get_display failed for session '{session_id}': {result}",
)
.into());
}
if display_ptr.is_null() {
return Ok(None);
}
let display_cstr = unsafe { CStr::from_ptr(display_ptr) };
let display_string = display_cstr.to_string_lossy().into_owned();
unsafe { libc::free(display_ptr as *mut libc::c_void) };
Ok(Some(display_string))
}
pub fn get_session_type(
session_id: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut type_ptr: *mut libc::c_char = ptr::null_mut();
let result = unsafe { ffi::sd_session_get_type(session_cstring.as_ptr(), &mut type_ptr) };
if result < 0 {
return Err(
format!("sd_session_get_type failed for session '{session_id}': {result}",).into(),
);
}
if type_ptr.is_null() {
return Ok(None);
}
let type_cstr = unsafe { CStr::from_ptr(type_ptr) };
let type_string = type_cstr.to_string_lossy().into_owned();
unsafe { libc::free(type_ptr as *mut libc::c_void) };
Ok(Some(type_string))
}
pub fn get_session_seat(
session_id: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
let session_cstring = CString::new(session_id)?;
let mut seat_ptr: *mut libc::c_char = ptr::null_mut();
let result = unsafe { ffi::sd_session_get_seat(session_cstring.as_ptr(), &mut seat_ptr) };
if result < 0 {
return Err(
format!("sd_session_get_seat failed for session '{session_id}': {result}",).into(),
);
}
if seat_ptr.is_null() {
return Ok(None);
}
let seat_cstr = unsafe { CStr::from_ptr(seat_ptr) };
let seat_string = seat_cstr.to_string_lossy().into_owned();
unsafe { libc::free(seat_ptr as *mut libc::c_void) };
Ok(Some(seat_string))
}
pub fn get_boot_time() -> Result<SystemTime, Box<dyn std::error::Error>> {
use std::fs;
let metadata = fs::metadata("/var/lib/systemd/random-seed")
.map_err(|e| format!("Failed to read /var/lib/systemd/random-seed: {e}"))?;
metadata
.modified()
.map_err(|e| format!("Failed to get modification time: {e}").into())
}
}
#[derive(Debug, Clone)]
pub struct SystemdLoginRecord {
pub user: String,
pub session_id: String,
pub seat_or_tty: String,
pub raw_device: String,
pub host: String,
pub login_time: SystemTime,
pub pid: u32,
pub session_leader_pid: u32,
pub record_type: SystemdRecordType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SystemdRecordType {
UserProcess = 7, LoginProcess = 6, BootTime = 2, }
impl SystemdLoginRecord {
pub fn is_user_process(&self) -> bool {
!self.user.is_empty() && self.record_type == SystemdRecordType::UserProcess
}
pub fn login_time_offset(&self) -> utmpx::time::OffsetDateTime {
let duration = self
.login_time
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let ts_nanos: i128 = (duration.as_nanos()).try_into().unwrap_or(0);
let local_offset = utmpx::time::OffsetDateTime::now_local()
.map_or_else(|_| utmpx::time::UtcOffset::UTC, |v| v.offset());
utmpx::time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos)
.unwrap_or_else(|_| {
utmpx::time::OffsetDateTime::now_local()
.unwrap_or_else(|_| utmpx::time::OffsetDateTime::now_utc())
})
.to_offset(local_offset)
}
}
pub fn read_login_records() -> UResult<Vec<SystemdLoginRecord>> {
let mut records = Vec::new();
if let Ok(boot_time) = login::get_boot_time() {
let boot_record = SystemdLoginRecord {
user: "reboot".to_string(),
session_id: "boot".to_string(),
seat_or_tty: "~".to_string(), raw_device: String::new(),
host: String::new(),
login_time: boot_time,
pid: 0,
session_leader_pid: 0,
record_type: SystemdRecordType::BootTime,
};
records.push(boot_record);
}
let mut sessions = login::get_sessions()
.map_err(|e| USimpleError::new(1, format!("Failed to get systemd sessions: {e}")))?;
sessions.sort();
sessions.reverse();
for session_id in sessions {
let Ok(uid) = login::get_session_uid(&session_id) else {
continue;
};
let user = unsafe {
let mut passwd = MaybeUninit::<libc::passwd>::uninit();
let buf_size = {
let size = libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX);
if size == -1 {
16384 } else {
size as usize
}
};
let mut buf = vec![0u8; buf_size];
let mut result: *mut libc::passwd = std::ptr::null_mut();
let ret = libc::getpwuid_r(
uid,
passwd.as_mut_ptr(),
buf.as_mut_ptr() as *mut libc::c_char,
buf.len(),
&mut result,
);
if ret == 0 && !result.is_null() {
let passwd = passwd.assume_init();
CStr::from_ptr(passwd.pw_name)
.to_string_lossy()
.into_owned()
} else {
format!("{uid}") }
};
let start_time = login::get_session_start_time(&session_id)
.map(|usec| UNIX_EPOCH + std::time::Duration::from_micros(usec))
.unwrap_or(UNIX_EPOCH);
let mut tty = login::get_session_tty(&session_id)
.ok()
.flatten()
.unwrap_or_default();
let mut seat = login::get_session_seat(&session_id)
.ok()
.flatten()
.unwrap_or_default();
if tty.starts_with('?') {
tty = tty[1..].to_string();
}
if seat.starts_with('?') {
seat = seat[1..].to_string();
}
let remote_host = login::get_session_remote_host(&session_id)
.ok()
.flatten()
.unwrap_or_default();
let display = login::get_session_display(&session_id)
.ok()
.flatten()
.unwrap_or_default();
let _session_type = login::get_session_type(&session_id)
.ok()
.flatten()
.unwrap_or_default();
let host = if remote_host.is_empty() {
display.clone()
} else {
remote_host
};
if tty.is_empty() && seat.is_empty() && display.is_empty() {
continue;
}
let create_record = |seat_or_tty: String,
raw_device: String,
user: String,
session_id: String,
host: String| {
SystemdLoginRecord {
user,
session_id,
seat_or_tty,
raw_device,
host,
login_time: start_time,
pid: 0, session_leader_pid: 0,
record_type: SystemdRecordType::UserProcess,
}
};
if !seat.is_empty() && !tty.is_empty() {
let seat_formatted = format!("?{seat}");
records.push(create_record(
seat_formatted,
seat,
user.clone(),
session_id.clone(),
host.clone(),
));
let tty_formatted = if tty.starts_with("tty") {
format!("*{tty}")
} else {
tty.clone()
};
records.push(create_record(tty_formatted, tty, user, session_id, host)); } else if !seat.is_empty() {
let seat_formatted = format!("?{seat}");
records.push(create_record(seat_formatted, seat, user, session_id, host));
} else if !tty.is_empty() {
let tty_formatted = if tty.starts_with("tty") {
format!("*{tty}")
} else {
tty.clone()
};
records.push(create_record(tty_formatted, tty, user, session_id, host));
} else if !display.is_empty() {
records.push(create_record(
display,
String::new(),
user,
session_id,
host,
));
}
}
Ok(records)
}
pub struct SystemdUtmpxCompat {
record: SystemdLoginRecord,
}
impl SystemdUtmpxCompat {
pub fn new(record: SystemdLoginRecord) -> Self {
Self { record }
}
pub fn record_type(&self) -> i16 {
self.record.record_type as i16
}
pub fn pid(&self) -> i32 {
self.record.pid as i32
}
pub fn terminal_suffix(&self) -> String {
self.record.session_id.clone()
}
pub fn user(&self) -> String {
self.record.user.clone()
}
pub fn host(&self) -> String {
self.record.host.clone()
}
pub fn tty_device(&self) -> String {
if self.record.raw_device.is_empty() {
self.record.seat_or_tty.clone()
} else {
self.record.raw_device.clone()
}
}
pub fn login_time(&self) -> utmpx::time::OffsetDateTime {
self.record.login_time_offset()
}
pub fn exit_status(&self) -> (i16, i16) {
(0, 0) }
pub fn is_user_process(&self) -> bool {
self.record.is_user_process()
}
pub fn canon_host(&self) -> std::io::Result<String> {
Ok(self.record.host.clone())
}
}
pub struct SystemdUtmpxIter {
records: Vec<SystemdLoginRecord>,
current_index: usize,
}
impl SystemdUtmpxIter {
pub fn new() -> UResult<Self> {
let records = read_login_records()?;
Ok(Self {
records,
current_index: 0,
})
}
pub fn empty() -> Self {
Self {
records: Vec::new(),
current_index: 0,
}
}
pub fn next_record(&mut self) -> Option<SystemdUtmpxCompat> {
if self.current_index >= self.records.len() {
return None;
}
let record = self.records[self.current_index].clone();
self.current_index += 1;
Some(SystemdUtmpxCompat::new(record))
}
pub fn get_all_records(&self) -> Vec<SystemdUtmpxCompat> {
self.records
.iter()
.cloned()
.map(SystemdUtmpxCompat::new)
.collect()
}
pub fn reset(&mut self) {
self.current_index = 0;
}
pub fn len(&self) -> usize {
self.records.len()
}
pub fn is_empty(&self) -> bool {
self.records.is_empty()
}
}
impl Iterator for SystemdUtmpxIter {
type Item = SystemdUtmpxCompat;
fn next(&mut self) -> Option<Self::Item> {
self.next_record()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_iterator() {
let mut iter = SystemdUtmpxIter::empty();
assert_eq!(iter.len(), 0);
assert!(iter.is_empty());
assert!(iter.next().is_none());
assert!(iter.next_record().is_none());
}
#[test]
fn test_iterator_with_mock_data() {
let mock_records = vec![
SystemdLoginRecord {
session_id: "session1".to_string(),
user: "user1".to_string(),
seat_or_tty: "tty1".to_string(),
raw_device: "tty1".to_string(),
host: "host1".to_string(),
login_time: std::time::UNIX_EPOCH,
pid: 1234,
session_leader_pid: 1234,
record_type: SystemdRecordType::UserProcess,
},
SystemdLoginRecord {
session_id: "session2".to_string(),
user: "user2".to_string(),
seat_or_tty: "pts/0".to_string(),
raw_device: "pts/0".to_string(),
host: "host2".to_string(),
login_time: std::time::UNIX_EPOCH,
pid: 5678,
session_leader_pid: 5678,
record_type: SystemdRecordType::UserProcess,
},
];
let mut iter = SystemdUtmpxIter {
records: mock_records,
current_index: 0,
};
assert_eq!(iter.len(), 2);
assert!(!iter.is_empty());
let first = iter.next();
assert!(first.is_some());
let second = iter.next();
assert!(second.is_some());
let third = iter.next();
assert!(third.is_none());
assert!(iter.next().is_none());
}
#[test]
fn test_get_all_records() {
let mock_records = vec![SystemdLoginRecord {
session_id: "session1".to_string(),
user: "user1".to_string(),
seat_or_tty: "tty1".to_string(),
raw_device: "tty1".to_string(),
host: "host1".to_string(),
login_time: std::time::UNIX_EPOCH,
pid: 1234,
session_leader_pid: 1234,
record_type: SystemdRecordType::UserProcess,
}];
let iter = SystemdUtmpxIter {
records: mock_records,
current_index: 0,
};
let all_records = iter.get_all_records();
assert_eq!(all_records.len(), 1);
}
#[test]
fn test_systemd_record_conversion() {
let record = SystemdLoginRecord {
session_id: "c1".to_string(),
user: "testuser".to_string(),
seat_or_tty: "seat0".to_string(),
raw_device: "seat0".to_string(),
host: "localhost".to_string(),
login_time: std::time::UNIX_EPOCH + std::time::Duration::from_secs(1000),
pid: 9999,
session_leader_pid: 9999,
record_type: SystemdRecordType::UserProcess,
};
let compat = SystemdUtmpxCompat::new(record);
assert_eq!(compat.user(), "testuser");
assert_eq!(compat.tty_device().as_str(), "seat0");
assert_eq!(compat.host(), "localhost");
}
}