diff --git a/src/uu/chroot/src/chroot.rs b/src/uu/chroot/src/chroot.rs index 9ea861d46..fb20b0ccc 100644 --- a/src/uu/chroot/src/chroot.rs +++ b/src/uu/chroot/src/chroot.rs @@ -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()) } } diff --git a/src/uu/id/src/id.rs b/src/uu/id/src/id.rs index 8b16ba8b7..b8fb51d79 100644 --- a/src/uu/id/src/id.rs +++ b/src/uu/id/src/id.rs @@ -507,7 +507,7 @@ fn pline(possible_uid: Option) { ); } -#[cfg(any(target_os = "linux", target_os = "android"))] +#[cfg(any(target_os = "linux", target_os = "android", target_os = "openbsd"))] fn pline(possible_uid: Option) { let uid = possible_uid.unwrap_or_else(getuid); let pw = Passwd::locate(uid).unwrap(); @@ -524,10 +524,10 @@ fn pline(possible_uid: Option) { ); } -#[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}; diff --git a/src/uu/nohup/src/nohup.rs b/src/uu/nohup/src/nohup.rs index 602cb7ca7..74dfa71c5 100644 --- a/src/uu/nohup/src/nohup.rs +++ b/src/uu/nohup/src/nohup.rs @@ -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() } diff --git a/src/uu/pinky/src/pinky.rs b/src/uu/pinky/src/pinky.rs index 02161cb36..6b393b905 100644 --- a/src/uu/pinky/src/pinky.rs +++ b/src/uu/pinky/src/pinky.rs @@ -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 = matches - .get_many::(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, -} - 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::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 { - 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: 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)); - } -} diff --git a/src/uu/pinky/src/platform/mod.rs b/src/uu/pinky/src/platform/mod.rs new file mode 100644 index 000000000..e0e87dca1 --- /dev/null +++ b/src/uu/pinky/src/platform/mod.rs @@ -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::*; diff --git a/src/uu/pinky/src/platform/openbsd.rs b/src/uu/pinky/src/platform/openbsd.rs new file mode 100644 index 000000000..7e6970c1f --- /dev/null +++ b/src/uu/pinky/src/platform/openbsd.rs @@ -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(()) +} diff --git a/src/uu/pinky/src/platform/unix.rs b/src/uu/pinky/src/platform/unix.rs new file mode 100644 index 000000000..ecea2b9f2 --- /dev/null +++ b/src/uu/pinky/src/platform/unix.rs @@ -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 = matches + .get_many::(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, +} + +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::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 { + 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: 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)); + } +} diff --git a/src/uu/uptime/src/platform/mod.rs b/src/uu/uptime/src/platform/mod.rs new file mode 100644 index 000000000..e0e87dca1 --- /dev/null +++ b/src/uu/uptime/src/platform/mod.rs @@ -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::*; diff --git a/src/uu/uptime/src/platform/openbsd.rs b/src/uu/uptime/src/platform/openbsd.rs new file mode 100644 index 000000000..7e6970c1f --- /dev/null +++ b/src/uu/uptime/src/platform/openbsd.rs @@ -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(()) +} diff --git a/src/uu/uptime/src/platform/unix.rs b/src/uu/uptime/src/platform/unix.rs new file mode 100644 index 000000000..df3e5e653 --- /dev/null +++ b/src/uu/uptime/src/platform/unix.rs @@ -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, 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, 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) -> 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) -> 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}, "), + }; +} diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index 778fbc920..196ae60ba 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -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, 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, 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) -> 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) -> 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}, "), - }; -} diff --git a/src/uu/users/src/platform/mod.rs b/src/uu/users/src/platform/mod.rs new file mode 100644 index 000000000..e0e87dca1 --- /dev/null +++ b/src/uu/users/src/platform/mod.rs @@ -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::*; diff --git a/src/uu/users/src/platform/openbsd.rs b/src/uu/users/src/platform/openbsd.rs new file mode 100644 index 000000000..7e6970c1f --- /dev/null +++ b/src/uu/users/src/platform/openbsd.rs @@ -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(()) +} diff --git a/src/uu/users/src/platform/unix.rs b/src/uu/users/src/platform/unix.rs new file mode 100644 index 000000000..99c9ce776 --- /dev/null +++ b/src/uu/users/src/platform/unix.rs @@ -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::(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::>(); + + if !users.is_empty() { + users.sort(); + println!("{}", users.join(" ")); + } + + Ok(()) +} diff --git a/src/uu/users/src/users.rs b/src/uu/users/src/users.rs index 199882b7e..d299399f3 100644 --- a/src/uu/users/src/users.rs +++ b/src/uu/users/src/users.rs @@ -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::(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::>(); - - if !users.is_empty() { - users.sort(); - println!("{}", users.join(" ")); - } - - Ok(()) -} +use platform::uumain; pub fn uu_app() -> Command { Command::new(uucore::util_name()) diff --git a/src/uu/who/src/platform/mod.rs b/src/uu/who/src/platform/mod.rs new file mode 100644 index 000000000..e0e87dca1 --- /dev/null +++ b/src/uu/who/src/platform/mod.rs @@ -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::*; diff --git a/src/uu/who/src/platform/openbsd.rs b/src/uu/who/src/platform/openbsd.rs new file mode 100644 index 000000000..7e6970c1f --- /dev/null +++ b/src/uu/who/src/platform/openbsd.rs @@ -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(()) +} diff --git a/src/uu/who/src/platform/unix.rs b/src/uu/who/src/platform/unix.rs new file mode 100644 index 000000000..b59b73a57 --- /dev/null +++ b/src/uu/who/src/platform/unix.rs @@ -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 = matches + .get_many::(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, +} + +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::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::>(); + 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::(); + + 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: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", + ); + } +} diff --git a/src/uu/who/src/who.rs b/src/uu/who/src/who.rs index 788368aaf..1eb28e874 100644 --- a/src/uu/who/src/who.rs +++ b/src/uu/who/src/who.rs @@ -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 = matches - .get_many::(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, -} - -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::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::>(); - 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::(); - - 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: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", - ); - } -} diff --git a/src/uucore/src/lib/features.rs b/src/uucore/src/lib/features.rs index a28e8a7bf..e26de487b 100644 --- a/src/uucore/src/lib/features.rs +++ b/src/uucore/src/lib/features.rs @@ -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" diff --git a/src/uucore/src/lib/lib.rs b/src/uucore/src/lib/lib.rs index 426b4216c..dcef26443 100644 --- a/src/uucore/src/lib/lib.rs +++ b/src/uucore/src/lib/lib.rs @@ -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"