1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-30 04:27:45 +00:00

Merge pull request #5620 from lcheylus/openbsd-utmpx

Support unix feature on OpenBSD (utmpx not supported)
This commit is contained in:
Sylvestre Ledru 2023-12-16 18:15:11 +01:00 committed by GitHub
commit 35ae43e71f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1083 additions and 895 deletions

View file

@ -251,7 +251,7 @@ fn set_main_group(group: &str) -> UResult<()> {
Ok(())
}
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
#[cfg(any(target_vendor = "apple", target_os = "freebsd", target_os = "openbsd"))]
fn set_groups(groups: &[libc::gid_t]) -> libc::c_int {
unsafe { setgroups(groups.len() as libc::c_int, groups.as_ptr()) }
}

View file

@ -507,7 +507,7 @@ fn pline(possible_uid: Option<uid_t>) {
);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(any(target_os = "linux", target_os = "android", target_os = "openbsd"))]
fn pline(possible_uid: Option<uid_t>) {
let uid = possible_uid.unwrap_or_else(getuid);
let pw = Passwd::locate(uid).unwrap();
@ -524,10 +524,10 @@ fn pline(possible_uid: Option<uid_t>) {
);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
#[cfg(any(target_os = "linux", target_os = "android", target_os = "openbsd"))]
fn auditid() {}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "openbsd")))]
fn auditid() {
use std::mem::MaybeUninit;
@ -624,7 +624,7 @@ fn id_print(state: &State, groups: &[u32]) {
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "openbsd")))]
mod audit {
use super::libc::{c_int, c_uint, dev_t, pid_t, uid_t};

View file

@ -198,7 +198,12 @@ extern "C" {
fn _vprocmgr_detach_from_console(flags: u32) -> *const libc::c_int;
}
#[cfg(any(target_os = "linux", target_os = "android", target_os = "freebsd"))]
#[cfg(any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd"
))]
unsafe fn _vprocmgr_detach_from_console(_: u32) -> *const libc::c_int {
std::ptr::null()
}

View file

@ -5,21 +5,11 @@
// spell-checker:ignore (ToDO) BUFSIZE gecos fullname, mesg iobuf
use uucore::entries::{Locate, Passwd};
use uucore::error::{FromIo, UResult};
use uucore::libc::S_IWGRP;
use uucore::utmpx::{self, time, Utmpx};
use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;
use std::os::unix::fs::MetadataExt;
use clap::{crate_version, Arg, ArgAction, Command};
use std::path::PathBuf;
use uucore::{format_usage, help_about, help_usage};
mod platform;
const ABOUT: &str = help_about!("pinky.md");
const USAGE: &str = help_usage!("pinky.md");
@ -37,86 +27,8 @@ mod options {
pub const HELP: &str = "help";
}
fn get_long_usage() -> String {
format!(
"A lightweight 'finger' program; print user information.\n\
The utmp file will be {}.",
utmpx::DEFAULT_FILE
)
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let users: Vec<String> = matches
.get_many::<String>(options::USER)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
// If true, display the hours:minutes since each user has touched
// the keyboard, or blank if within the last minute, or days followed
// by a 'd' if not within the last day.
let mut include_idle = true;
// If true, display a line at the top describing each field.
let include_heading = !matches.get_flag(options::OMIT_HEADINGS);
// if true, display the user's full name from pw_gecos.
let mut include_fullname = true;
// if true, display the user's ~/.project file when doing long format.
let include_project = !matches.get_flag(options::OMIT_PROJECT_FILE);
// if true, display the user's ~/.plan file when doing long format.
let include_plan = !matches.get_flag(options::OMIT_PLAN_FILE);
// if true, display the user's home directory and shell
// when doing long format.
let include_home_and_shell = !matches.get_flag(options::OMIT_HOME_DIR);
// if true, use the "short" output format.
let do_short_format = !matches.get_flag(options::LONG_FORMAT);
/* if true, display the ut_host field. */
let mut include_where = true;
if matches.get_flag(options::OMIT_NAME) {
include_fullname = false;
}
if matches.get_flag(options::OMIT_NAME_HOST) {
include_fullname = false;
include_where = false;
}
if matches.get_flag(options::OMIT_NAME_HOST_TIME) {
include_fullname = false;
include_idle = false;
include_where = false;
}
let pk = Pinky {
include_idle,
include_heading,
include_fullname,
include_project,
include_plan,
include_home_and_shell,
include_where,
names: users,
};
if do_short_format {
match pk.short_pinky() {
Ok(_) => Ok(()),
Err(e) => Err(e.map_err_context(String::new)),
}
} else {
pk.long_pinky();
Ok(())
}
}
use platform::uumain;
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
@ -195,17 +107,6 @@ pub fn uu_app() -> Command {
)
}
struct Pinky {
include_idle: bool,
include_heading: bool,
include_fullname: bool,
include_project: bool,
include_plan: bool,
include_where: bool,
include_home_and_shell: bool,
names: Vec<String>,
}
pub trait Capitalize {
fn capitalize(&self) -> String;
}
@ -223,180 +124,3 @@ impl Capitalize for str {
})
}
}
fn idle_string(when: i64) -> String {
thread_local! {
static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap();
}
NOW.with(|n| {
let duration = n.unix_timestamp() - when;
if duration < 60 {
// less than 1min
" ".to_owned()
} else if duration < 24 * 3600 {
// less than 1day
let hours = duration / (60 * 60);
let minutes = (duration % (60 * 60)) / 60;
format!("{hours:02}:{minutes:02}")
} else {
// more than 1day
let days = duration / (24 * 3600);
format!("{days}d")
}
})
}
fn time_string(ut: &Utmpx) -> String {
// "%b %e %H:%M"
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]")
.unwrap();
ut.login_time().format(&time_format).unwrap() // LC_ALL=C
}
fn gecos_to_fullname(pw: &Passwd) -> Option<String> {
let mut gecos = if let Some(gecos) = &pw.user_info {
gecos.clone()
} else {
return None;
};
if let Some(n) = gecos.find(',') {
gecos.truncate(n);
}
Some(gecos.replace('&', &pw.name.capitalize()))
}
impl Pinky {
fn print_entry(&self, ut: &Utmpx) -> std::io::Result<()> {
let mut pts_path = PathBuf::from("/dev");
pts_path.push(ut.tty_device().as_str());
let mesg;
let last_change;
match pts_path.metadata() {
#[allow(clippy::unnecessary_cast)]
Ok(meta) => {
mesg = if meta.mode() & S_IWGRP as u32 == 0 {
'*'
} else {
' '
};
last_change = meta.atime();
}
_ => {
mesg = '?';
last_change = 0;
}
}
print!("{1:<8.0$}", utmpx::UT_NAMESIZE, ut.user());
if self.include_fullname {
let fullname = if let Ok(pw) = Passwd::locate(ut.user().as_ref()) {
gecos_to_fullname(&pw)
} else {
None
};
if let Some(fullname) = fullname {
print!(" {fullname:<19.19}");
} else {
print!(" {:19}", " ???");
}
}
print!(" {}{:<8.*}", mesg, utmpx::UT_LINESIZE, ut.tty_device());
if self.include_idle {
if last_change == 0 {
print!(" {:<6}", "?????");
} else {
print!(" {:<6}", idle_string(last_change));
}
}
print!(" {}", time_string(ut));
let mut s = ut.host();
if self.include_where && !s.is_empty() {
s = ut.canon_host()?;
print!(" {s}");
}
println!();
Ok(())
}
fn print_heading(&self) {
print!("{:<8}", "Login");
if self.include_fullname {
print!(" {:<19}", "Name");
}
print!(" {:<9}", " TTY");
if self.include_idle {
print!(" {:<6}", "Idle");
}
print!(" {:<16}", "When");
if self.include_where {
print!(" Where");
}
println!();
}
fn short_pinky(&self) -> std::io::Result<()> {
if self.include_heading {
self.print_heading();
}
for ut in Utmpx::iter_all_records() {
if ut.is_user_process()
&& (self.names.is_empty() || self.names.iter().any(|n| n.as_str() == ut.user()))
{
self.print_entry(&ut)?;
}
}
Ok(())
}
fn long_pinky(&self) {
for u in &self.names {
print!("Login name: {u:<28}In real life: ");
if let Ok(pw) = Passwd::locate(u.as_str()) {
let fullname = gecos_to_fullname(&pw).unwrap_or_default();
let user_dir = pw.user_dir.unwrap_or_default();
let user_shell = pw.user_shell.unwrap_or_default();
println!(" {fullname}");
if self.include_home_and_shell {
print!("Directory: {user_dir:<29}");
println!("Shell: {user_shell}");
}
if self.include_project {
let mut p = PathBuf::from(&user_dir);
p.push(".project");
if let Ok(f) = File::open(p) {
print!("Project: ");
read_to_console(f);
}
}
if self.include_plan {
let mut p = PathBuf::from(&user_dir);
p.push(".plan");
if let Ok(f) = File::open(p) {
println!("Plan:");
read_to_console(f);
}
}
println!();
} else {
println!(" ???");
}
}
}
}
fn read_to_console<F: Read>(f: F) {
let mut reader = BufReader::new(f);
let mut iobuf = Vec::new();
if reader.read_to_end(&mut iobuf).is_ok() {
print!("{}", String::from_utf8_lossy(&iobuf));
}
}

View file

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

View file

@ -0,0 +1,17 @@
// This file is part of the uutils coreutils 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,291 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) BUFSIZE gecos fullname, mesg iobuf
use crate::options;
use crate::uu_app;
use crate::Capitalize;
use uucore::entries::{Locate, Passwd};
use uucore::error::{FromIo, UResult};
use uucore::libc::S_IWGRP;
use uucore::utmpx::{self, time, Utmpx};
use std::io::prelude::*;
use std::io::BufReader;
use std::fs::File;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
fn get_long_usage() -> String {
format!(
"A lightweight 'finger' program; print user information.\n\
The utmp file will be {}.",
utmpx::DEFAULT_FILE
)
}
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let users: Vec<String> = matches
.get_many::<String>(options::USER)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
// If true, display the hours:minutes since each user has touched
// the keyboard, or blank if within the last minute, or days followed
// by a 'd' if not within the last day.
let mut include_idle = true;
// If true, display a line at the top describing each field.
let include_heading = !matches.get_flag(options::OMIT_HEADINGS);
// if true, display the user's full name from pw_gecos.
let mut include_fullname = true;
// if true, display the user's ~/.project file when doing long format.
let include_project = !matches.get_flag(options::OMIT_PROJECT_FILE);
// if true, display the user's ~/.plan file when doing long format.
let include_plan = !matches.get_flag(options::OMIT_PLAN_FILE);
// if true, display the user's home directory and shell
// when doing long format.
let include_home_and_shell = !matches.get_flag(options::OMIT_HOME_DIR);
// if true, use the "short" output format.
let do_short_format = !matches.get_flag(options::LONG_FORMAT);
/* if true, display the ut_host field. */
let mut include_where = true;
if matches.get_flag(options::OMIT_NAME) {
include_fullname = false;
}
if matches.get_flag(options::OMIT_NAME_HOST) {
include_fullname = false;
include_where = false;
}
if matches.get_flag(options::OMIT_NAME_HOST_TIME) {
include_fullname = false;
include_idle = false;
include_where = false;
}
let pk = Pinky {
include_idle,
include_heading,
include_fullname,
include_project,
include_plan,
include_home_and_shell,
include_where,
names: users,
};
if do_short_format {
match pk.short_pinky() {
Ok(_) => Ok(()),
Err(e) => Err(e.map_err_context(String::new)),
}
} else {
pk.long_pinky();
Ok(())
}
}
struct Pinky {
include_idle: bool,
include_heading: bool,
include_fullname: bool,
include_project: bool,
include_plan: bool,
include_where: bool,
include_home_and_shell: bool,
names: Vec<String>,
}
fn idle_string(when: i64) -> String {
thread_local! {
static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap();
}
NOW.with(|n| {
let duration = n.unix_timestamp() - when;
if duration < 60 {
// less than 1min
" ".to_owned()
} else if duration < 24 * 3600 {
// less than 1day
let hours = duration / (60 * 60);
let minutes = (duration % (60 * 60)) / 60;
format!("{hours:02}:{minutes:02}")
} else {
// more than 1day
let days = duration / (24 * 3600);
format!("{days}d")
}
})
}
fn time_string(ut: &Utmpx) -> String {
// "%b %e %H:%M"
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]")
.unwrap();
ut.login_time().format(&time_format).unwrap() // LC_ALL=C
}
fn gecos_to_fullname(pw: &Passwd) -> Option<String> {
let mut gecos = if let Some(gecos) = &pw.user_info {
gecos.clone()
} else {
return None;
};
if let Some(n) = gecos.find(',') {
gecos.truncate(n);
}
Some(gecos.replace('&', &pw.name.capitalize()))
}
impl Pinky {
fn print_entry(&self, ut: &Utmpx) -> std::io::Result<()> {
let mut pts_path = PathBuf::from("/dev");
pts_path.push(ut.tty_device().as_str());
let mesg;
let last_change;
match pts_path.metadata() {
#[allow(clippy::unnecessary_cast)]
Ok(meta) => {
mesg = if meta.mode() & S_IWGRP as u32 == 0 {
'*'
} else {
' '
};
last_change = meta.atime();
}
_ => {
mesg = '?';
last_change = 0;
}
}
print!("{1:<8.0$}", utmpx::UT_NAMESIZE, ut.user());
if self.include_fullname {
let fullname = if let Ok(pw) = Passwd::locate(ut.user().as_ref()) {
gecos_to_fullname(&pw)
} else {
None
};
if let Some(fullname) = fullname {
print!(" {fullname:<19.19}");
} else {
print!(" {:19}", " ???");
}
}
print!(" {}{:<8.*}", mesg, utmpx::UT_LINESIZE, ut.tty_device());
if self.include_idle {
if last_change == 0 {
print!(" {:<6}", "?????");
} else {
print!(" {:<6}", idle_string(last_change));
}
}
print!(" {}", time_string(ut));
let mut s = ut.host();
if self.include_where && !s.is_empty() {
s = ut.canon_host()?;
print!(" {s}");
}
println!();
Ok(())
}
fn print_heading(&self) {
print!("{:<8}", "Login");
if self.include_fullname {
print!(" {:<19}", "Name");
}
print!(" {:<9}", " TTY");
if self.include_idle {
print!(" {:<6}", "Idle");
}
print!(" {:<16}", "When");
if self.include_where {
print!(" Where");
}
println!();
}
fn short_pinky(&self) -> std::io::Result<()> {
if self.include_heading {
self.print_heading();
}
for ut in Utmpx::iter_all_records() {
if ut.is_user_process()
&& (self.names.is_empty() || self.names.iter().any(|n| n.as_str() == ut.user()))
{
self.print_entry(&ut)?;
}
}
Ok(())
}
fn long_pinky(&self) {
for u in &self.names {
print!("Login name: {u:<28}In real life: ");
if let Ok(pw) = Passwd::locate(u.as_str()) {
let fullname = gecos_to_fullname(&pw).unwrap_or_default();
let user_dir = pw.user_dir.unwrap_or_default();
let user_shell = pw.user_shell.unwrap_or_default();
println!(" {fullname}");
if self.include_home_and_shell {
print!("Directory: {user_dir:<29}");
println!("Shell: {user_shell}");
}
if self.include_project {
let mut p = PathBuf::from(&user_dir);
p.push(".project");
if let Ok(f) = File::open(p) {
print!("Project: ");
read_to_console(f);
}
}
if self.include_plan {
let mut p = PathBuf::from(&user_dir);
p.push(".plan");
if let Ok(f) = File::open(p) {
println!("Plan:");
read_to_console(f);
}
}
println!();
} else {
println!(" ???");
}
}
}
}
fn read_to_console<F: Read>(f: F) {
let mut reader = BufReader::new(f);
let mut iobuf = Vec::new();
if reader.read_to_end(&mut iobuf).is_ok() {
print!("{}", String::from_utf8_lossy(&iobuf));
}
}

View file

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

View file

@ -0,0 +1,17 @@
// This file is part of the uutils coreutils 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,161 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) getloadavg upsecs updays nusers loadavg boottime uphours upmins
use crate::options;
use crate::uu_app;
use chrono::{Local, TimeZone, Utc};
use uucore::libc::time_t;
use uucore::error::{UResult, USimpleError};
#[cfg(unix)]
use uucore::libc::getloadavg;
#[cfg(windows)]
extern "C" {
fn GetTickCount() -> uucore::libc::uint32_t;
}
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let (boot_time, user_count) = process_utmpx();
let uptime = get_uptime(boot_time);
if uptime < 0 {
Err(USimpleError::new(1, "could not retrieve system uptime"))
} else {
if matches.get_flag(options::SINCE) {
let initial_date = Local
.timestamp_opt(Utc::now().timestamp() - uptime, 0)
.unwrap();
println!("{}", initial_date.format("%Y-%m-%d %H:%M:%S"));
return Ok(());
}
print_time();
let upsecs = uptime;
print_uptime(upsecs);
print_nusers(user_count);
print_loadavg();
Ok(())
}
}
#[cfg(unix)]
fn print_loadavg() {
use uucore::libc::c_double;
let mut avg: [c_double; 3] = [0.0; 3];
let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) };
if loads == -1 {
println!();
} else {
print!("load average: ");
for n in 0..loads {
print!(
"{:.2}{}",
avg[n as usize],
if n == loads - 1 { "\n" } else { ", " }
);
}
}
}
#[cfg(windows)]
fn print_loadavg() {
// XXX: currently this is a noop as Windows does not seem to have anything comparable to
// getloadavg()
}
#[cfg(unix)]
fn process_utmpx() -> (Option<time_t>, usize) {
use uucore::utmpx::*;
let mut nusers = 0;
let mut boot_time = None;
for line in Utmpx::iter_all_records() {
match line.record_type() {
USER_PROCESS => nusers += 1,
BOOT_TIME => {
let dt = line.login_time();
if dt.unix_timestamp() > 0 {
boot_time = Some(dt.unix_timestamp() as time_t);
}
}
_ => continue,
}
}
(boot_time, nusers)
}
#[cfg(windows)]
fn process_utmpx() -> (Option<time_t>, usize) {
(None, 0) // TODO: change 0 to number of users
}
fn print_nusers(nusers: usize) {
match nusers.cmp(&1) {
std::cmp::Ordering::Equal => print!("1 user, "),
std::cmp::Ordering::Greater => print!("{nusers} users, "),
_ => {}
};
}
fn print_time() {
let local_time = Local::now().time();
print!(" {} ", local_time.format("%H:%M:%S"));
}
#[cfg(unix)]
fn get_uptime(boot_time: Option<time_t>) -> i64 {
use std::fs::File;
use std::io::Read;
let mut proc_uptime_s = String::new();
let proc_uptime = File::open("/proc/uptime")
.ok()
.and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok())
.and_then(|_| proc_uptime_s.split_whitespace().next())
.and_then(|s| s.split('.').next().unwrap_or("0").parse().ok());
proc_uptime.unwrap_or_else(|| match boot_time {
Some(t) => {
let now = Local::now().timestamp();
#[cfg(target_pointer_width = "64")]
let boottime: i64 = t;
#[cfg(not(target_pointer_width = "64"))]
let boottime: i64 = t.into();
now - boottime
}
None => -1,
})
}
#[cfg(windows)]
fn get_uptime(_boot_time: Option<time_t>) -> i64 {
unsafe { GetTickCount() as i64 }
}
fn print_uptime(upsecs: i64) {
let updays = upsecs / 86400;
let uphours = (upsecs - (updays * 86400)) / 3600;
let upmins = (upsecs - (updays * 86400) - (uphours * 3600)) / 60;
match updays.cmp(&1) {
std::cmp::Ordering::Equal => print!("up {updays:1} day, {uphours:2}:{upmins:02}, "),
std::cmp::Ordering::Greater => {
print!("up {updays:1} days, {uphours:2}:{upmins:02}, ");
}
_ => print!("up {uphours:2}:{upmins:02}, "),
};
}

View file

@ -3,15 +3,11 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) getloadavg upsecs updays nusers loadavg boottime uphours upmins
use chrono::{Local, TimeZone, Utc};
use clap::{crate_version, Arg, ArgAction, Command};
use uucore::libc::time_t;
use uucore::{format_usage, help_about, help_usage};
use uucore::error::{UResult, USimpleError};
mod platform;
const ABOUT: &str = help_about!("uptime.md");
const USAGE: &str = help_usage!("uptime.md");
@ -19,40 +15,8 @@ pub mod options {
pub static SINCE: &str = "since";
}
#[cfg(unix)]
use uucore::libc::getloadavg;
#[cfg(windows)]
extern "C" {
fn GetTickCount() -> uucore::libc::uint32_t;
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app().try_get_matches_from(args)?;
let (boot_time, user_count) = process_utmpx();
let uptime = get_uptime(boot_time);
if uptime < 0 {
Err(USimpleError::new(1, "could not retrieve system uptime"))
} else {
if matches.get_flag(options::SINCE) {
let initial_date = Local
.timestamp_opt(Utc::now().timestamp() - uptime, 0)
.unwrap();
println!("{}", initial_date.format("%Y-%m-%d %H:%M:%S"));
return Ok(());
}
print_time();
let upsecs = uptime;
print_uptime(upsecs);
print_nusers(user_count);
print_loadavg();
Ok(())
}
}
use platform::uumain;
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
@ -68,115 +32,3 @@ pub fn uu_app() -> Command {
.action(ArgAction::SetTrue),
)
}
#[cfg(unix)]
fn print_loadavg() {
use uucore::libc::c_double;
let mut avg: [c_double; 3] = [0.0; 3];
let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) };
if loads == -1 {
println!();
} else {
print!("load average: ");
for n in 0..loads {
print!(
"{:.2}{}",
avg[n as usize],
if n == loads - 1 { "\n" } else { ", " }
);
}
}
}
#[cfg(windows)]
fn print_loadavg() {
// XXX: currently this is a noop as Windows does not seem to have anything comparable to
// getloadavg()
}
#[cfg(unix)]
fn process_utmpx() -> (Option<time_t>, usize) {
use uucore::utmpx::*;
let mut nusers = 0;
let mut boot_time = None;
for line in Utmpx::iter_all_records() {
match line.record_type() {
USER_PROCESS => nusers += 1,
BOOT_TIME => {
let dt = line.login_time();
if dt.unix_timestamp() > 0 {
boot_time = Some(dt.unix_timestamp() as time_t);
}
}
_ => continue,
}
}
(boot_time, nusers)
}
#[cfg(windows)]
fn process_utmpx() -> (Option<time_t>, usize) {
(None, 0) // TODO: change 0 to number of users
}
fn print_nusers(nusers: usize) {
match nusers.cmp(&1) {
std::cmp::Ordering::Equal => print!("1 user, "),
std::cmp::Ordering::Greater => print!("{nusers} users, "),
_ => {}
};
}
fn print_time() {
let local_time = Local::now().time();
print!(" {} ", local_time.format("%H:%M:%S"));
}
#[cfg(unix)]
fn get_uptime(boot_time: Option<time_t>) -> i64 {
use std::fs::File;
use std::io::Read;
let mut proc_uptime_s = String::new();
let proc_uptime = File::open("/proc/uptime")
.ok()
.and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok())
.and_then(|_| proc_uptime_s.split_whitespace().next())
.and_then(|s| s.split('.').next().unwrap_or("0").parse().ok());
proc_uptime.unwrap_or_else(|| match boot_time {
Some(t) => {
let now = Local::now().timestamp();
#[cfg(target_pointer_width = "64")]
let boottime: i64 = t;
#[cfg(not(target_pointer_width = "64"))]
let boottime: i64 = t.into();
now - boottime
}
None => -1,
})
}
#[cfg(windows)]
fn get_uptime(_boot_time: Option<time_t>) -> i64 {
unsafe { GetTickCount() as i64 }
}
fn print_uptime(upsecs: i64) {
let updays = upsecs / 86400;
let uphours = (upsecs - (updays * 86400)) / 3600;
let upmins = (upsecs - (updays * 86400) - (uphours * 3600)) / 60;
match updays.cmp(&1) {
std::cmp::Ordering::Equal => print!("up {updays:1} day, {uphours:2}:{upmins:02}, "),
std::cmp::Ordering::Greater => {
print!("up {updays:1} days, {uphours:2}:{upmins:02}, ");
}
_ => print!("up {uphours:2}:{upmins:02}, "),
};
}

View file

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

View file

@ -0,0 +1,17 @@
// This file is part of the uutils coreutils 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,53 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (paths) wtmp
use crate::uu_app;
use std::ffi::OsString;
use std::path::Path;
use uucore::error::UResult;
use uucore::utmpx::{self, Utmpx};
static ARG_FILES: &str = "files";
fn get_long_usage() -> String {
format!(
"Output who is currently logged in according to FILE.
If FILE is not specified, use {}. /var/log/wtmp as FILE is common.",
utmpx::DEFAULT_FILE
)
}
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let files: Vec<&Path> = matches
.get_many::<OsString>(ARG_FILES)
.map(|v| v.map(AsRef::as_ref).collect())
.unwrap_or_default();
let filename = if files.is_empty() {
utmpx::DEFAULT_FILE.as_ref()
} else {
files[0]
};
let mut users = Utmpx::iter_all_records_from(filename)
.filter(Utmpx::is_user_process)
.map(|ut| ut.user())
.collect::<Vec<_>>();
if !users.is_empty() {
users.sort();
println!("{}", users.join(" "));
}
Ok(())
}

View file

@ -5,57 +5,19 @@
// spell-checker:ignore (paths) wtmp
use std::ffi::OsString;
use std::path::Path;
use clap::builder::ValueParser;
use clap::{crate_version, Arg, Command};
use uucore::error::UResult;
use uucore::utmpx::{self, Utmpx};
use uucore::{format_usage, help_about, help_usage};
mod platform;
const ABOUT: &str = help_about!("users.md");
const USAGE: &str = help_usage!("users.md");
static ARG_FILES: &str = "files";
fn get_long_usage() -> String {
format!(
"Output who is currently logged in according to FILE.
If FILE is not specified, use {}. /var/log/wtmp as FILE is common.",
utmpx::DEFAULT_FILE
)
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let files: Vec<&Path> = matches
.get_many::<OsString>(ARG_FILES)
.map(|v| v.map(AsRef::as_ref).collect())
.unwrap_or_default();
let filename = if files.is_empty() {
utmpx::DEFAULT_FILE.as_ref()
} else {
files[0]
};
let mut users = Utmpx::iter_all_records_from(filename)
.filter(Utmpx::is_user_process)
.map(|ut| ut.user())
.collect::<Vec<_>>();
if !users.is_empty() {
users.sort();
println!("{}", users.join(" "));
}
Ok(())
}
use platform::uumain;
pub fn uu_app() -> Command {
Command::new(uucore::util_name())

View file

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

View file

@ -0,0 +1,17 @@
// This file is part of the uutils coreutils 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,430 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) ttyname hostnames runlevel mesg wtmp statted boottime deadprocs initspawn clockchange curr runlvline pidstr exitstr hoststr
use crate::options;
use crate::uu_app;
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult};
use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP};
use uucore::utmpx::{self, time, Utmpx};
use std::borrow::Cow;
use std::ffi::CStr;
use std::fmt::Write;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
fn get_long_usage() -> String {
format!(
"If FILE is not specified, use {}. /var/log/wtmp as FILE is common.\n\
If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.",
utmpx::DEFAULT_FILE,
)
}
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let files: Vec<String> = matches
.get_many::<String>(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
// If true, attempt to canonicalize hostnames via a DNS lookup.
let do_lookup = matches.get_flag(options::LOOKUP);
// If true, display only a list of usernames and count of
// the users logged on.
// Ignored for 'who am i'.
let short_list = matches.get_flag(options::COUNT);
let all = matches.get_flag(options::ALL);
// If true, display a line at the top describing each field.
let include_heading = matches.get_flag(options::HEADING);
// If true, display a '+' for each user if mesg y, a '-' if mesg n,
// or a '?' if their tty cannot be statted.
let include_mesg = all || matches.get_flag(options::MESG);
// If true, display the last boot time.
let need_boottime = all || matches.get_flag(options::BOOT);
// If true, display dead processes.
let need_deadprocs = all || matches.get_flag(options::DEAD);
// If true, display processes waiting for user login.
let need_login = all || matches.get_flag(options::LOGIN);
// If true, display processes started by init.
let need_initspawn = all || matches.get_flag(options::PROCESS);
// If true, display the last clock change.
let need_clockchange = all || matches.get_flag(options::TIME);
// If true, display the current runlevel.
let need_runlevel = all || matches.get_flag(options::RUNLEVEL);
let use_defaults = !(all
|| need_boottime
|| need_deadprocs
|| need_login
|| need_initspawn
|| need_runlevel
|| need_clockchange
|| matches.get_flag(options::USERS));
// If true, display user processes.
let need_users = all || matches.get_flag(options::USERS) || use_defaults;
// If true, display the hours:minutes since each user has touched
// the keyboard, or "." if within the last minute, or "old" if
// not within the last day.
let include_idle = need_deadprocs || need_login || need_runlevel || need_users;
// If true, display process termination & exit status.
let include_exit = need_deadprocs;
// If true, display only name, line, and time fields.
let short_output = !include_exit && use_defaults;
// If true, display info only for the controlling tty.
let my_line_only = matches.get_flag(options::ONLY_HOSTNAME_USER) || files.len() == 2;
let mut who = Who {
do_lookup,
short_list,
short_output,
include_idle,
include_heading,
include_mesg,
include_exit,
need_boottime,
need_deadprocs,
need_login,
need_initspawn,
need_clockchange,
need_runlevel,
need_users,
my_line_only,
args: files,
};
who.exec()
}
struct Who {
do_lookup: bool,
short_list: bool,
short_output: bool,
include_idle: bool,
include_heading: bool,
include_mesg: bool,
include_exit: bool,
need_boottime: bool,
need_deadprocs: bool,
need_login: bool,
need_initspawn: bool,
need_clockchange: bool,
need_runlevel: bool,
need_users: bool,
my_line_only: bool,
args: Vec<String>,
}
fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> {
thread_local! {
static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap();
}
NOW.with(|n| {
let now = n.unix_timestamp();
if boottime < when && now - 24 * 3600 < when && when <= now {
let seconds_idle = now - when;
if seconds_idle < 60 {
" . ".into()
} else {
format!(
"{:02}:{:02}",
seconds_idle / 3600,
(seconds_idle % 3600) / 60
)
.into()
}
} else {
" old ".into()
}
})
}
fn time_string(ut: &Utmpx) -> String {
// "%b %e %H:%M"
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]")
.unwrap();
ut.login_time().format(&time_format).unwrap() // LC_ALL=C
}
#[inline]
fn current_tty() -> String {
unsafe {
let res = ttyname(STDIN_FILENO);
if res.is_null() {
String::new()
} else {
CStr::from_ptr(res as *const _)
.to_string_lossy()
.trim_start_matches("/dev/")
.to_owned()
}
}
}
impl Who {
#[allow(clippy::cognitive_complexity)]
fn exec(&mut self) -> UResult<()> {
let run_level_chk = |_record: i16| {
#[cfg(not(target_os = "linux"))]
return false;
#[cfg(target_os = "linux")]
return _record == utmpx::RUN_LVL;
};
let f = if self.args.len() == 1 {
self.args[0].as_ref()
} else {
utmpx::DEFAULT_FILE
};
if self.short_list {
let users = Utmpx::iter_all_records_from(f)
.filter(Utmpx::is_user_process)
.map(|ut| ut.user())
.collect::<Vec<_>>();
println!("{}", users.join(" "));
println!("# users={}", users.len());
} else {
let records = Utmpx::iter_all_records_from(f);
if self.include_heading {
self.print_heading();
}
let cur_tty = if self.my_line_only {
current_tty()
} else {
String::new()
};
for ut in records {
if !self.my_line_only || cur_tty == ut.tty_device() {
if self.need_users && ut.is_user_process() {
self.print_user(&ut)?;
} else if self.need_runlevel && run_level_chk(ut.record_type()) {
if cfg!(target_os = "linux") {
self.print_runlevel(&ut);
}
} else if self.need_boottime && ut.record_type() == utmpx::BOOT_TIME {
self.print_boottime(&ut);
} else if self.need_clockchange && ut.record_type() == utmpx::NEW_TIME {
self.print_clockchange(&ut);
} else if self.need_initspawn && ut.record_type() == utmpx::INIT_PROCESS {
self.print_initspawn(&ut);
} else if self.need_login && ut.record_type() == utmpx::LOGIN_PROCESS {
self.print_login(&ut);
} else if self.need_deadprocs && ut.record_type() == utmpx::DEAD_PROCESS {
self.print_deadprocs(&ut);
}
}
if ut.record_type() == utmpx::BOOT_TIME {}
}
}
Ok(())
}
#[inline]
fn print_runlevel(&self, ut: &Utmpx) {
let last = (ut.pid() / 256) as u8 as char;
let curr = (ut.pid() % 256) as u8 as char;
let runlvline = format!("run-level {curr}");
let comment = format!("last={}", if last == 'N' { 'S' } else { 'N' });
self.print_line(
"",
' ',
&runlvline,
&time_string(ut),
"",
"",
if last.is_control() { "" } else { &comment },
"",
);
}
#[inline]
fn print_clockchange(&self, ut: &Utmpx) {
self.print_line("", ' ', "clock change", &time_string(ut), "", "", "", "");
}
#[inline]
fn print_login(&self, ut: &Utmpx) {
let comment = format!("id={}", ut.terminal_suffix());
let pidstr = format!("{}", ut.pid());
self.print_line(
"LOGIN",
' ',
&ut.tty_device(),
&time_string(ut),
"",
&pidstr,
&comment,
"",
);
}
#[inline]
fn print_deadprocs(&self, ut: &Utmpx) {
let comment = format!("id={}", ut.terminal_suffix());
let pidstr = format!("{}", ut.pid());
let e = ut.exit_status();
let exitstr = format!("term={} exit={}", e.0, e.1);
self.print_line(
"",
' ',
&ut.tty_device(),
&time_string(ut),
"",
&pidstr,
&comment,
&exitstr,
);
}
#[inline]
fn print_initspawn(&self, ut: &Utmpx) {
let comment = format!("id={}", ut.terminal_suffix());
let pidstr = format!("{}", ut.pid());
self.print_line(
"",
' ',
&ut.tty_device(),
&time_string(ut),
"",
&pidstr,
&comment,
"",
);
}
#[inline]
fn print_boottime(&self, ut: &Utmpx) {
self.print_line("", ' ', "system boot", &time_string(ut), "", "", "", "");
}
fn print_user(&self, ut: &Utmpx) -> UResult<()> {
let mut p = PathBuf::from("/dev");
p.push(ut.tty_device().as_str());
let mesg;
let last_change;
match p.metadata() {
Ok(meta) => {
#[cfg(all(
not(target_os = "android"),
not(target_os = "freebsd"),
not(target_vendor = "apple")
))]
let iwgrp = S_IWGRP;
#[cfg(any(target_os = "android", target_os = "freebsd", target_vendor = "apple"))]
let iwgrp = S_IWGRP as u32;
mesg = if meta.mode() & iwgrp == 0 { '-' } else { '+' };
last_change = meta.atime();
}
_ => {
mesg = '?';
last_change = 0;
}
}
let idle = if last_change == 0 {
" ?".into()
} else {
idle_string(last_change, 0)
};
let s = if self.do_lookup {
ut.canon_host().map_err_context(|| {
let host = ut.host();
format!(
"failed to canonicalize {}",
host.split(':').next().unwrap_or(&host).quote()
)
})?
} else {
ut.host()
};
let hoststr = if s.is_empty() { s } else { format!("({s})") };
self.print_line(
ut.user().as_ref(),
mesg,
ut.tty_device().as_ref(),
time_string(ut).as_str(),
idle.as_ref(),
format!("{}", ut.pid()).as_str(),
hoststr.as_str(),
"",
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn print_line(
&self,
user: &str,
state: char,
line: &str,
time: &str,
idle: &str,
pid: &str,
comment: &str,
exit: &str,
) {
let mut buf = String::with_capacity(64);
let msg = vec![' ', state].into_iter().collect::<String>();
write!(buf, "{user:<8}").unwrap();
if self.include_mesg {
buf.push_str(&msg);
}
write!(buf, " {line:<12}").unwrap();
// "%b %e %H:%M" (LC_ALL=C)
let time_size = 3 + 2 + 2 + 1 + 2;
write!(buf, " {time:<time_size$}").unwrap();
if !self.short_output {
if self.include_idle {
write!(buf, " {idle:<6}").unwrap();
}
write!(buf, " {pid:>10}").unwrap();
}
write!(buf, " {comment:<8}").unwrap();
if self.include_exit {
write!(buf, " {exit:<12}").unwrap();
}
println!("{}", buf.trim_end());
}
#[inline]
fn print_heading(&self) {
self.print_line(
"NAME", ' ', "LINE", "TIME", "IDLE", "PID", "COMMENT", "EXIT",
);
}
}

View file

@ -5,19 +5,11 @@
// spell-checker:ignore (ToDO) ttyname hostnames runlevel mesg wtmp statted boottime deadprocs initspawn clockchange curr runlvline pidstr exitstr hoststr
use uucore::display::Quotable;
use uucore::error::{FromIo, UResult};
use uucore::libc::{ttyname, STDIN_FILENO, S_IWGRP};
use uucore::utmpx::{self, time, Utmpx};
use clap::{crate_version, Arg, ArgAction, Command};
use std::borrow::Cow;
use std::ffi::CStr;
use std::fmt::Write;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
use uucore::{format_usage, help_about, help_usage};
mod platform;
mod options {
pub const ALL: &str = "all";
pub const BOOT: &str = "boot";
@ -44,107 +36,8 @@ static RUNLEVEL_HELP: &str = "print current runlevel";
#[cfg(not(target_os = "linux"))]
static RUNLEVEL_HELP: &str = "print current runlevel (This is meaningless on non Linux)";
fn get_long_usage() -> String {
format!(
"If FILE is not specified, use {}. /var/log/wtmp as FILE is common.\n\
If ARG1 ARG2 given, -m presumed: 'am i' or 'mom likes' are usual.",
utmpx::DEFAULT_FILE,
)
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uu_app()
.after_help(get_long_usage())
.try_get_matches_from(args)?;
let files: Vec<String> = matches
.get_many::<String>(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default();
// If true, attempt to canonicalize hostnames via a DNS lookup.
let do_lookup = matches.get_flag(options::LOOKUP);
// If true, display only a list of usernames and count of
// the users logged on.
// Ignored for 'who am i'.
let short_list = matches.get_flag(options::COUNT);
let all = matches.get_flag(options::ALL);
// If true, display a line at the top describing each field.
let include_heading = matches.get_flag(options::HEADING);
// If true, display a '+' for each user if mesg y, a '-' if mesg n,
// or a '?' if their tty cannot be statted.
let include_mesg = all || matches.get_flag(options::MESG);
// If true, display the last boot time.
let need_boottime = all || matches.get_flag(options::BOOT);
// If true, display dead processes.
let need_deadprocs = all || matches.get_flag(options::DEAD);
// If true, display processes waiting for user login.
let need_login = all || matches.get_flag(options::LOGIN);
// If true, display processes started by init.
let need_initspawn = all || matches.get_flag(options::PROCESS);
// If true, display the last clock change.
let need_clockchange = all || matches.get_flag(options::TIME);
// If true, display the current runlevel.
let need_runlevel = all || matches.get_flag(options::RUNLEVEL);
let use_defaults = !(all
|| need_boottime
|| need_deadprocs
|| need_login
|| need_initspawn
|| need_runlevel
|| need_clockchange
|| matches.get_flag(options::USERS));
// If true, display user processes.
let need_users = all || matches.get_flag(options::USERS) || use_defaults;
// If true, display the hours:minutes since each user has touched
// the keyboard, or "." if within the last minute, or "old" if
// not within the last day.
let include_idle = need_deadprocs || need_login || need_runlevel || need_users;
// If true, display process termination & exit status.
let include_exit = need_deadprocs;
// If true, display only name, line, and time fields.
let short_output = !include_exit && use_defaults;
// If true, display info only for the controlling tty.
let my_line_only = matches.get_flag(options::ONLY_HOSTNAME_USER) || files.len() == 2;
let mut who = Who {
do_lookup,
short_list,
short_output,
include_idle,
include_heading,
include_mesg,
include_exit,
need_boottime,
need_deadprocs,
need_login,
need_initspawn,
need_clockchange,
need_runlevel,
need_users,
my_line_only,
args: files,
};
who.exec()
}
use platform::uumain;
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
@ -256,312 +149,3 @@ pub fn uu_app() -> Command {
.value_hint(clap::ValueHint::FilePath),
)
}
struct Who {
do_lookup: bool,
short_list: bool,
short_output: bool,
include_idle: bool,
include_heading: bool,
include_mesg: bool,
include_exit: bool,
need_boottime: bool,
need_deadprocs: bool,
need_login: bool,
need_initspawn: bool,
need_clockchange: bool,
need_runlevel: bool,
need_users: bool,
my_line_only: bool,
args: Vec<String>,
}
fn idle_string<'a>(when: i64, boottime: i64) -> Cow<'a, str> {
thread_local! {
static NOW: time::OffsetDateTime = time::OffsetDateTime::now_local().unwrap();
}
NOW.with(|n| {
let now = n.unix_timestamp();
if boottime < when && now - 24 * 3600 < when && when <= now {
let seconds_idle = now - when;
if seconds_idle < 60 {
" . ".into()
} else {
format!(
"{:02}:{:02}",
seconds_idle / 3600,
(seconds_idle % 3600) / 60
)
.into()
}
} else {
" old ".into()
}
})
}
fn time_string(ut: &Utmpx) -> String {
// "%b %e %H:%M"
let time_format: Vec<time::format_description::FormatItem> =
time::format_description::parse("[month repr:short] [day padding:space] [hour]:[minute]")
.unwrap();
ut.login_time().format(&time_format).unwrap() // LC_ALL=C
}
#[inline]
fn current_tty() -> String {
unsafe {
let res = ttyname(STDIN_FILENO);
if res.is_null() {
String::new()
} else {
CStr::from_ptr(res as *const _)
.to_string_lossy()
.trim_start_matches("/dev/")
.to_owned()
}
}
}
impl Who {
#[allow(clippy::cognitive_complexity)]
fn exec(&mut self) -> UResult<()> {
let run_level_chk = |_record: i16| {
#[cfg(not(target_os = "linux"))]
return false;
#[cfg(target_os = "linux")]
return _record == utmpx::RUN_LVL;
};
let f = if self.args.len() == 1 {
self.args[0].as_ref()
} else {
utmpx::DEFAULT_FILE
};
if self.short_list {
let users = Utmpx::iter_all_records_from(f)
.filter(Utmpx::is_user_process)
.map(|ut| ut.user())
.collect::<Vec<_>>();
println!("{}", users.join(" "));
println!("# users={}", users.len());
} else {
let records = Utmpx::iter_all_records_from(f);
if self.include_heading {
self.print_heading();
}
let cur_tty = if self.my_line_only {
current_tty()
} else {
String::new()
};
for ut in records {
if !self.my_line_only || cur_tty == ut.tty_device() {
if self.need_users && ut.is_user_process() {
self.print_user(&ut)?;
} else if self.need_runlevel && run_level_chk(ut.record_type()) {
if cfg!(target_os = "linux") {
self.print_runlevel(&ut);
}
} else if self.need_boottime && ut.record_type() == utmpx::BOOT_TIME {
self.print_boottime(&ut);
} else if self.need_clockchange && ut.record_type() == utmpx::NEW_TIME {
self.print_clockchange(&ut);
} else if self.need_initspawn && ut.record_type() == utmpx::INIT_PROCESS {
self.print_initspawn(&ut);
} else if self.need_login && ut.record_type() == utmpx::LOGIN_PROCESS {
self.print_login(&ut);
} else if self.need_deadprocs && ut.record_type() == utmpx::DEAD_PROCESS {
self.print_deadprocs(&ut);
}
}
if ut.record_type() == utmpx::BOOT_TIME {}
}
}
Ok(())
}
#[inline]
fn print_runlevel(&self, ut: &Utmpx) {
let last = (ut.pid() / 256) as u8 as char;
let curr = (ut.pid() % 256) as u8 as char;
let runlvline = format!("run-level {curr}");
let comment = format!("last={}", if last == 'N' { 'S' } else { 'N' });
self.print_line(
"",
' ',
&runlvline,
&time_string(ut),
"",
"",
if last.is_control() { "" } else { &comment },
"",
);
}
#[inline]
fn print_clockchange(&self, ut: &Utmpx) {
self.print_line("", ' ', "clock change", &time_string(ut), "", "", "", "");
}
#[inline]
fn print_login(&self, ut: &Utmpx) {
let comment = format!("id={}", ut.terminal_suffix());
let pidstr = format!("{}", ut.pid());
self.print_line(
"LOGIN",
' ',
&ut.tty_device(),
&time_string(ut),
"",
&pidstr,
&comment,
"",
);
}
#[inline]
fn print_deadprocs(&self, ut: &Utmpx) {
let comment = format!("id={}", ut.terminal_suffix());
let pidstr = format!("{}", ut.pid());
let e = ut.exit_status();
let exitstr = format!("term={} exit={}", e.0, e.1);
self.print_line(
"",
' ',
&ut.tty_device(),
&time_string(ut),
"",
&pidstr,
&comment,
&exitstr,
);
}
#[inline]
fn print_initspawn(&self, ut: &Utmpx) {
let comment = format!("id={}", ut.terminal_suffix());
let pidstr = format!("{}", ut.pid());
self.print_line(
"",
' ',
&ut.tty_device(),
&time_string(ut),
"",
&pidstr,
&comment,
"",
);
}
#[inline]
fn print_boottime(&self, ut: &Utmpx) {
self.print_line("", ' ', "system boot", &time_string(ut), "", "", "", "");
}
fn print_user(&self, ut: &Utmpx) -> UResult<()> {
let mut p = PathBuf::from("/dev");
p.push(ut.tty_device().as_str());
let mesg;
let last_change;
match p.metadata() {
Ok(meta) => {
#[cfg(all(
not(target_os = "android"),
not(target_os = "freebsd"),
not(target_vendor = "apple")
))]
let iwgrp = S_IWGRP;
#[cfg(any(target_os = "android", target_os = "freebsd", target_vendor = "apple"))]
let iwgrp = S_IWGRP as u32;
mesg = if meta.mode() & iwgrp == 0 { '-' } else { '+' };
last_change = meta.atime();
}
_ => {
mesg = '?';
last_change = 0;
}
}
let idle = if last_change == 0 {
" ?".into()
} else {
idle_string(last_change, 0)
};
let s = if self.do_lookup {
ut.canon_host().map_err_context(|| {
let host = ut.host();
format!(
"failed to canonicalize {}",
host.split(':').next().unwrap_or(&host).quote()
)
})?
} else {
ut.host()
};
let hoststr = if s.is_empty() { s } else { format!("({s})") };
self.print_line(
ut.user().as_ref(),
mesg,
ut.tty_device().as_ref(),
time_string(ut).as_str(),
idle.as_ref(),
format!("{}", ut.pid()).as_str(),
hoststr.as_str(),
"",
);
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn print_line(
&self,
user: &str,
state: char,
line: &str,
time: &str,
idle: &str,
pid: &str,
comment: &str,
exit: &str,
) {
let mut buf = String::with_capacity(64);
let msg = vec![' ', state].into_iter().collect::<String>();
write!(buf, "{user:<8}").unwrap();
if self.include_mesg {
buf.push_str(&msg);
}
write!(buf, " {line:<12}").unwrap();
// "%b %e %H:%M" (LC_ALL=C)
let time_size = 3 + 2 + 2 + 1 + 2;
write!(buf, " {time:<time_size$}").unwrap();
if !self.short_output {
if self.include_idle {
write!(buf, " {idle:<6}").unwrap();
}
write!(buf, " {pid:>10}").unwrap();
}
write!(buf, " {comment:<8}").unwrap();
if self.include_exit {
write!(buf, " {exit:<12}").unwrap();
}
println!("{}", buf.trim_end());
}
#[inline]
fn print_heading(&self) {
self.print_line(
"NAME", ' ', "LINE", "TIME", "IDLE", "PID", "COMMENT", "EXIT",
);
}
}

View file

@ -52,6 +52,7 @@ pub mod signals;
unix,
not(target_os = "android"),
not(target_os = "fuchsia"),
not(target_os = "openbsd"),
not(target_os = "redox"),
not(target_env = "musl"),
feature = "utmpx"

View file

@ -79,6 +79,7 @@ pub use crate::features::signals;
unix,
not(target_os = "android"),
not(target_os = "fuchsia"),
not(target_os = "openbsd"),
not(target_os = "redox"),
not(target_env = "musl"),
feature = "utmpx"