1use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
16
17use appliance_pid1::net::{align4, netlink_message, netlink_socket};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
24pub struct CpuTicks {
25 pub busy: u64,
26 pub total: u64,
27}
28
29impl CpuTicks {
30 pub fn busy_percent_since(self, earlier: CpuTicks) -> Option<u8> {
34 let total = self.total.checked_sub(earlier.total)?;
35 if total == 0 {
36 return None;
37 }
38 let busy = self.busy.saturating_sub(earlier.busy);
39 Some((busy * 100 / total).min(100) as u8)
40 }
41}
42
43pub fn parse_stat(text: &str) -> (Option<CpuTicks>, usize) {
46 let mut ticks = None;
47 let mut cpus = 0;
48 for line in text.lines() {
49 let mut parts = line.split_ascii_whitespace();
50 let Some(name) = parts.next() else { continue };
51 if name == "cpu" {
52 let v: Vec<u64> = parts.filter_map(|p| p.parse().ok()).collect();
53 if v.len() >= 4 {
55 let total: u64 = v.iter().sum();
56 let idle = v[3] + v.get(4).copied().unwrap_or(0);
57 ticks = Some(CpuTicks {
58 busy: total.saturating_sub(idle),
59 total,
60 });
61 }
62 } else if name.starts_with("cpu") && name[3..].chars().all(|c| c.is_ascii_digit()) {
63 cpus += 1;
64 }
65 }
66 (ticks, cpus)
67}
68
69pub fn parse_meminfo(text: &str) -> Option<(u64, u64)> {
71 let mut total = None;
72 let mut avail = None;
73 for line in text.lines() {
74 let (key, rest) = line.split_once(':')?;
75 let kib: u64 = rest.split_ascii_whitespace().next()?.parse().ok()?;
76 match key {
77 "MemTotal" => total = Some(kib * 1024),
78 "MemAvailable" => avail = Some(kib * 1024),
79 _ => {}
80 }
81 if total.is_some() && avail.is_some() {
82 break;
83 }
84 }
85 Some((total?, avail?))
86}
87
88pub fn parse_uptime(text: &str) -> Option<u64> {
90 let first = text.split_ascii_whitespace().next()?;
91 let secs: f64 = first.parse().ok()?;
92 Some(secs as u64)
93}
94
95pub fn parse_loadavg(text: &str) -> Option<String> {
97 text.split_ascii_whitespace().next().map(str::to_owned)
98}
99
100pub fn parse_cpu_model(text: &str) -> Option<String> {
108 let line = text.lines().find(|l| l.starts_with("model name"))?;
109 let (_, value) = line.split_once(':')?;
110 let mut out = String::new();
111 for word in value.split_ascii_whitespace() {
112 let w = word
113 .replace("(R)", "")
114 .replace("(TM)", "")
115 .replace("(tm)", "");
116 if w.is_empty()
117 || w == "CPU"
118 || w == "Processor"
119 || w == "@"
120 || w.ends_with("GHz")
121 || is_core_count(&w)
122 {
123 continue;
124 }
125 if !out.is_empty() {
126 out.push(' ');
127 }
128 out.push_str(&w);
129 }
130 if out.is_empty() {
131 None
132 } else {
133 Some(out)
134 }
135}
136
137fn is_core_count(word: &str) -> bool {
139 let Some(digits) = word
140 .strip_suffix("-Cores")
141 .or_else(|| word.strip_suffix("-Core"))
142 else {
143 return false;
144 };
145 !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit())
146}
147
148#[derive(Debug, Clone, PartialEq, Eq)]
152pub struct MountFacts {
153 pub source: String,
155 pub fstype: String,
157}
158
159pub fn parse_mounts(text: &str, mountpoint: &str) -> Option<MountFacts> {
163 let mut found = None;
164 for line in text.lines() {
165 let mut f = line.split_ascii_whitespace();
166 let (Some(source), Some(target), Some(fstype)) = (f.next(), f.next(), f.next()) else {
167 continue;
168 };
169 if unescape_mount(target) == mountpoint {
170 found = Some(MountFacts {
171 source: unescape_mount(source),
172 fstype: fstype.to_owned(),
173 });
174 }
175 }
176 found
177}
178
179fn unescape_mount(s: &str) -> String {
180 let b = s.as_bytes();
181 let mut out = Vec::with_capacity(b.len());
182 let mut i = 0;
183 while i < b.len() {
184 if b[i] == b'\\'
185 && i + 3 < b.len()
186 && b[i + 1..i + 4].iter().all(|c| (b'0'..=b'7').contains(c))
187 {
188 let v = (b[i + 1] - b'0') * 64 + (b[i + 2] - b'0') * 8 + (b[i + 3] - b'0');
189 out.push(v);
190 i += 4;
191 } else {
192 out.push(b[i]);
193 i += 1;
194 }
195 }
196 String::from_utf8_lossy(&out).into_owned()
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub struct SetFacts {
202 pub level: String,
204 pub members: usize,
206 pub member_bytes: u64,
210 pub chunk_bytes: u64,
212}
213
214pub fn read_set_facts(block_dir: &std::path::Path) -> Option<SetFacts> {
220 let md = block_dir.join("md");
221 let text = |name: &str| {
222 std::fs::read_to_string(md.join(name))
223 .ok()
224 .map(|t| t.trim().to_owned())
225 };
226 let level = text("level").filter(|l| !l.is_empty())?;
227 let members: usize = text("raid_disks")?.parse().ok().filter(|&n| n > 0)?;
228 let chunk_bytes: u64 = text("chunk_size").and_then(|c| c.parse().ok()).unwrap_or(0);
229 let mut smallest: Option<u64> = None;
230 for entry in std::fs::read_dir(&md).ok()? {
231 let Ok(entry) = entry else { continue };
232 let name = entry.file_name();
233 let Some(dev) = name.to_str().and_then(|n| n.strip_prefix("dev-")) else {
234 continue;
235 };
236 let kib = std::fs::read_to_string(entry.path().join("size"))
237 .ok()
238 .and_then(|t| t.trim().parse::<u64>().ok())
239 .map(|k| k * 1024)
240 .or_else(|| {
241 std::fs::read_to_string(block_dir.join("../../class/block").join(dev).join("size"))
243 .ok()
244 .and_then(|t| t.trim().parse::<u64>().ok())
245 .map(|s| s * 512)
246 });
247 if let Some(b) = kib.filter(|&b| b > 0) {
248 smallest = Some(smallest.map_or(b, |s| s.min(b)));
249 }
250 }
251 Some(SetFacts {
252 level,
253 members,
254 member_bytes: smallest?,
255 chunk_bytes,
256 })
257}
258
259#[derive(Debug, Clone, PartialEq, Eq)]
262pub struct DiskKind {
263 pub fs: String,
265 pub set: Option<SetFacts>,
266}
267
268impl DiskKind {
269 pub fn from_mount(mount: &MountFacts, set: Option<SetFacts>) -> Self {
270 let fs = match mount.fstype.as_str() {
271 "vfat" | "msdos" | "fat" => "fat".to_owned(),
272 other => other.to_owned(),
273 };
274 DiskKind { fs, set }
275 }
276}
277
278impl std::fmt::Display for DiskKind {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 if let Some(set) = &self.set {
282 write!(
283 f,
284 "{}\u{d7}{} {} ",
285 set.members,
286 crate::dashboard::fmt_bytes(set.member_bytes),
287 set.level
288 )?;
289 }
290 f.write_str(&self.fs)
291 }
292}
293
294fn sysfs_block_dir_of(source: &str) -> Option<std::path::PathBuf> {
298 use std::os::unix::fs::{FileTypeExt as _, MetadataExt as _};
299 let meta = std::fs::metadata(source).ok()?;
300 if !meta.file_type().is_block_device() {
301 return None;
302 }
303 let rdev = meta.rdev();
304 let dir = std::path::PathBuf::from(format!(
305 "/sys/dev/block/{}:{}",
306 rustix::fs::major(rdev),
307 rustix::fs::minor(rdev)
308 ));
309 dir.is_dir().then_some(dir)
310}
311
312pub fn disk_kind(mountpoint: &std::path::Path) -> Option<DiskKind> {
316 let mount = parse_mounts(&read("/proc/mounts")?, mountpoint.to_str()?)?;
317 let set = sysfs_block_dir_of(&mount.source).and_then(|d| read_set_facts(&d));
318 Some(DiskKind::from_mount(&mount, set))
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct IfAddr {
324 pub interface: String,
325 pub address: IpAddr,
326 pub prefix: u8,
327}
328
329impl std::fmt::Display for IfAddr {
330 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331 write!(f, "{} {}/{}", self.interface, self.address, self.prefix)
332 }
333}
334
335const RTM_GETADDR: u16 = 22;
338const RTM_NEWADDR: u16 = 20;
339const NLMSG_DONE: u16 = 3;
340const NLMSG_ERROR: u16 = 2;
341const NLM_F_REQUEST: u16 = 0x0001;
342const NLM_F_DUMP: u16 = 0x0300;
343const NLM_F_MULTI: u16 = 0x0002;
344const AF_INET: u8 = 2;
345const AF_INET6: u8 = 10;
346const IFA_ADDRESS: u16 = 1;
347const IFA_LOCAL: u16 = 2;
348const IFA_LABEL: u16 = 3;
349const RT_SCOPE_HOST: u8 = 254;
351const RT_SCOPE_LINK: u8 = 253;
353
354pub fn getaddr_dump_message(seq: u32) -> Vec<u8> {
356 netlink_message(RTM_GETADDR, NLM_F_REQUEST | NLM_F_DUMP, seq, &[0u8; 8])
359}
360
361pub fn parse_getaddr_reply(
368 buf: &[u8],
369 resolve: &mut dyn FnMut(u32) -> Option<String>,
370) -> (Vec<IfAddr>, bool) {
371 let mut out = Vec::new();
372 let mut off = 0usize;
373 let mut done = false;
374 while off + 16 <= buf.len() {
375 let len = u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]]) as usize;
376 let kind = u16::from_le_bytes([buf[off + 4], buf[off + 5]]);
377 let flags = u16::from_le_bytes([buf[off + 6], buf[off + 7]]);
378 if len < 16 || off + len > buf.len() {
379 break;
380 }
381 match kind {
382 NLMSG_DONE => {
383 done = true;
384 break;
385 }
386 NLMSG_ERROR => {
387 done = true;
388 break;
389 }
390 RTM_NEWADDR if len >= 24 => {
391 let body = &buf[off + 16..off + len];
392 let family = body[0];
393 let prefix = body[1];
394 let scope = body[3];
395 let index = u32::from_le_bytes([body[4], body[5], body[6], body[7]]);
396 if scope != RT_SCOPE_HOST && scope != RT_SCOPE_LINK {
397 let mut address = None;
398 let mut local = None;
399 let mut label = None;
400 let mut a = 8usize;
401 while a + 4 <= body.len() {
402 let alen = u16::from_le_bytes([body[a], body[a + 1]]) as usize;
403 let atype = u16::from_le_bytes([body[a + 2], body[a + 3]]);
404 if alen < 4 || a + alen > body.len() {
405 break;
406 }
407 let payload = &body[a + 4..a + alen];
408 match atype {
409 IFA_ADDRESS => address = ip_from(family, payload),
410 IFA_LOCAL => local = ip_from(family, payload),
411 IFA_LABEL => {
412 let end = payload
413 .iter()
414 .position(|&b| b == 0)
415 .unwrap_or(payload.len());
416 label = String::from_utf8(payload[..end].to_vec()).ok();
417 }
418 _ => {}
419 }
420 a += align4(alen);
421 }
422 if let Some(ip) = local.or(address) {
426 let interface = label
427 .or_else(|| resolve(index))
428 .unwrap_or_else(|| format!("if{index}"));
429 out.push(IfAddr {
430 interface,
431 address: ip,
432 prefix,
433 });
434 }
435 }
436 }
437 _ => {}
438 }
439 if flags & NLM_F_MULTI == 0 {
440 done = true;
442 }
443 off += align4(len);
444 }
445 (out, done)
446}
447
448fn ip_from(family: u8, payload: &[u8]) -> Option<IpAddr> {
449 match (family, payload.len()) {
450 (AF_INET, 4) => Some(IpAddr::V4(Ipv4Addr::new(
451 payload[0], payload[1], payload[2], payload[3],
452 ))),
453 (AF_INET6, 16) => {
454 let mut b = [0u8; 16];
455 b.copy_from_slice(payload);
456 Some(IpAddr::V6(Ipv6Addr::from(b)))
457 }
458 _ => None,
459 }
460}
461
462pub fn addresses() -> Vec<IfAddr> {
465 use rustix::net::{
466 netdevice, recv, send, socket, AddressFamily, RecvFlags, SendFlags, SocketType,
467 };
468 let Ok(sock) = netlink_socket() else {
469 return Vec::new();
470 };
471 if send(&sock, &getaddr_dump_message(1), SendFlags::empty()).is_err() {
472 return Vec::new();
473 }
474 let probe = socket(AddressFamily::INET, SocketType::DGRAM, None).ok();
475 let mut resolve = |index: u32| -> Option<String> {
476 let probe = probe.as_ref()?;
477 netdevice::index_to_name(probe, index).ok()
478 };
479 let mut out = Vec::new();
480 let mut buf = vec![0u8; 16384];
481 for _ in 0..64 {
483 let Ok((n, _)) = recv(&sock, &mut buf[..], RecvFlags::empty()) else {
484 break;
485 };
486 if n == 0 {
487 break;
488 }
489 let (mut got, done) = parse_getaddr_reply(&buf[..n], &mut resolve);
490 out.append(&mut got);
491 if done {
492 break;
493 }
494 }
495 out
496}
497
498fn read(path: &str) -> Option<String> {
501 std::fs::read_to_string(path).ok()
502}
503
504pub fn hostname() -> Option<String> {
505 let h = read("/proc/sys/kernel/hostname")?;
506 let h = h.trim();
507 if h.is_empty() || h == "(none)" {
508 None
509 } else {
510 Some(h.to_owned())
511 }
512}
513
514pub fn cpu_ticks() -> (Option<CpuTicks>, usize) {
515 read("/proc/stat")
516 .map(|t| parse_stat(&t))
517 .unwrap_or((None, 0))
518}
519
520pub fn memory() -> Option<(u64, u64)> {
521 meminfo().ok()
522}
523
524pub fn meminfo() -> Result<(u64, u64), String> {
528 let text = read_why("/proc/meminfo")?;
529 parse_meminfo(&text)
530 .ok_or_else(|| "/proc/meminfo has no MemTotal and MemAvailable lines to parse".to_owned())
531}
532
533pub fn load1_value() -> Result<f64, String> {
536 let text = read_why("/proc/loadavg")?;
537 parse_loadavg(&text)
538 .and_then(|first| first.parse::<f64>().ok())
539 .ok_or_else(|| "/proc/loadavg does not start with a number".to_owned())
540}
541
542fn read_why(path: &str) -> Result<String, String> {
545 std::fs::read_to_string(path).map_err(|err| format!("{path} could not be read ({err})"))
546}
547
548pub fn uptime_secs() -> Option<u64> {
549 parse_uptime(&read("/proc/uptime")?)
550}
551
552pub fn load1() -> Option<String> {
553 parse_loadavg(&read("/proc/loadavg")?)
554}
555
556pub fn cpu_model() -> Option<String> {
557 parse_cpu_model(&read("/proc/cpuinfo")?)
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
563pub struct IndexFacts {
564 pub git_dir: std::path::PathBuf,
565 pub snap: crate::storage::IndexSnapshot,
566}
567
568pub fn index_progress(engine: crate::storage::Engine) -> Vec<IndexFacts> {
571 engine
572 .index_progress()
573 .into_iter()
574 .map(|(git_dir, snap)| IndexFacts { git_dir, snap })
575 .collect()
576}
577
578pub fn disk(path: &std::path::Path) -> Option<(u64, u64)> {
580 let st = rustix::fs::statfs(path).ok()?;
581 let bsize = st.f_frsize as u64;
582 Some((st.f_blocks * bsize, st.f_bavail * bsize))
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use appliance_pid1::net::rtattr;
589
590 const STAT: &str = "cpu 4705 0 3540 178540 320 0 87 0 0 0\ncpu0 2000 0 1700 89000 200 0 40 0 0 0\ncpu1 2705 0 1840 89540 120 0 47 0 0 0\nintr 12345\nctxt 1\n";
591
592 #[test]
593 fn stat_counts_cpus_and_turns_two_reads_into_a_busy_percent() {
594 let (t, cpus) = parse_stat(STAT);
595 assert_eq!(cpus, 2);
596 let t = t.unwrap();
597 assert_eq!(t.total, 4705 + 3540 + 178540 + 320 + 87);
598 assert_eq!(t.busy, 4705 + 3540 + 87);
599 let later = CpuTicks {
600 busy: t.busy + 50,
601 total: t.total + 200,
602 };
603 assert_eq!(later.busy_percent_since(t), Some(25));
604 assert_eq!(
605 t.busy_percent_since(t),
606 None,
607 "an empty interval is no percentage"
608 );
609 assert_eq!(
610 t.busy_percent_since(later),
611 None,
612 "a wrapped/earlier read is refused, not negative"
613 );
614 }
615
616 #[test]
617 fn meminfo_gives_total_and_available_in_bytes() {
618 let text = "MemTotal: 16303748 kB\nMemFree: 1234 kB\nMemAvailable: 15000000 kB\nBuffers: 1 kB\n";
619 assert_eq!(
620 parse_meminfo(text),
621 Some((16303748 * 1024, 15000000 * 1024))
622 );
623 assert_eq!(
624 parse_meminfo("MemTotal: 1 kB\n"),
625 None,
626 "a file without MemAvailable is not guessed at"
627 );
628 }
629
630 #[test]
631 fn uptime_load_and_model_parse_the_real_shapes() {
632 assert_eq!(parse_uptime("350735.47 234388.90\n"), Some(350735));
633 assert_eq!(
634 parse_loadavg("0.08 0.03 0.01 1/412 12345\n"),
635 Some("0.08".into())
636 );
637 assert_eq!(parse_cpu_model("processor: 0\n"), None);
638 }
639
640 #[test]
644 fn the_cpu_model_is_trimmed_of_suffixes_that_are_not_the_chip_name() {
645 let table = [
646 ("AMD EPYC 7542 32-Core Processor", "AMD EPYC 7542"),
647 (
648 "AMD Ryzen Threadripper PRO 3975WX 32-Cores",
649 "AMD Ryzen Threadripper PRO 3975WX",
650 ),
651 (
652 "Intel(R) Xeon(R) CPU E5-2680 v4 @ 2.40GHz",
653 "Intel Xeon E5-2680 v4",
654 ),
655 (
656 "Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz",
657 "Intel Core i7-8650U",
658 ),
659 (
660 "Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz",
661 "Intel Xeon Platinum 8375C",
662 ),
663 ("QEMU Virtual CPU version 2.5+", "QEMU Virtual version 2.5+"),
664 ("ARM Cortex-A72", "ARM Cortex-A72"),
665 ];
666 for (raw, want) in table {
667 let cpuinfo = format!("processor\t: 0\nvendor_id\t: x\nmodel name\t: {raw}\n");
668 assert_eq!(parse_cpu_model(&cpuinfo).as_deref(), Some(want), "{raw}");
669 }
670 assert!(
671 is_core_count("32-Core")
672 && is_core_count("64-Cores")
673 && !is_core_count("-Core")
674 && !is_core_count("Cortex-A72")
675 );
676 }
677
678 fn newaddr(
681 family: u8,
682 prefix: u8,
683 scope: u8,
684 index: u32,
685 addr: &[u8],
686 label: Option<&str>,
687 ) -> Vec<u8> {
688 let mut body = vec![family, prefix, 0, scope];
689 body.extend_from_slice(&index.to_le_bytes());
690 body.extend_from_slice(&rtattr(IFA_ADDRESS, addr));
691 if let Some(l) = label {
692 let mut z = l.as_bytes().to_vec();
693 z.push(0);
694 body.extend_from_slice(&rtattr(IFA_LABEL, &z));
695 }
696 netlink_message(RTM_NEWADDR, NLM_F_MULTI, 1, &body)
697 }
698
699 fn done() -> Vec<u8> {
700 netlink_message(NLMSG_DONE, NLM_F_MULTI, 1, &[0u8; 4])
701 }
702
703 #[test]
704 fn the_dump_request_is_a_request_plus_dump_over_an_empty_ifaddrmsg() {
705 let m = getaddr_dump_message(7);
706 assert_eq!(m.len(), 24);
707 assert_eq!(u16::from_le_bytes([m[4], m[5]]), RTM_GETADDR);
708 assert_eq!(u16::from_le_bytes([m[6], m[7]]), NLM_F_REQUEST | NLM_F_DUMP);
709 assert_eq!(&m[16..], &[0u8; 8]);
710 }
711
712 #[test]
713 fn a_dump_reply_yields_the_dialable_addresses_and_skips_loopback_and_link_local() {
714 let mut buf = Vec::new();
715 buf.extend(newaddr(
716 AF_INET,
717 8,
718 RT_SCOPE_HOST,
719 1,
720 &[127, 0, 0, 1],
721 Some("lo"),
722 ));
723 buf.extend(newaddr(AF_INET, 24, 0, 2, &[10, 0, 2, 15], Some("eth0")));
724 buf.extend(newaddr(
725 AF_INET6,
726 64,
727 RT_SCOPE_LINK,
728 2,
729 &[0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
730 None,
731 ));
732 buf.extend(newaddr(
733 AF_INET6,
734 64,
735 0,
736 2,
737 &[0x20, 0x01, 0xd, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
738 None,
739 ));
740 buf.extend(done());
741 let mut resolved = Vec::new();
742 let (addrs, finished) = parse_getaddr_reply(&buf, &mut |i| {
743 resolved.push(i);
744 Some(format!("if{i}-resolved"))
745 });
746 assert!(finished);
747 assert_eq!(
748 addrs.iter().map(|a| a.to_string()).collect::<Vec<_>>(),
749 ["eth0 10.0.2.15/24", "if2-resolved 2001:db8::1/64"]
750 );
751 assert_eq!(
752 resolved,
753 [2],
754 "only the label-less address asked for a name"
755 );
756 }
757
758 #[test]
759 fn a_reply_without_done_says_so_and_a_truncated_one_stops_cleanly() {
760 let full = newaddr(AF_INET, 24, 0, 2, &[10, 0, 2, 15], Some("eth0"));
761 let (addrs, finished) = parse_getaddr_reply(&full, &mut |_| None);
762 assert_eq!(addrs.len(), 1);
763 assert!(!finished, "multipart without DONE: more datagrams follow");
764 let (addrs, _) = parse_getaddr_reply(&full[..full.len() - 3], &mut |_| None);
765 assert!(
766 addrs.is_empty(),
767 "a message whose declared length overruns the buffer is refused"
768 );
769 }
770
771 #[test]
772 fn the_live_readers_answer_on_this_box() {
773 let (ticks, cpus) = cpu_ticks();
776 assert!(ticks.is_some() && cpus >= 1);
777 let (total, avail) = memory().expect("meminfo");
778 assert!(total >= avail && total > 0);
779 assert!(uptime_secs().is_some());
780 let (dt, da) = disk(std::path::Path::new("/")).expect("statfs /");
781 assert!(dt >= da && dt > 0);
782 }
783}
784
785#[derive(Debug, Clone, PartialEq, Eq)]
808pub struct PciHotplug {
809 pub present: bool,
810 pub evidence: String,
812}
813
814pub const HOTPLUG_CONTROLLERS: [&str; 3] = ["acpiphp", "pciehp", "shpchp"];
817
818pub fn decide_pci_hotplug(config: Option<&str>, has_module: &dyn Fn(&str) -> bool) -> PciHotplug {
821 if let Some(text) = config {
822 let y = |sym: &str| text.lines().any(|l| l.trim() == format!("{sym}=y"));
823 let controllers: Vec<&str> = [
824 "CONFIG_HOTPLUG_PCI_ACPI",
825 "CONFIG_HOTPLUG_PCI_PCIE",
826 "CONFIG_HOTPLUG_PCI_SHPC",
827 ]
828 .into_iter()
829 .filter(|s| y(s))
830 .collect();
831 let present = y("CONFIG_HOTPLUG_PCI") && !controllers.is_empty();
832 return PciHotplug {
833 present,
834 evidence: if present {
835 format!("config.gz: CONFIG_HOTPLUG_PCI=y, {}", controllers.join(" "))
836 } else {
837 "config.gz: CONFIG_HOTPLUG_PCI or every controller is not set".to_owned()
838 },
839 };
840 }
841 let found: Vec<&str> = HOTPLUG_CONTROLLERS
842 .into_iter()
843 .filter(|m| has_module(m))
844 .collect();
845 PciHotplug {
846 present: !found.is_empty(),
847 evidence: if found.is_empty() {
848 "sys/module: no acpiphp, pciehp or shpchp (no /proc/config.gz)".to_owned()
849 } else {
850 format!("sys/module: {} (no /proc/config.gz)", found.join(" "))
851 },
852 }
853}
854
855pub fn pci_hotplug() -> PciHotplug {
857 use std::io::Read as _;
858 let config = std::fs::File::open("/proc/config.gz").ok().and_then(|f| {
859 let mut s = String::new();
860 flate2::read::GzDecoder::new(f)
861 .read_to_string(&mut s)
862 .ok()
863 .map(|_| s)
864 });
865 decide_pci_hotplug(config.as_deref(), &|m| {
866 std::path::Path::new("/sys/module").join(m).is_dir()
867 })
868}
869
870#[cfg(test)]
871mod pci_hotplug_tests {
872 use super::*;
873
874 #[test]
875 fn config_gz_decides_when_present_and_needs_a_controller() {
876 let none = |_: &str| true; let hot = "CONFIG_PCI=y\nCONFIG_HOTPLUG_PCI=y\nCONFIG_HOTPLUG_PCI_ACPI=y\n";
878 let p = decide_pci_hotplug(Some(hot), &none);
879 assert!(
880 p.present && p.evidence.contains("HOTPLUG_PCI_ACPI"),
881 "{p:?}"
882 );
883 assert!(
884 !decide_pci_hotplug(Some("CONFIG_HOTPLUG_PCI=y\n"), &none).present,
885 "the core without a controller acknowledges nothing"
886 );
887 assert!(
888 !decide_pci_hotplug(
889 Some("# CONFIG_HOTPLUG_PCI is not set\nCONFIG_HOTPLUG_PCI_ACPI=y\n"),
890 &none
891 )
892 .present
893 );
894 }
895
896 #[test]
897 fn without_config_gz_the_builtin_controllers_decide() {
898 let p = decide_pci_hotplug(None, &|m| m == "pciehp" || m == "acpiphp");
899 assert!(
900 p.present && p.evidence == "sys/module: acpiphp pciehp (no /proc/config.gz)",
901 "{p:?}"
902 );
903 let p = decide_pci_hotplug(None, &|_| false);
904 assert!(!p.present && p.evidence.contains("no acpiphp"), "{p:?}");
905 }
906}