Merge pull request #160 from fuad1502/dmesg-time-format

Add`—time-format` option support for `dmesg`
This commit is contained in:
Daniel Hofstetter
2024-11-30 16:39:56 +01:00
committed by GitHub
14 changed files with 497 additions and 6 deletions

170
Cargo.lock generated
View File

@@ -11,6 +11,21 @@ dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.15"
@@ -60,6 +75,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -72,6 +93,12 @@ version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecount"
version = "0.6.8"
@@ -84,6 +111,15 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -96,6 +132,20 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-targets 0.52.6",
]
[[package]]
name = "clap"
version = "4.5.21"
@@ -270,6 +320,29 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core 0.52.0",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
@@ -293,6 +366,15 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.167"
@@ -311,6 +393,12 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "memchr"
version = "2.7.4"
@@ -344,6 +432,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_threads"
version = "0.1.7"
@@ -671,6 +768,12 @@ dependencies = [
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -903,6 +1006,7 @@ dependencies = [
name = "uu_dmesg"
version = "0.0.1"
dependencies = [
"chrono",
"clap",
"regex",
"serde",
@@ -1013,6 +1117,61 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.82",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
[[package]]
name = "wild"
version = "2.2.1"
@@ -1050,7 +1209,16 @@ version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
dependencies = [
"windows-core",
"windows-core 0.57.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]

View File

@@ -82,6 +82,8 @@ tempfile = { workspace = true }
libc = { workspace = true }
rand = { workspace = true }
uucore = { workspace = true, features = ["entries", "process", "signals"] }
# dmesg test require fixed-boot-time feature turned on.
dmesg = { version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg", features = ["fixed-boot-time"] }
[target.'cfg(unix)'.dev-dependencies]
xattr = { workspace = true }

View File

@@ -16,3 +16,7 @@ uucore = { workspace = true }
regex = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
chrono = "0.4.38"
[features]
fixed-boot-time = []

View File

@@ -12,6 +12,7 @@ use uucore::{
};
mod json;
mod time_formatter;
const ABOUT: &str = help_about!("dmesg.md");
const USAGE: &str = help_usage!("dmesg.md");
@@ -26,6 +27,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
if matches.get_flag(options::JSON) {
dmesg.output_format = OutputFormat::Json;
}
if let Some(time_format) = matches.get_one::<String>(options::TIME_FORMAT) {
dmesg.time_format = match &time_format[..] {
"delta" => TimeFormat::Delta,
"reltime" => TimeFormat::Reltime,
"ctime" => TimeFormat::Ctime,
"notime" => TimeFormat::Notime,
"iso" => TimeFormat::Iso,
"raw" => TimeFormat::Raw,
_ => {
return Err(USimpleError::new(
1,
format!("unknown time format: {time_format}"),
))
}
};
}
dmesg.parse()?.print();
Ok(())
}
@@ -49,16 +66,27 @@ pub fn uu_app() -> Command {
.help("use JSON output format")
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::TIME_FORMAT)
.long("time-format")
.help(
"show timestamp using the given format:\n".to_string()
+ " [delta|reltime|ctime|notime|iso|raw]",
)
.action(ArgAction::Set),
)
}
mod options {
pub const KMSG_FILE: &str = "kmsg-file";
pub const JSON: &str = "json";
pub const TIME_FORMAT: &str = "time-format";
}
struct Dmesg<'a> {
kmsg_file: &'a str,
output_format: OutputFormat,
time_format: TimeFormat,
records: Option<Vec<Record>>,
}
@@ -67,6 +95,7 @@ impl Dmesg<'_> {
Dmesg {
kmsg_file: "/dev/kmsg",
output_format: OutputFormat::Normal,
time_format: TimeFormat::Raw,
records: None,
}
}
@@ -112,7 +141,7 @@ impl Dmesg<'_> {
fn print(&self) {
match self.output_format {
OutputFormat::Json => self.print_json(),
OutputFormat::Normal => unimplemented!(),
OutputFormat::Normal => self.print_normal(),
}
}
@@ -121,6 +150,34 @@ impl Dmesg<'_> {
println!("{}", json::serialize_records(records));
}
}
fn print_normal(&self) {
if let Some(records) = &self.records {
let mut reltime_formatter = time_formatter::ReltimeFormatter::new();
let mut delta_formatter = time_formatter::DeltaFormatter::new();
for record in records {
match self.time_format {
TimeFormat::Delta => {
print!("[{}] ", delta_formatter.format(record.timestamp_us))
}
TimeFormat::Reltime => {
print!("[{}] ", reltime_formatter.format(record.timestamp_us))
}
TimeFormat::Ctime => {
print!("[{}] ", time_formatter::ctime(record.timestamp_us))
}
TimeFormat::Iso => {
print!("{} ", time_formatter::iso(record.timestamp_us))
}
TimeFormat::Raw => {
print!("[{}] ", time_formatter::raw(record.timestamp_us))
}
TimeFormat::Notime => (),
}
println!("{}", record.message);
}
}
}
}
enum OutputFormat {
@@ -128,10 +185,19 @@ enum OutputFormat {
Json,
}
enum TimeFormat {
Delta,
Reltime,
Ctime,
Notime,
Iso,
Raw,
}
struct Record {
priority_facility: u32,
_sequence: u64,
timestamp_us: u64,
timestamp_us: i64,
message: String,
}

View File

@@ -23,7 +23,7 @@ struct Dmesg<'a> {
#[derive(serde::Serialize)]
struct Record<'a> {
pri: u32,
time: u64,
time: i64,
msg: &'a str,
}
@@ -129,11 +129,11 @@ impl serde_json::ser::Formatter for DmesgFormatter {
}
}
fn write_u64<W>(&mut self, writer: &mut W, value: u64) -> io::Result<()>
fn write_i64<W>(&mut self, writer: &mut W, value: i64) -> io::Result<()>
where
W: ?Sized + io::Write,
{
// The only u64 field in Dmesg is time, which requires a specific format
// The only i64 field in Dmesg is time, which requires a specific format
let seconds = value / 1000000;
let sub_seconds = value % 1000000;
let repr = format!("{:>5}.{:0>6}", seconds, sub_seconds);

View File

@@ -0,0 +1,170 @@
// This file is part of the uutils util-linux package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{DateTime, FixedOffset, TimeDelta};
#[cfg(feature = "fixed-boot-time")]
use chrono::{NaiveDate, NaiveTime};
use std::sync::OnceLock;
pub fn raw(timestamp_us: i64) -> String {
let seconds = timestamp_us / 1000000;
let sub_seconds = timestamp_us % 1000000;
format!("{:>5}.{:0>6}", seconds, sub_seconds)
}
pub fn ctime(timestamp_us: i64) -> String {
let date_time = boot_time()
.checked_add_signed(TimeDelta::microseconds(timestamp_us))
.unwrap();
date_time.format("%a %b %d %H:%M:%S %Y").to_string()
}
pub fn iso(timestamp_us: i64) -> String {
let date_time = boot_time()
.checked_add_signed(TimeDelta::microseconds(timestamp_us))
.unwrap();
date_time.format("%Y-%m-%dT%H:%M:%S,%6f%:z").to_string()
}
pub struct ReltimeFormatter {
state: State,
prev_timestamp_us: i64,
previous_unix_timestamp: i64,
}
pub struct DeltaFormatter {
state: State,
prev_timestamp_us: i64,
}
pub enum State {
Initial,
AfterBoot,
Delta,
}
impl ReltimeFormatter {
pub fn new() -> Self {
ReltimeFormatter {
state: State::Initial,
prev_timestamp_us: 0,
previous_unix_timestamp: 0,
}
}
pub fn format(&mut self, timestamp_us: i64) -> String {
let date_time = boot_time()
.checked_add_signed(TimeDelta::microseconds(timestamp_us))
.unwrap();
let unix_timestamp = date_time.timestamp();
let minute_changes = (unix_timestamp / 60) != (self.previous_unix_timestamp / 60);
let format_res = match self.state {
State::Initial => date_time.format("%b%d %H:%M").to_string(),
_ if minute_changes => date_time.format("%b%d %H:%M").to_string(),
State::AfterBoot => Self::delta(0),
State::Delta => Self::delta(timestamp_us - self.prev_timestamp_us),
};
self.prev_timestamp_us = timestamp_us;
self.previous_unix_timestamp = unix_timestamp;
self.state = match self.state {
State::Initial if timestamp_us == 0 => State::AfterBoot,
_ => State::Delta,
};
format_res
}
fn delta(delta_us: i64) -> String {
let seconds = i64::abs(delta_us / 1000000);
let sub_seconds = i64::abs(delta_us % 1000000);
let sign = if delta_us >= 0 { '+' } else { '-' };
let res = format!("{}{}.{:0>6}", sign, seconds, sub_seconds);
format!("{:>11}", res)
}
}
impl DeltaFormatter {
pub fn new() -> Self {
DeltaFormatter {
state: State::Initial,
prev_timestamp_us: 0,
}
}
pub fn format(&mut self, timestamp_us: i64) -> String {
let format_res = match self.state {
State::Delta => Self::delta(timestamp_us - self.prev_timestamp_us),
_ => Self::delta(0),
};
self.prev_timestamp_us = timestamp_us;
self.state = match self.state {
State::Initial if timestamp_us == 0 => State::AfterBoot,
_ => State::Delta,
};
format_res
}
fn delta(delta_us: i64) -> String {
let seconds = i64::abs(delta_us / 1000000);
let sub_seconds = i64::abs(delta_us % 1000000);
let mut res = format!("{}.{:0>6}", seconds, sub_seconds);
if delta_us < 0 {
res.insert(0, '-');
}
format!("<{:>12}>", res)
}
}
static BOOT_TIME: OnceLock<DateTime<FixedOffset>> = OnceLock::new();
#[cfg(feature = "fixed-boot-time")]
fn boot_time() -> DateTime<FixedOffset> {
*BOOT_TIME.get_or_init(|| {
let date = NaiveDate::from_ymd_opt(2024, 11, 18).unwrap();
let time = NaiveTime::from_hms_micro_opt(19, 34, 12, 866807).unwrap();
let tz = FixedOffset::east_opt(7 * 3600).unwrap();
chrono::NaiveDateTime::new(date, time)
.and_local_timezone(tz)
.unwrap()
})
}
#[cfg(not(feature = "fixed-boot-time"))]
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
fn boot_time() -> DateTime<FixedOffset> {
*BOOT_TIME.get_or_init(|| boot_time_from_utmpx().unwrap())
}
#[cfg(not(feature = "fixed-boot-time"))]
#[cfg(windows)]
fn boot_time() -> DateTime<FixedOffset> {
// TODO: get windows boot time
*BOOT_TIME.get_or_init(|| chrono::DateTime::from_timestamp(0, 0).unwrap().into())
}
#[cfg(not(feature = "fixed-boot-time"))]
#[cfg(target_os = "openbsd")]
fn boot_time() -> DateTime<FixedOffset> {
// TODO: get openbsd boot time
*BOOT_TIME.get_or_init(|| chrono::DateTime::from_timestamp(0, 0).unwrap().into())
}
#[cfg(not(feature = "fixed-boot-time"))]
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
fn boot_time_from_utmpx() -> Option<DateTime<FixedOffset>> {
for record in uucore::utmpx::Utmpx::iter_all_records() {
if record.record_type() == uucore::utmpx::BOOT_TIME {
let t = record.login_time();
return Some(
chrono::DateTime::from_timestamp(t.unix_timestamp(), t.nanosecond())
.unwrap()
.with_timezone(&chrono::Local)
.into(),
);
}
}
None
}

View File

@@ -30,3 +30,54 @@ fn test_kmsg_json() {
.no_stderr()
.stdout_is_templated_fixture("test_kmsg_json.expected", &[("\r\n", "\n")]);
}
#[test]
fn test_kmsg_time_format_delta() {
test_kmsg_time_format("delta");
}
#[test]
fn test_kmsg_time_format_reltime() {
test_kmsg_time_format("reltime");
}
#[test]
fn test_kmsg_time_format_ctime() {
test_kmsg_time_format("ctime");
}
#[test]
fn test_kmsg_time_format_notime() {
test_kmsg_time_format("notime");
}
#[test]
fn test_kmsg_time_format_iso() {
test_kmsg_time_format("iso");
}
#[test]
fn test_kmsg_time_format_raw() {
test_kmsg_time_format("raw");
}
fn test_kmsg_time_format(format: &str) {
let time_format_arg = format!("--time-format={format}");
let expected_output = format!("test_kmsg_time_format_{format}.expected");
new_ucmd!()
.arg("--kmsg-file")
.arg("kmsg.input.1")
.arg(time_format_arg)
.succeeds()
.no_stderr()
.stdout_is_templated_fixture(expected_output, &[("\r\n", "\n")]);
}
#[test]
fn test_invalid_time_format() {
new_ucmd!()
.arg("--time-format=definitely-invalid")
.fails()
.code_is(1)
.stderr_only("dmesg: unknown time format: definitely-invalid\n");
}

BIN
tests/fixtures/dmesg/kmsg.input.1 vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
[Mon Nov 18 19:34:12 2024] LOG_EMERG LOG_AUTH
[Mon Nov 18 19:34:13 2024] LOG_EMERG LOG_AUTHPRIV
[Mon Nov 18 19:34:13 2024] LOG_EMERG LOG_CRON
[Mon Nov 18 19:34:13 2024] LOG_EMERG LOG_DAEMON
[Mon Nov 18 19:35:00 2024] LOG_EMERG LOG_FTP

View File

@@ -0,0 +1,5 @@
[< 0.000000>] LOG_EMERG LOG_AUTH
[< 0.000000>] LOG_EMERG LOG_AUTHPRIV
[< -0.166667>] LOG_EMERG LOG_CRON
[< 0.666667>] LOG_EMERG LOG_DAEMON
[< 47.000000>] LOG_EMERG LOG_FTP

View File

@@ -0,0 +1,5 @@
2024-11-18T19:34:12,866807+07:00 LOG_EMERG LOG_AUTH
2024-11-18T19:34:13,366807+07:00 LOG_EMERG LOG_AUTHPRIV
2024-11-18T19:34:13,200140+07:00 LOG_EMERG LOG_CRON
2024-11-18T19:34:13,866807+07:00 LOG_EMERG LOG_DAEMON
2024-11-18T19:35:00,866807+07:00 LOG_EMERG LOG_FTP

View File

@@ -0,0 +1,5 @@
LOG_EMERG LOG_AUTH
LOG_EMERG LOG_AUTHPRIV
LOG_EMERG LOG_CRON
LOG_EMERG LOG_DAEMON
LOG_EMERG LOG_FTP

View File

@@ -0,0 +1,5 @@
[ 0.000000] LOG_EMERG LOG_AUTH
[ 0.500000] LOG_EMERG LOG_AUTHPRIV
[ 0.333333] LOG_EMERG LOG_CRON
[ 1.000000] LOG_EMERG LOG_DAEMON
[ 48.000000] LOG_EMERG LOG_FTP

View File

@@ -0,0 +1,5 @@
[Nov18 19:34] LOG_EMERG LOG_AUTH
[ +0.000000] LOG_EMERG LOG_AUTHPRIV
[ -0.166667] LOG_EMERG LOG_CRON
[ +0.666667] LOG_EMERG LOG_DAEMON
[Nov18 19:35] LOG_EMERG LOG_FTP