Skip to main content

linuxutils_system/
dmesg.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use chrono::{Local, TimeZone};
6use clap::Parser;
7use colored::Colorize;
8use rustix::{
9    fs::{self, Mode, OFlags, SeekFrom},
10    io::Errno,
11    time::{ClockId, clock_gettime},
12};
13use std::{
14    io::{self, IsTerminal, Write},
15    process::ExitCode,
16};
17
18const KMSG_PATH: &str = "/dev/kmsg";
19
20const LEVEL_NAMES: [&str; 8] = [
21    "emerg", "alert", "crit", "err", "warn", "notice", "info", "debug",
22];
23
24#[derive(Parser)]
25#[command(name = "dmesg", about = "Print or control the kernel ring buffer")]
26pub struct Args {
27    /// Restrict output to the given comma-separated list of facilities
28    #[arg(short = 'f', long, value_delimiter = ',')]
29    facility: Vec<String>,
30
31    /// Enable human-readable output (implies --color and --reltime)
32    #[arg(short = 'H', long)]
33    human: bool,
34
35    /// Print only kernel messages
36    #[arg(short = 'k', long)]
37    kernel: bool,
38
39    /// Restrict output to the given comma-separated list of levels
40    #[arg(short = 'l', long, value_delimiter = ',')]
41    level: Vec<String>,
42
43    /// Colorize the output (auto, always, never)
44    #[arg(short = 'L', long, num_args = 0..=1, default_missing_value = "auto")]
45    color: Option<String>,
46
47    /// Print raw message buffer (with priority prefix)
48    #[arg(short = 'r', long)]
49    raw: bool,
50
51    /// Display human-readable timestamps
52    #[arg(short = 'T', long)]
53    ctime: bool,
54
55    /// Do not print timestamps
56    #[arg(short = 't', long)]
57    notime: bool,
58
59    /// Print only userspace messages
60    #[arg(short = 'u', long)]
61    userspace: bool,
62
63    /// Wait for new messages
64    #[arg(short = 'w', long)]
65    follow: bool,
66
67    /// Wait for and print only new messages
68    #[arg(short = 'W', long = "follow-new")]
69    follow_new: bool,
70
71    /// Decode facility and priority to human-readable prefixes
72    #[arg(short = 'x', long)]
73    decode: bool,
74
75    /// Show time delta between messages
76    #[arg(short = 'd', long = "show-delta")]
77    show_delta: bool,
78
79    /// Display local time and delta in human-readable format
80    #[arg(short = 'e', long)]
81    reltime: bool,
82
83    /// Time format: raw, ctime, reltime, delta, notime, iso
84    #[arg(long = "time-format")]
85    time_format: Option<String>,
86}
87
88#[derive(Clone, Copy, PartialEq)]
89enum TimeFormat {
90    Raw,
91    Ctime,
92    Iso,
93    Notime,
94    Delta,
95    Reltime,
96}
97
98struct KmsgRecord {
99    priority: u32,
100    _sequence: u64,
101    timestamp_us: u64,
102    message: String,
103}
104
105impl KmsgRecord {
106    fn level(&self) -> u8 {
107        (self.priority & 0x7) as u8
108    }
109    fn facility(&self) -> u8 {
110        (self.priority >> 3) as u8
111    }
112}
113
114fn facility_name(code: u8) -> &'static str {
115    match code {
116        0 => "kern",
117        1 => "user",
118        2 => "mail",
119        3 => "daemon",
120        4 => "auth",
121        5 => "syslog",
122        6 => "lpr",
123        7 => "news",
124        8 => "uucp",
125        9 => "cron",
126        10 => "authpriv",
127        11 => "ftp",
128        16 => "local0",
129        17 => "local1",
130        18 => "local2",
131        19 => "local3",
132        20 => "local4",
133        21 => "local5",
134        22 => "local6",
135        23 => "local7",
136        _ => "unknown",
137    }
138}
139
140fn parse_level_name(name: &str) -> Option<u8> {
141    LEVEL_NAMES
142        .iter()
143        .position(|&n| n == name.to_ascii_lowercase())
144        .map(|i| i as u8)
145}
146
147fn parse_facility_name(name: &str) -> Option<u8> {
148    match name.to_ascii_lowercase().as_str() {
149        "kern" => Some(0),
150        "user" => Some(1),
151        "mail" => Some(2),
152        "daemon" => Some(3),
153        "auth" => Some(4),
154        "syslog" => Some(5),
155        "lpr" => Some(6),
156        "news" => Some(7),
157        "uucp" => Some(8),
158        "cron" => Some(9),
159        "authpriv" => Some(10),
160        "ftp" => Some(11),
161        "local0" => Some(16),
162        "local1" => Some(17),
163        "local2" => Some(18),
164        "local3" => Some(19),
165        "local4" => Some(20),
166        "local5" => Some(21),
167        "local6" => Some(22),
168        "local7" => Some(23),
169        _ => None,
170    }
171}
172
173fn parse_kmsg_record(data: &[u8]) -> Option<KmsgRecord> {
174    let text = std::str::from_utf8(data).ok()?;
175    let text = text.trim_end();
176    let (header, message) = text.split_once(';')?;
177    let mut parts = header.splitn(4, ',');
178
179    let priority: u32 = parts.next()?.parse().ok()?;
180    let sequence: u64 = parts.next()?.parse().ok()?;
181    let timestamp_us: u64 = parts.next()?.parse().ok()?;
182    let _flags = parts.next()?;
183
184    // Take first line only (skip continuation/metadata lines)
185    let message = message.lines().next().unwrap_or("").to_string();
186
187    Some(KmsgRecord {
188        priority,
189        _sequence: sequence,
190        timestamp_us,
191        message,
192    })
193}
194
195fn boot_time_offset_us() -> i64 {
196    let rt = clock_gettime(ClockId::Realtime);
197    let bt = clock_gettime(ClockId::Boottime);
198    let rt_us = rt.tv_sec as i64 * 1_000_000 + rt.tv_nsec as i64 / 1000;
199    let bt_us = bt.tv_sec as i64 * 1_000_000 + bt.tv_nsec as i64 / 1000;
200    rt_us - bt_us
201}
202
203fn format_timestamp(
204    timestamp_us: u64,
205    format: TimeFormat,
206    offset_us: i64,
207    prev_timestamp_us: Option<u64>,
208    use_color: bool,
209) -> String {
210    let colorize = |s: String| -> String {
211        if use_color { s.green().to_string() } else { s }
212    };
213
214    match format {
215        TimeFormat::Notime => String::new(),
216        TimeFormat::Raw => {
217            let secs = timestamp_us / 1_000_000;
218            let usecs = timestamp_us % 1_000_000;
219            colorize(format!("[{secs:>5}.{usecs:06}] "))
220        }
221        TimeFormat::Ctime => {
222            let wall_us = offset_us + timestamp_us as i64;
223            let secs = wall_us / 1_000_000;
224            let nsecs = ((wall_us % 1_000_000) * 1000) as u32;
225            let dt = Local.timestamp_opt(secs, nsecs).unwrap();
226            colorize(format!("[{}] ", dt.format("%a %b %e %H:%M:%S %Y")))
227        }
228        TimeFormat::Iso => {
229            let wall_us = offset_us + timestamp_us as i64;
230            let secs = wall_us / 1_000_000;
231            let nsecs = ((wall_us % 1_000_000) * 1000) as u32;
232            let dt = Local.timestamp_opt(secs, nsecs).unwrap();
233            colorize(format!("{} ", dt.format("%Y-%m-%dT%H:%M:%S,%6f%:z")))
234        }
235        TimeFormat::Delta => {
236            let secs = timestamp_us / 1_000_000;
237            let usecs = timestamp_us % 1_000_000;
238            let delta = prev_timestamp_us
239                .map(|prev| timestamp_us.saturating_sub(prev))
240                .unwrap_or(0);
241            let d_secs = delta / 1_000_000;
242            let d_usecs = delta % 1_000_000;
243            colorize(format!(
244                "[{secs:>5}.{usecs:06} <{d_secs:>5}.{d_usecs:06}>] "
245            ))
246        }
247        TimeFormat::Reltime => {
248            match prev_timestamp_us
249                .map(|prev| timestamp_us.saturating_sub(prev))
250            {
251                Some(d) if d < 60_000_000 => {
252                    let d_secs = d / 1_000_000;
253                    let d_usecs = d % 1_000_000;
254                    colorize(format!("[  +{d_secs:>3}.{d_usecs:06}] "))
255                }
256                _ => {
257                    let wall_us = offset_us + timestamp_us as i64;
258                    let secs = wall_us / 1_000_000;
259                    let nsecs = ((wall_us % 1_000_000) * 1000) as u32;
260                    let dt = Local.timestamp_opt(secs, nsecs).unwrap();
261                    let ts = format!("[{}] ", dt.format("%b%e %H:%M"));
262                    if use_color { ts.cyan().to_string() } else { ts }
263                }
264            }
265        }
266    }
267}
268
269fn color_message(level: u8, msg: &str, use_color: bool) -> String {
270    if !use_color {
271        return msg.to_string();
272    }
273    if msg.contains("segfault") {
274        return msg.red().bold().to_string();
275    }
276    match level {
277        0..=2 => msg.red().bold().to_string(),
278        3 => msg.red().to_string(),
279        4 => msg.yellow().to_string(),
280        _ => color_subsystem(msg),
281    }
282}
283
284fn color_subsystem(msg: &str) -> String {
285    if let Some(idx) = msg.find(": ") {
286        let (subsys, rest) = msg.split_at(idx + 1);
287        format!("{}{}", subsys.yellow(), rest)
288    } else {
289        msg.to_string()
290    }
291}
292
293fn build_level_filter(args: &Args) -> Vec<u8> {
294    let mut levels = Vec::new();
295    for l in &args.level {
296        if let Some(base) = l.strip_suffix('+') {
297            if let Some(idx) = parse_level_name(base) {
298                for i in 0..=idx {
299                    if !levels.contains(&i) {
300                        levels.push(i);
301                    }
302                }
303            }
304        } else if let Some(idx) = parse_level_name(l)
305            && !levels.contains(&idx)
306        {
307            levels.push(idx);
308        }
309    }
310    levels
311}
312
313fn build_facility_filter(args: &Args) -> Vec<u8> {
314    let mut facilities = Vec::new();
315    if args.kernel {
316        facilities.push(0);
317    }
318    if args.userspace {
319        for code in 1..=23u8 {
320            if !facilities.contains(&code) {
321                facilities.push(code);
322            }
323        }
324    }
325    for f in &args.facility {
326        if let Some(code) = parse_facility_name(f)
327            && !facilities.contains(&code)
328        {
329            facilities.push(code);
330        }
331    }
332    facilities
333}
334
335fn matches_filters(
336    record: &KmsgRecord,
337    level_filter: &[u8],
338    facility_filter: &[u8],
339) -> bool {
340    if !level_filter.is_empty() && !level_filter.contains(&record.level()) {
341        return false;
342    }
343    if !facility_filter.is_empty()
344        && !facility_filter.contains(&record.facility())
345    {
346        return false;
347    }
348    true
349}
350
351struct OutputOpts {
352    time_format: TimeFormat,
353    offset_us: i64,
354    decode: bool,
355    raw: bool,
356    use_color: bool,
357}
358
359fn print_record(
360    out: &mut impl Write,
361    record: &KmsgRecord,
362    opts: &OutputOpts,
363    prev_timestamp_us: Option<u64>,
364) {
365    if opts.raw {
366        let _ = writeln!(out, "<{}>{}", record.priority, record.message);
367        return;
368    }
369
370    let mut line = String::new();
371
372    if opts.decode {
373        let fac = facility_name(record.facility());
374        let lvl = LEVEL_NAMES[record.level() as usize];
375        line.push_str(&format!("{fac:<6}:{lvl:<6}: "));
376    }
377
378    line.push_str(&format_timestamp(
379        record.timestamp_us,
380        opts.time_format,
381        opts.offset_us,
382        prev_timestamp_us,
383        opts.use_color,
384    ));
385
386    line.push_str(&color_message(
387        record.level(),
388        &record.message,
389        opts.use_color,
390    ));
391
392    let _ = writeln!(out, "{line}");
393}
394
395pub fn run(args: Args) -> ExitCode {
396    let use_color = match args.color.as_deref() {
397        Some("never") => false,
398        Some("always") => true,
399        _ if args.human => io::stdout().is_terminal(),
400        Some(_) => io::stdout().is_terminal(),
401        None => io::stdout().is_terminal(),
402    };
403    colored::control::set_override(use_color);
404
405    let time_format = if args.notime {
406        TimeFormat::Notime
407    } else if let Some(ref fmt) = args.time_format {
408        match fmt.as_str() {
409            "raw" => TimeFormat::Raw,
410            "ctime" => TimeFormat::Ctime,
411            "iso" => TimeFormat::Iso,
412            "notime" => TimeFormat::Notime,
413            "delta" => TimeFormat::Delta,
414            "reltime" => TimeFormat::Reltime,
415            other => {
416                eprintln!("dmesg: unknown time format: {other}");
417                return ExitCode::FAILURE;
418            }
419        }
420    } else if args.human || args.reltime {
421        TimeFormat::Reltime
422    } else if args.ctime {
423        TimeFormat::Ctime
424    } else if args.show_delta {
425        TimeFormat::Delta
426    } else {
427        TimeFormat::Raw
428    };
429
430    let level_filter = build_level_filter(&args);
431    let facility_filter = build_facility_filter(&args);
432
433    let fd = match fs::open(
434        KMSG_PATH,
435        OFlags::RDONLY | OFlags::NONBLOCK,
436        Mode::empty(),
437    ) {
438        Ok(fd) => fd,
439        Err(e) => {
440            eprintln!(
441                "dmesg: failed to open {KMSG_PATH}: {}",
442                io::Error::from(e)
443            );
444            return ExitCode::FAILURE;
445        }
446    };
447
448    if args.follow_new
449        && let Err(e) = fs::seek(&fd, SeekFrom::End(0))
450    {
451        eprintln!("dmesg: seek error: {}", io::Error::from(e));
452        return ExitCode::FAILURE;
453    }
454
455    let opts = OutputOpts {
456        time_format,
457        offset_us: boot_time_offset_us(),
458        decode: args.decode,
459        raw: args.raw,
460        use_color,
461    };
462    let mut prev_timestamp_us: Option<u64> = None;
463    let mut buf = [0u8; 8192];
464    let stdout = io::stdout();
465    let mut out = stdout.lock();
466    let following = args.follow || args.follow_new;
467
468    loop {
469        match rustix::io::read(&fd, &mut buf) {
470            Ok(0) => break,
471            Ok(n) => {
472                if let Some(record) = parse_kmsg_record(&buf[..n]) {
473                    if matches_filters(&record, &level_filter, &facility_filter)
474                    {
475                        print_record(
476                            &mut out,
477                            &record,
478                            &opts,
479                            prev_timestamp_us,
480                        );
481                    }
482                    prev_timestamp_us = Some(record.timestamp_us);
483                }
484            }
485            Err(Errno::AGAIN) => break,
486            Err(Errno::INTR) => continue,
487            Err(e) => {
488                eprintln!("dmesg: read error: {}", io::Error::from(e));
489                return ExitCode::FAILURE;
490            }
491        }
492    }
493
494    if following {
495        let flags = fs::fcntl_getfl(&fd).unwrap_or(OFlags::empty());
496        if let Err(e) = fs::fcntl_setfl(&fd, flags.difference(OFlags::NONBLOCK))
497        {
498            eprintln!(
499                "dmesg: failed to set blocking mode: {}",
500                io::Error::from(e)
501            );
502            return ExitCode::FAILURE;
503        }
504
505        loop {
506            match rustix::io::read(&fd, &mut buf) {
507                Ok(0) => break,
508                Ok(n) => {
509                    if let Some(record) = parse_kmsg_record(&buf[..n]) {
510                        if matches_filters(
511                            &record,
512                            &level_filter,
513                            &facility_filter,
514                        ) {
515                            print_record(
516                                &mut out,
517                                &record,
518                                &opts,
519                                prev_timestamp_us,
520                            );
521                            let _ = out.flush();
522                        }
523                        prev_timestamp_us = Some(record.timestamp_us);
524                    }
525                }
526                Err(Errno::INTR) => continue,
527                Err(e) => {
528                    eprintln!("dmesg: read error: {}", io::Error::from(e));
529                    return ExitCode::FAILURE;
530                }
531            }
532        }
533    }
534
535    ExitCode::SUCCESS
536}