Add last Utility (#65)

This commit is contained in:
Andy
2024-09-19 23:43:18 +08:00
committed by GitHub
parent 201b2fe829
commit 49fc7ff3ca
12 changed files with 1037 additions and 80 deletions

12
src/uu/last/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "uu_last"
version = "0.0.1"
edition = "2021"
[lib]
path = "src/last.rs"
[dependencies]
uucore = { workspace = true, features = ["utmpx"] }
clap = { workspace = true}
dns-lookup = { workspace = true }

8
src/uu/last/last.md Normal file
View File

@@ -0,0 +1,8 @@
# last
```
Usage:
[options] [<username>...] [<tty>...]
```
Show a listing of last logged in users.

94
src/uu/last/src/last.rs Normal file
View File

@@ -0,0 +1,94 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use clap::{crate_version, Arg, ArgAction, Command};
use uucore::{format_usage, help_about, help_usage};
mod platform;
mod options {
pub const SYSTEM: &str = "system";
pub const HOSTLAST: &str = "hostlast";
pub const NO_HOST: &str = "nohostname";
pub const LIMIT: &str = "limit";
pub const DNS: &str = "dns";
pub const TIME_FORMAT: &str = "time-format";
pub const USER_TTY: &str = "username";
pub const FILE: &str = "file";
}
const ABOUT: &str = help_about!("last.md");
const USAGE: &str = help_usage!("last.md");
#[uucore::main]
use platform::uumain;
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(crate_version!())
.about(ABOUT)
.override_usage(format_usage(USAGE))
.infer_long_args(true)
.arg(
Arg::new(options::FILE)
.short('f')
.long("file")
.action(ArgAction::Set)
.default_value("/var/log/wtmp")
.help("use a specific file instead of /var/log/wtmp")
.required(false),
)
.arg(
Arg::new(options::SYSTEM)
.short('x')
.long(options::SYSTEM)
.action(ArgAction::SetTrue)
.required(false)
.help("display system shutdown entries and run level changes"),
)
.arg(
Arg::new(options::DNS)
.short('d')
.long(options::DNS)
.action(ArgAction::SetTrue)
.required(false)
.help("translate the IP number back into a hostname"),
)
.arg(
Arg::new(options::HOSTLAST)
.short('a')
.long(options::HOSTLAST)
.action(ArgAction::SetTrue)
.required(false)
.help("display hostnames in the last column"),
)
.arg(
Arg::new(options::NO_HOST)
.short('R')
.long(options::NO_HOST)
.action(ArgAction::SetTrue)
.required(false)
.help("don't display the hostname field"),
)
.arg(
Arg::new(options::LIMIT)
.short('n')
.long(options::LIMIT)
.action(ArgAction::Set)
.required(false)
.help("how many lines to show")
.value_parser(clap::value_parser!(i32))
.allow_negative_numbers(true),
)
.arg(
Arg::new(options::TIME_FORMAT)
.long(options::TIME_FORMAT)
.action(ArgAction::Set)
.required(false)
.help("show timestamps in the specified <format>: notime|short|full|iso")
.default_value("short"),
)
.arg(Arg::new(options::USER_TTY).action(ArgAction::Append))
}

1
src/uu/last/src/main.rs Normal file
View File

@@ -0,0 +1 @@
uucore::bin!(uu_last);

View File

@@ -0,0 +1,19 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
#[cfg(unix)]
mod unix;
#[cfg(unix)]
pub use self::unix::*;
#[cfg(target_os = "openbsd")]
mod openbsd;
#[cfg(target_os = "openbsd")]
pub use self::openbsd::*;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
pub use self::windows::*;

View File

@@ -0,0 +1,17 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// Specific implementation for OpenBSD: tool unsupported (utmpx not supported)
use crate::uu_app;
use uucore::error::UResult;
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let _matches = uu_app().try_get_matches_from(args)?;
println!("unsupported command on OpenBSD");
Ok(())
}

View File

@@ -0,0 +1,535 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use crate::options;
use crate::uu_app;
use uucore::error::UIoError;
use uucore::error::UResult;
use uucore::error::USimpleError;
use uucore::utmpx::time::OffsetDateTime;
use uucore::utmpx::{time, Utmpx};
use std::fmt::Write;
use std::fs;
use std::io;
use std::net::Ipv4Addr;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Duration;
fn get_long_usage() -> String {
format!(
"If FILE is not specified, use {}. /var/log/wtmp as FILE is common.",
WTMP_PATH,
)
}
const WTMP_PATH: &str = "/var/log/wtmp";
static TIME_FORMAT_STR: [&str; 4] = ["notime", "short", "full", "iso"];
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let system = matches.get_flag(options::SYSTEM);
let dns = matches.get_flag(options::DNS);
let hostlast = matches.get_flag(options::HOSTLAST);
let nohost = matches.get_flag(options::NO_HOST);
let limit: i32 = if let Some(num) = matches.get_one::<i32>(options::LIMIT) {
*num
} else {
0 // Original implementation has 0 mean no limit (print all values)
};
let time_format = if let Some(format) = matches.get_one::<String>(options::TIME_FORMAT) {
let format_str = format.as_str().trim();
if TIME_FORMAT_STR.contains(&format_str) {
Ok(format.to_string())
} else {
Err(USimpleError::new(
0,
format!("unknown time format: {format}"),
))
}
} else {
Ok("short".to_string())
}?;
let file: String = if let Some(files) = matches.get_one::<String>(options::FILE) {
files.to_string()
} else {
WTMP_PATH.to_string()
};
let user: Option<Vec<String>> =
if let Some(users) = matches.get_many::<String>(options::USER_TTY) {
users
.map(|v| {
if is_numeric(v) {
Some(format!("tty{v}"))
} else {
Some(v.to_owned())
}
})
.collect()
} else {
None
};
let mut last = Last {
last_reboot_ut: None,
last_shutdown_ut: None,
last_dead_ut: vec![],
system,
dns,
host_last: hostlast,
no_host: nohost,
limit,
file: file.to_string(),
users: user,
time_format,
};
last.exec()
}
const RUN_LEVEL_STR: &str = "runlevel";
const REBOOT_STR: &str = "reboot";
const SHUTDOWN_STR: &str = "shutdown";
struct Last {
last_reboot_ut: Option<Utmpx>,
last_shutdown_ut: Option<Utmpx>,
last_dead_ut: Vec<Utmpx>,
system: bool,
dns: bool,
host_last: bool,
no_host: bool,
file: String,
time_format: String,
users: Option<Vec<String>>,
limit: i32,
}
fn is_numeric(s: &str) -> bool {
s.chars().all(|c| c.is_numeric())
}
#[inline]
fn calculate_time_delta(
curr_datetime: &OffsetDateTime,
last_datetime: &OffsetDateTime,
) -> time::Duration {
let curr_duration = time::Duration::new(
curr_datetime.unix_timestamp(),
curr_datetime.nanosecond().try_into().unwrap_or_default(), // nanosecond value is always a value between 0 and 1.000.000.000, shouldn't panic
);
let last_duration = time::Duration::new(
last_datetime.unix_timestamp(),
last_datetime.nanosecond().try_into().unwrap_or_default(), // nanosecond value is always a value between 0 and 1.000.000.000, shouldn't panic
);
last_duration - curr_duration
}
#[inline]
fn duration_string(duration: time::Duration) -> String {
let mut seconds = duration.whole_seconds();
let days = seconds / 86400;
seconds -= days * 86400;
let hours = seconds / 3600;
seconds -= hours * 3600;
let minutes = seconds / 60;
if days > 0 {
format!("({}+{:0>2}:{:0>2})", days, hours, minutes)
} else {
format!("({:0>2}:{:0>2})", hours, minutes)
}
}
fn find_dns_name(ut: &Utmpx) -> String {
let default = Ipv4Addr::new(0, 0, 0, 0);
let ip = std::net::IpAddr::V4(Ipv4Addr::from_str(&ut.host()).unwrap_or(default));
if ip.to_string().trim() == "0.0.0.0" {
ip.to_string()
} else {
dns_lookup::lookup_addr(&ip).unwrap_or_default()
}
}
impl Last {
const TIME_FULL_FMT: &'static str = "[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]:[second] [year]";
const END_TIME_SHORT_FMT: &'static str = "[hour]:[minute]";
const START_TIME_SHORT_FMT: &'static str =
"[weekday repr:short] [month repr:short] [day padding:space] [hour]:[minute]";
const TIME_ISO_FMT: &'static str =
"[year]-[month]-[day]T[hour]:[minute]:[second]+[offset_hour]:[offset_minute]";
#[allow(clippy::cognitive_complexity)]
fn exec(&mut self) -> UResult<()> {
let mut ut_stack: Vec<Utmpx> = vec![];
// For 'last' output, older output needs to be printed last (FILO), as
// UtmpxIter does not implement Rev trait. A better implementation
// might include implementing UtmpxIter as doubly linked
Utmpx::iter_all_records_from(&self.file).for_each(|ut| ut_stack.push(ut));
let mut counter = 0;
let mut first_ut_time = None;
while let Some(ut) = ut_stack.pop() {
if ut_stack.is_empty() {
// By the end of loop we will have the earliest time
// (This avoids getting into issues with the compiler)
let first_login_time = ut.login_time();
first_ut_time = Some(self.utmp_file_time(
first_login_time.unix_timestamp(),
first_login_time.nanosecond().into(),
));
}
if counter >= self.limit && self.limit > 0 {
break;
}
if ut.is_user_process() {
let mut dead_proc: Option<Utmpx> = None;
if let Some(pos) = self
.last_dead_ut
.iter()
.position(|dead_ut| ut.tty_device() == dead_ut.tty_device())
{
dead_proc = Some(self.last_dead_ut.swap_remove(pos));
}
if self.print_user(&ut, dead_proc.as_ref()) {
counter += 1;
}
} else if ut.user() == RUN_LEVEL_STR {
if self.print_runlevel(&ut) {
counter += 1;
}
} else if ut.user() == SHUTDOWN_STR {
if self.print_shutdown(&ut) {
counter += 1;
}
self.last_shutdown_ut = Some(ut);
} else if ut.user() == REBOOT_STR {
if self.print_reboot(&ut) {
counter += 1;
}
self.last_reboot_ut = Some(ut);
} else if ut.user() == "" {
// Dead process end date
self.last_dead_ut.push(ut);
}
}
let path = std::path::absolute(&self.file)?;
let path_str = path
.file_name()
.ok_or_else(|| {
if path.is_dir() {
UIoError::new(io::ErrorKind::InvalidData, "Is a directory")
} else {
UIoError::new(io::ErrorKind::Unsupported, "Undefined")
}
})?
.to_str()
.ok_or(UIoError::new(
io::ErrorKind::InvalidData,
"invalid character data (not UTF-8)",
))?;
if let Some(file_time) = first_ut_time {
println!("\n{} begins {}", path_str, file_time);
} else {
let secs = fs::metadata(&self.file)?.ctime();
let nsecs = fs::metadata(&self.file)?.ctime_nsec() as u64;
let file_time = self.utmp_file_time(secs, nsecs);
println!("\n{} begins {}", path_str, file_time);
}
Ok(())
}
#[inline]
fn utmp_file_time(&self, secs: i64, nsecs: u64) -> String {
let description = match self.time_format.as_str() {
"short" | "full" => Self::TIME_FULL_FMT,
"iso" => Self::TIME_ISO_FMT,
_ => return "".to_string(),
};
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse(description).unwrap_or_default();
let time = time::OffsetDateTime::from_unix_timestamp(secs)
.unwrap_or(time::OffsetDateTime::UNIX_EPOCH)
+ Duration::from_nanos(nsecs);
let offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC);
let offset_secs: u64 = offset.whole_seconds() as u64;
// Adding back the time to the offset so that offset_time is correct.
let offset_time = time.replace_offset(offset) + Duration::from_secs(offset_secs);
offset_time.format(&time_format).unwrap_or_default()
}
#[inline]
fn time_string(&self, ut: &Utmpx) -> String {
let description = match self.time_format.as_str() {
"short" => Self::START_TIME_SHORT_FMT,
"full" => Self::TIME_FULL_FMT,
"iso" => Self::TIME_ISO_FMT,
_ => return "".to_string(),
};
// "%b %e %H:%M"
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse(description).unwrap_or_default();
ut.login_time().format(&time_format).unwrap_or_default()
}
#[inline]
fn end_time_string(&self, user_process_str: Option<&str>, end_ut: &OffsetDateTime) -> String {
match user_process_str {
Some(val) => val.to_string(),
_ => {
let description = match self.time_format.as_str() {
"short" => format!("- {}", Self::END_TIME_SHORT_FMT),
"full" => format!("- {}", Self::TIME_FULL_FMT),
"iso" => format!("- {}", Self::TIME_ISO_FMT),
_ => return "".to_string(),
};
// "%H:%M"
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse(&description).unwrap_or_default();
end_ut.format(&time_format).unwrap_or_default()
}
}
}
#[inline]
fn end_state_string(&self, ut: &Utmpx, dead_ut: Option<&Utmpx>) -> (String, String) {
// This function takes a considerable amount of CPU cycles to complete;
// root cause seems to be the ut.login_time function, which reads a
// file to determine local offset for UTC. Perhaps this function
// should be updated to save that UTC offset for subsequent calls
let mut proc_status: Option<&str> = None;
let curr_datetime = ut.login_time();
if let Some(dead) = dead_ut {
let dead_datetime = dead.login_time();
let time_delta = duration_string(calculate_time_delta(&curr_datetime, &dead_datetime));
return (
self.end_time_string(proc_status, &dead_datetime),
time_delta.to_string(),
);
}
let reboot_datetime: Option<OffsetDateTime>;
let shutdown_datetime: Option<OffsetDateTime>;
if let Some(reboot) = &self.last_reboot_ut {
reboot_datetime = Some(reboot.login_time());
} else {
reboot_datetime = None;
}
if let Some(shutdown) = &self.last_shutdown_ut {
shutdown_datetime = Some(shutdown.login_time());
} else {
shutdown_datetime = None;
}
if shutdown_datetime.is_none() {
if ut.is_user_process() {
// If a reboot has occurred since the user logged in, but not shutdown is recorded
// then a crash must have occurred.
if reboot_datetime.is_some() && reboot_datetime.unwrap() > ut.login_time() {
("- crash".to_string(), "".to_string())
} else {
(" still logged in".to_string(), "".to_string())
}
} else {
(" still running".to_string(), "".to_string())
}
} else {
let shutdown = shutdown_datetime
.unwrap_or_else(|| time::OffsetDateTime::from_unix_timestamp(0).unwrap());
let time_delta = duration_string(calculate_time_delta(&curr_datetime, &shutdown));
if ut.is_user_process() {
proc_status = Some("- down");
}
(
self.end_time_string(proc_status, &shutdown),
time_delta.to_string(),
)
}
}
#[inline]
fn print_runlevel(&self, ut: &Utmpx) -> bool {
if let Some(users) = &self.users {
if !users
.iter()
.any(|val| val.as_str().trim() == ut.user().trim())
{
return false;
}
}
if self.system {
let curr = (ut.pid() % 256) as u8 as char;
let runlvline = format!("(to lvl {curr})");
let (end_date, delta) = self.end_state_string(ut, None);
let host = if self.dns {
find_dns_name(ut)
} else {
ut.host()
};
self.print_line(
RUN_LEVEL_STR,
&runlvline,
&self.time_string(ut),
&host,
&end_date,
&delta,
);
true
} else {
false
}
}
#[inline]
fn print_shutdown(&self, ut: &Utmpx) -> bool {
if let Some(users) = &self.users {
if !users.iter().any(|val| {
val.as_str().trim() == "system down" || val.as_str().trim() == ut.user().trim()
}) {
return false;
}
}
let host = if self.dns {
find_dns_name(ut)
} else {
ut.host()
};
if self.system {
let (end_date, delta) = self.end_state_string(ut, None);
self.print_line(
SHUTDOWN_STR,
"system down",
&self.time_string(ut),
&host,
&end_date,
&delta,
);
true
} else {
false
}
}
#[inline]
fn print_reboot(&self, ut: &Utmpx) -> bool {
if let Some(users) = &self.users {
if !users.iter().any(|val| {
val.as_str().trim() == ut.user().trim() || val.as_str().trim() == "system boot"
}) {
return false;
}
}
let (end_date, delta) = self.end_state_string(ut, None);
let host = if self.dns {
find_dns_name(ut)
} else {
ut.host()
};
self.print_line(
REBOOT_STR,
"system boot",
&self.time_string(ut),
&host,
&end_date,
&delta,
);
true
}
#[inline]
fn print_user(&self, ut: &Utmpx, dead_ut: Option<&Utmpx>) -> bool {
if let Some(users) = &self.users {
if !users.iter().any(|val| {
val.as_str().trim() == ut.tty_device().as_str().trim()
|| val.as_str().trim() == ut.user().trim()
}) {
return false;
}
}
let mut p = PathBuf::from("/dev");
p.push(ut.tty_device().as_str());
let host = if self.dns {
find_dns_name(ut)
} else {
ut.host()
};
let (end_date, delta) = self.end_state_string(ut, dead_ut);
self.print_line(
ut.user().as_ref(),
ut.tty_device().as_ref(),
self.time_string(ut).as_str(),
&host,
&end_date,
&delta,
);
true
}
#[inline]
#[allow(clippy::too_many_arguments)]
fn print_line(
&self,
user: &str,
line: &str,
time: &str,
host: &str,
end_time: &str,
delta: &str,
) {
let mut buf = String::with_capacity(64);
let host_to_print = host.get(0..16).unwrap_or(host);
write!(buf, "{user:<8}").unwrap_or_default();
write!(buf, " {line:<12}").unwrap_or_default();
if !self.host_last && !self.no_host {
write!(buf, " {host_to_print:<16}").unwrap_or_default();
}
let time_size = 3 + 2 + 2 + 1 + 2;
if self.host_last && !self.no_host && self.time_format != "notime" {
write!(buf, " {time:<time_size$}").unwrap_or_default();
write!(buf, " {end_time:<8}").unwrap_or_default();
write!(buf, " {host_to_print}").unwrap_or_default();
} else if self.time_format != "notime" {
write!(buf, " {time:<time_size$}").unwrap_or_default();
write!(buf, " {end_time:<8}").unwrap_or_default();
}
write!(buf, " {delta:^6}").unwrap_or_default();
println!("{}", buf.trim_end());
}
}

View File

@@ -0,0 +1,17 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// Specific implementation for Windows: tool unsupported (utmpx not supported)
use crate::uu_app;
use uucore::error::UResult;
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let _matches = uu_app().try_get_matches_from(args)?;
println!("unsupported command on Windows");
Ok(())
}