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 #[arg(short = 'f', long, value_delimiter = ',')]
29 facility: Vec<String>,
30
31 #[arg(short = 'H', long)]
33 human: bool,
34
35 #[arg(short = 'k', long)]
37 kernel: bool,
38
39 #[arg(short = 'l', long, value_delimiter = ',')]
41 level: Vec<String>,
42
43 #[arg(short = 'L', long, num_args = 0..=1, default_missing_value = "auto")]
45 color: Option<String>,
46
47 #[arg(short = 'r', long)]
49 raw: bool,
50
51 #[arg(short = 'T', long)]
53 ctime: bool,
54
55 #[arg(short = 't', long)]
57 notime: bool,
58
59 #[arg(short = 'u', long)]
61 userspace: bool,
62
63 #[arg(short = 'w', long)]
65 follow: bool,
66
67 #[arg(short = 'W', long = "follow-new")]
69 follow_new: bool,
70
71 #[arg(short = 'x', long)]
73 decode: bool,
74
75 #[arg(short = 'd', long = "show-delta")]
77 show_delta: bool,
78
79 #[arg(short = 'e', long)]
81 reltime: bool,
82
83 #[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 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}