1use std::collections::HashMap;
6use std::fs;
7use std::io;
8use std::path::Path;
9use std::sync::{Arc, Mutex, OnceLock};
10
11use thiserror::Error;
12use tokio::sync::RwLock;
13use tokio::sync::Semaphore;
14use tracing::Instrument;
15use tracing::{debug, error};
16use uzers::get_user_by_uid;
17use zbus::fdo::{DBusProxy, StatsProxy};
18use zbus::names::BusName;
19use zvariant::{Dict, OwnedValue, Value};
20
21use crate::MachineStats;
22
23#[derive(Error, Debug)]
24pub enum MonitordDbusStatsError {
25 #[error("D-Bus error: {0}")]
26 ZbusError(#[from] zbus::Error),
27 #[error("D-Bus fdo error: {0}")]
28 FdoError(#[from] zbus::fdo::Error),
29 #[error("Task join error: {0}")]
30 JoinError(#[from] tokio::task::JoinError),
31}
32
33#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
40pub struct DBusBrokerPeerAccounting {
41 pub id: String,
43 pub well_known_name: Option<String>,
45
46 pub unix_user_id: Option<u32>,
49 pub process_id: Option<u32>,
51 pub unix_group_ids: Option<Vec<u32>>,
53 pub name_objects: Option<u32>,
59 pub match_bytes: Option<u32>,
61 pub matches: Option<u32>,
63 pub reply_objects: Option<u32>,
65 pub incoming_bytes: Option<u32>,
67 pub incoming_fds: Option<u32>,
69 pub outgoing_bytes: Option<u32>,
71 pub outgoing_fds: Option<u32>,
73 pub activation_request_bytes: Option<u32>,
75 pub activation_request_fds: Option<u32>,
77}
78
79impl DBusBrokerPeerAccounting {
80 pub fn has_well_known_name(&self) -> bool {
82 self.well_known_name.is_some()
83 }
84
85 pub fn get_name(&self) -> &str {
87 self.well_known_name.as_deref().unwrap_or(&self.id)
88 }
89
90 pub fn get_cgroup_name(&self) -> Result<String, io::Error> {
91 let pid = self
92 .process_id
93 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "missing process_id"))?;
94
95 let path = format!("/proc/{}/cgroup", pid);
96 let content = fs::read_to_string(&path)?;
97
98 let cgroup = content.strip_prefix("0::").ok_or_else(|| {
100 io::Error::new(io::ErrorKind::InvalidData, "unexpected cgroup format")
101 })?;
102
103 Ok(cgroup.trim().trim_matches('/').replace('/', "-"))
104 }
105}
106
107#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
111pub struct DBusBrokerCGroupAccounting {
112 pub name: String,
114
115 pub name_objects: Option<u32>,
118 pub match_bytes: Option<u32>,
120 pub matches: Option<u32>,
122 pub reply_objects: Option<u32>,
124 pub incoming_bytes: Option<u32>,
126 pub incoming_fds: Option<u32>,
128 pub outgoing_bytes: Option<u32>,
130 pub outgoing_fds: Option<u32>,
132 pub activation_request_bytes: Option<u32>,
134 pub activation_request_fds: Option<u32>,
136}
137
138impl DBusBrokerCGroupAccounting {
139 pub fn combine_with_peer(&mut self, peer: &DBusBrokerPeerAccounting) {
140 fn sum(a: &mut Option<u32>, b: &Option<u32>) {
141 *a = match (a.take(), b) {
142 (Some(x), Some(y)) => Some(x + y),
143 (Some(x), None) => Some(x),
144 (None, Some(y)) => Some(*y),
145 (None, None) => None,
146 };
147 }
148
149 sum(&mut self.name_objects, &peer.name_objects);
150 sum(&mut self.match_bytes, &peer.match_bytes);
151 sum(&mut self.matches, &peer.matches);
152 sum(&mut self.reply_objects, &peer.reply_objects);
153 sum(&mut self.incoming_bytes, &peer.incoming_bytes);
154 sum(&mut self.incoming_fds, &peer.incoming_fds);
155 sum(&mut self.outgoing_bytes, &peer.outgoing_bytes);
156 sum(&mut self.outgoing_fds, &peer.outgoing_fds);
157 sum(
158 &mut self.activation_request_bytes,
159 &peer.activation_request_bytes,
160 );
161 sum(
162 &mut self.activation_request_fds,
163 &peer.activation_request_fds,
164 );
165 }
166}
167
168#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
171pub struct CurMaxPair {
172 pub cur: u32,
174 pub max: u32,
176}
177
178impl CurMaxPair {
179 pub fn get_usage(&self) -> u32 {
180 self.max - self.cur
183 }
184}
185
186#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
189pub struct DBusBrokerUserAccounting {
190 pub uid: u32,
192 pub username: String,
194
195 pub bytes: Option<CurMaxPair>,
197 pub fds: Option<CurMaxPair>,
199 pub stale_fds: Option<u32>,
207 pub matches: Option<CurMaxPair>,
209 pub objects: Option<CurMaxPair>,
211 }
214
215impl DBusBrokerUserAccounting {
216 fn new(uid: u32) -> Self {
217 let username = match get_user_by_uid(uid) {
218 Some(user) => user.name().to_string_lossy().into_owned(),
219 None => uid.to_string(),
220 };
221
222 Self {
223 uid,
224 username,
225 ..Default::default()
226 }
227 }
228}
229
230#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
233pub struct DBusStats {
234 pub serial: Option<u32>,
236 pub active_connections: Option<u32>,
238 pub incomplete_connections: Option<u32>,
240 pub bus_names: Option<u32>,
242 pub peak_bus_names: Option<u32>,
244 pub peak_bus_names_per_connection: Option<u32>,
246 pub match_rules: Option<u32>,
248 pub peak_match_rules: Option<u32>,
250 pub peak_match_rules_per_connection: Option<u32>,
252 pub stale_fds: Option<u32>,
254
255 pub dbus_broker_peer_accounting: Option<HashMap<String, DBusBrokerPeerAccounting>>,
257 pub dbus_broker_cgroup_accounting: Option<HashMap<String, DBusBrokerCGroupAccounting>>,
259 pub dbus_broker_user_accounting: Option<HashMap<u32, DBusBrokerUserAccounting>>,
261}
262
263impl DBusStats {
264 pub fn peer_accounting(&self) -> Option<&HashMap<String, DBusBrokerPeerAccounting>> {
265 self.dbus_broker_peer_accounting.as_ref()
266 }
267
268 pub fn cgroup_accounting(&self) -> Option<&HashMap<String, DBusBrokerCGroupAccounting>> {
269 self.dbus_broker_cgroup_accounting.as_ref()
270 }
271
272 pub fn user_accounting(&self) -> Option<&HashMap<u32, DBusBrokerUserAccounting>> {
273 self.dbus_broker_user_accounting.as_ref()
274 }
275}
276
277fn parse_ppid_from_stat(stat: &str) -> Option<u32> {
278 let (_, after_comm) = stat.rsplit_once(") ")?;
279 let mut fields = after_comm.split_whitespace();
280 let _state = fields.next()?;
281 fields.next()?.parse().ok()
282}
283
284fn proc_cmdline_args(path: &Path) -> io::Result<Vec<String>> {
285 let bytes = fs::read(path)?;
286 Ok(bytes
287 .split(|b| *b == b'\0')
288 .filter(|arg| !arg.is_empty())
289 .map(|arg| String::from_utf8_lossy(arg).into_owned())
290 .collect())
291}
292
293fn proc_pid_dirs(proc_root: &Path) -> io::Result<Vec<(u32, std::path::PathBuf)>> {
294 let mut result = Vec::new();
295 for entry in fs::read_dir(proc_root)? {
296 let entry = entry?;
297 let file_name = entry.file_name();
298 let Some(name) = file_name.to_str() else {
299 continue;
300 };
301 let Ok(pid) = name.parse::<u32>() else {
302 continue;
303 };
304 result.push((pid, entry.path()));
305 }
306 Ok(result)
307}
308
309fn find_system_dbus_broker_pid(proc_root: &Path) -> io::Result<Option<u32>> {
310 let pid_dirs = proc_pid_dirs(proc_root)?;
311 let mut launcher_pids = Vec::new();
312
313 for (pid, path) in &pid_dirs {
314 let args = match proc_cmdline_args(&path.join("cmdline")) {
315 Ok(args) => args,
316 Err(_) => continue,
317 };
318
319 let is_system_launcher = args
320 .first()
321 .map(|arg| arg.ends_with("dbus-broker-launch"))
322 .unwrap_or(false)
323 && (args
324 .windows(2)
325 .any(|window| window[0] == "--scope" && window[1] == "system")
326 || args.iter().any(|arg| arg == "--scope=system"));
327
328 if is_system_launcher {
329 launcher_pids.push(*pid);
330 }
331 }
332
333 for (pid, path) in &pid_dirs {
334 let args = match proc_cmdline_args(&path.join("cmdline")) {
335 Ok(args) => args,
336 Err(_) => continue,
337 };
338
339 let is_broker = args
340 .first()
341 .map(|arg| arg.ends_with("dbus-broker"))
342 .unwrap_or(false);
343 if !is_broker {
344 continue;
345 }
346
347 let stat = match fs::read_to_string(path.join("stat")) {
348 Ok(stat) => stat,
349 Err(_) => continue,
350 };
351 let Some(ppid) = parse_ppid_from_stat(&stat) else {
352 continue;
353 };
354 if launcher_pids.contains(&ppid) {
355 return Ok(Some(*pid));
356 }
357 }
358
359 Ok(None)
360}
361
362fn is_stale_pidfd(fd_path: &Path, fdinfo_path: &Path) -> bool {
363 let Ok(target) = fs::read_link(fd_path) else {
364 return false;
365 };
366 if target != Path::new("anon_inode:[pidfd]") {
367 return false;
368 }
369
370 let Ok(fdinfo) = fs::read_to_string(fdinfo_path) else {
371 return false;
372 };
373 fdinfo.lines().any(|line| {
374 let Some(pid) = line.strip_prefix("Pid:") else {
375 return false;
376 };
377 pid.trim() == "-1"
378 })
379}
380
381fn count_stale_pidfds(proc_root: &Path, pid: u32) -> io::Result<u32> {
382 let fd_dir = proc_root.join(pid.to_string()).join("fd");
383 let fdinfo_dir = proc_root.join(pid.to_string()).join("fdinfo");
384 let mut count: u32 = 0;
385
386 for entry in fs::read_dir(fdinfo_dir)? {
387 let entry = entry?;
388 let fd_name = entry.file_name();
389 let fd_path = fd_dir.join(&fd_name);
390 if is_stale_pidfd(&fd_path, &entry.path()) {
391 count = count.saturating_add(1);
392 }
393 }
394
395 Ok(count)
396}
397
398#[cfg(test)]
399fn collect_system_dbus_broker_stale_fds_from_proc(proc_root: &Path) -> io::Result<Option<u32>> {
400 let Some(pid) = find_system_dbus_broker_pid(proc_root)? else {
401 return Ok(None);
402 };
403
404 Ok(Some(count_stale_pidfds(proc_root, pid)?))
405}
406
407fn collect_system_dbus_broker_stale_fds_with_cache(
408 proc_root: &Path,
409 broker_pid_cache: &Mutex<Option<u32>>,
410) -> io::Result<Option<u32>> {
411 let cached_pid = *broker_pid_cache
412 .lock()
413 .map_err(|_| io::Error::other("dbus-broker pid cache poisoned"))?;
414
415 if let Some(pid) = cached_pid {
416 match count_stale_pidfds(proc_root, pid) {
417 Ok(count) => return Ok(Some(count)),
418 Err(err) if err.kind() == io::ErrorKind::NotFound => {
419 if let Ok(mut cached_pid) = broker_pid_cache.lock() {
420 if *cached_pid == Some(pid) {
421 *cached_pid = None;
422 }
423 }
424 }
425 Err(err) => return Err(err),
426 }
427 }
428
429 let Some(pid) = find_system_dbus_broker_pid(proc_root)? else {
430 return Ok(None);
431 };
432
433 let count = count_stale_pidfds(proc_root, pid)?;
434 let mut cached_pid = broker_pid_cache
435 .lock()
436 .map_err(|_| io::Error::other("dbus-broker pid cache poisoned"))?;
437 *cached_pid = Some(pid);
438
439 Ok(Some(count))
440}
441
442fn is_expected_procfs_error(err: &io::Error) -> bool {
443 matches!(
444 err.kind(),
445 io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
446 )
447}
448
449fn collect_system_dbus_broker_stale_fds() -> Option<u32> {
450 static SYSTEM_DBUS_BROKER_PID: OnceLock<Mutex<Option<u32>>> = OnceLock::new();
451
452 let broker_pid_cache = SYSTEM_DBUS_BROKER_PID.get_or_init(|| Mutex::new(None));
453
454 match collect_system_dbus_broker_stale_fds_with_cache(Path::new("/proc"), broker_pid_cache) {
455 Ok(stale_fds) => stale_fds,
456 Err(err) => {
457 if is_expected_procfs_error(&err) {
458 debug!("could not collect dbus-broker stale fd stats: {}", err);
459 } else {
460 error!("failed to collect dbus-broker stale fd stats: {}", err);
461 }
462 None
463 }
464 }
465}
466
467fn get_u32(dict: &Dict, key: &str) -> Option<u32> {
468 let value_key: Value = key.into();
469 dict.get(&value_key).ok().and_then(|v| match v.flatten() {
470 Some(Value::U32(val)) => Some(*val),
471 _ => None,
472 })
473}
474
475fn get_u32_vec(dict: &Dict, key: &str) -> Option<Vec<u32>> {
476 let value_key: Value = key.into();
477 dict.get(&value_key).ok().and_then(|v| match v.flatten() {
478 Some(Value::Array(array)) => {
479 let vec: Vec<u32> = array
480 .iter()
481 .filter_map(|item| {
482 if let Value::U32(num) = item {
483 Some(*num)
484 } else {
485 None
486 }
487 })
488 .collect();
489
490 Some(vec)
491 }
492 _ => None,
493 })
494}
495
496fn parse_peer_struct(
518 peer_value: &Value,
519 well_known_to_peer_names: &HashMap<String, String>,
520) -> Option<DBusBrokerPeerAccounting> {
521 let peer_struct = match peer_value {
522 Value::Structure(peer_struct) => peer_struct,
523 _ => return None,
524 };
525
526 match peer_struct.fields() {
527 [Value::Str(id), Value::Dict(credentials), Value::Dict(stats), ..] => {
528 Some(DBusBrokerPeerAccounting {
529 id: id.to_string(),
530 well_known_name: well_known_to_peer_names.get(id.as_str()).cloned(),
531 unix_user_id: get_u32(credentials, "UnixUserID"),
532 process_id: get_u32(credentials, "ProcessID"),
533 unix_group_ids: get_u32_vec(credentials, "UnixGroupIDs"),
534 name_objects: get_u32(stats, "NameObjects"),
535 match_bytes: get_u32(stats, "MatchBytes"),
536 matches: get_u32(stats, "Matches"),
537 reply_objects: get_u32(stats, "ReplyObjects"),
538 incoming_bytes: get_u32(stats, "IncomingBytes"),
539 incoming_fds: get_u32(stats, "IncomingFds"),
540 outgoing_bytes: get_u32(stats, "OutgoingBytes"),
541 outgoing_fds: get_u32(stats, "OutgoingFds"),
542 activation_request_bytes: get_u32(stats, "ActivationRequestBytes"),
543 activation_request_fds: get_u32(stats, "ActivationRequestFds"),
544 })
545 }
546 _ => None,
547 }
548}
549
550async fn parse_peer_accounting(
551 connection: &zbus::Connection,
552 config: &crate::config::Config,
553 owned_value: Option<&OwnedValue>,
554) -> Result<Option<Vec<DBusBrokerPeerAccounting>>, MonitordDbusStatsError> {
555 if !config.dbus_stats.peer_stats && !config.dbus_stats.cgroup_stats {
558 return Ok(None);
559 }
560
561 let value: &Value = match owned_value {
562 Some(v) => v,
563 None => return Ok(None),
564 };
565
566 let peers_value = match value {
567 Value::Array(peers_value) => peers_value,
568 _ => return Ok(None),
569 };
570
571 let well_known_to_peer_names = get_well_known_to_peer_names(connection, config).await?;
572
573 let result = peers_value
574 .iter()
575 .filter_map(|peer| parse_peer_struct(peer, &well_known_to_peer_names))
576 .collect();
577
578 Ok(Some(result))
579}
580
581fn filter_and_collect_peer_accounting(
582 config: &crate::config::Config,
583 peers: Option<&Vec<DBusBrokerPeerAccounting>>,
584) -> Option<HashMap<String, DBusBrokerPeerAccounting>> {
585 if !config.dbus_stats.peer_stats {
587 return None;
588 }
589
590 let result = peers?
591 .iter()
592 .filter(|peer| {
593 if config.dbus_stats.peer_well_known_names_only && !peer.has_well_known_name() {
594 return false;
595 }
596
597 let id = peer.id.as_str();
598 let name = peer.get_name();
599 if config.dbus_stats.peer_blocklist.contains(id)
600 || config.dbus_stats.peer_blocklist.contains(name)
601 {
602 return false;
603 }
604
605 if !config.dbus_stats.peer_allowlist.is_empty()
606 && !config.dbus_stats.peer_allowlist.contains(id)
607 && !config.dbus_stats.peer_allowlist.contains(name)
608 {
609 return false;
610 }
611
612 true
613 })
614 .map(|peer| (peer.id.clone(), peer.clone()))
615 .collect();
616
617 Some(result)
618}
619
620fn filter_and_collect_cgroup_accounting(
621 config: &crate::config::Config,
622 peers: Option<&Vec<DBusBrokerPeerAccounting>>,
623) -> Option<HashMap<String, DBusBrokerCGroupAccounting>> {
624 if !config.dbus_stats.cgroup_stats {
626 return None;
627 }
628
629 let mut result: HashMap<String, DBusBrokerCGroupAccounting> = HashMap::new();
630
631 for peer in peers?.iter() {
632 let cgroup_name = match peer.get_cgroup_name() {
633 Ok(name) => name,
634 Err(err) => {
635 error!("Failed to get cgroup name for peer {}: {}", peer.id, err);
636 continue;
637 }
638 };
639
640 if config.dbus_stats.cgroup_blocklist.contains(&cgroup_name) {
641 continue;
642 }
643
644 if !config.dbus_stats.cgroup_allowlist.is_empty()
645 && !config.dbus_stats.cgroup_allowlist.contains(&cgroup_name)
646 {
647 continue;
648 }
649
650 let entry =
651 result
652 .entry(cgroup_name.clone())
653 .or_insert_with(|| DBusBrokerCGroupAccounting {
654 name: cgroup_name,
655 ..Default::default()
656 });
657
658 entry.combine_with_peer(peer);
659 }
660
661 Some(result)
662}
663
664fn parse_user_struct(user_value: &Value) -> Option<DBusBrokerUserAccounting> {
695 let user_struct = match user_value {
696 Value::Structure(user_struct) => user_struct,
697 _ => return None,
698 };
699
700 match user_struct.fields() {
701 [Value::U32(uid), Value::Array(user_stats), ..] => {
702 let mut user = DBusBrokerUserAccounting::new(*uid);
703 for user_stat in user_stats.iter() {
704 if let Value::Structure(user_stat) = user_stat {
705 if let [Value::Str(name), Value::U32(cur), Value::U32(max), ..] =
706 user_stat.fields()
707 {
708 let pair = CurMaxPair {
709 cur: *cur,
710 max: *max,
711 };
712 match name.as_str() {
713 "Bytes" => user.bytes = Some(pair),
714 "Fds" => user.fds = Some(pair),
715 "Matches" => user.matches = Some(pair),
716 "Objects" => user.objects = Some(pair),
717 _ => {} }
719 }
720 }
721 }
722
723 Some(user)
724 }
725 _ => None,
726 }
727}
728
729fn parse_user_accounting(
730 config: &crate::config::Config,
731 owned_value: &OwnedValue,
732) -> Option<HashMap<u32, DBusBrokerUserAccounting>> {
733 if !config.dbus_stats.user_stats {
735 return None;
736 }
737
738 let value: &Value = owned_value;
739 let users_value = match value {
740 Value::Array(users_value) => users_value,
741 _ => return None,
742 };
743
744 let result = users_value
745 .iter()
746 .filter_map(parse_user_struct)
747 .filter(|user| {
748 let uid = user.uid.to_string();
749 if config.dbus_stats.user_blocklist.contains(&uid)
750 || config.dbus_stats.user_blocklist.contains(&user.username)
751 {
752 return false;
753 }
754
755 if !config.dbus_stats.user_allowlist.is_empty()
756 && !config.dbus_stats.user_allowlist.contains(&uid)
757 && !config.dbus_stats.user_allowlist.contains(&user.username)
758 {
759 return false;
760 }
761
762 true
763 })
764 .map(|user| (user.uid, user))
765 .collect();
766
767 Some(result)
768}
769
770async fn get_well_known_to_peer_names(
771 connection: &zbus::Connection,
772 config: &crate::config::Config,
773) -> Result<HashMap<String, String>, MonitordDbusStatsError> {
774 let dbus_proxy: DBusProxy<'static> = DBusProxy::builder(connection)
775 .cache_properties(zbus::proxy::CacheProperties::No)
776 .build()
777 .await?;
778
779 let dbus_names = dbus_proxy.list_names().await?;
780
781 let semaphore = Arc::new(Semaphore::new(
787 config.dbus_stats.peer_name_concurrency.max(1) as usize,
788 ));
789 let parent_span = tracing::Span::current();
790 let mut join_set = tokio::task::JoinSet::new();
791 for owned_busname in dbus_names {
792 if let BusName::WellKnown(_) = &*owned_busname {
793 let dbus_proxy = dbus_proxy.clone();
794 let semaphore = Arc::clone(&semaphore);
795 let parent_span = parent_span.clone();
796 join_set.spawn(async move {
797 let _permit = semaphore
798 .acquire()
799 .await
800 .expect("semaphore closed unexpectedly");
801 let span = tracing::debug_span!(
802 parent: &parent_span,
803 "peer_name_lookup",
804 name = %owned_busname
805 );
806 async {
807 let owner = dbus_proxy.get_name_owner((&owned_busname).into()).await?;
808 Ok::<_, MonitordDbusStatsError>((owner.to_string(), owned_busname.to_string()))
809 }
810 .instrument(span)
811 .await
812 });
813 }
814 }
815
816 let mut result = HashMap::new();
817 while let Some(joined) = join_set.join_next().await {
818 let (owner, name) = joined??;
819 result.insert(owner, name);
820 }
821
822 Ok(result)
823}
824
825async fn parse_dbus_stats_inner(
827 config: &crate::config::Config,
828 connection: &zbus::Connection,
829 collect_stale_fds: bool,
830) -> Result<DBusStats, MonitordDbusStatsError> {
831 let stats_proxy = StatsProxy::builder(connection)
832 .cache_properties(zbus::proxy::CacheProperties::No)
833 .build()
834 .await?;
835
836 let stale_fds_task = if collect_stale_fds && config.dbus_stats.stale_fd_stats {
837 Some(tokio::task::spawn_blocking(
838 collect_system_dbus_broker_stale_fds,
839 ))
840 } else {
841 None
842 };
843
844 let stats = stats_proxy.get_stats().await?;
845 let peers = parse_peer_accounting(
846 connection,
847 config,
848 stats.rest().get("org.bus1.DBus.Debug.Stats.PeerAccounting"),
849 )
850 .await?;
851
852 let stale_fds = match stale_fds_task {
853 Some(task) => match task.await {
854 Ok(stale_fds) => stale_fds,
855 Err(err) => {
856 error!("dbus-broker stale fd collection task failed: {}", err);
857 None
858 }
859 },
860 None => None,
861 };
862 let mut dbus_broker_user_accounting = stats
863 .rest()
864 .get("org.bus1.DBus.Debug.Stats.UserAccounting")
865 .map(|user| parse_user_accounting(config, user))
866 .unwrap_or_default();
867
868 if let (Some(stale_fds), Some(user_accounting)) =
869 (stale_fds, dbus_broker_user_accounting.as_mut())
870 {
871 if let Some(root) = user_accounting.get_mut(&0) {
872 root.stale_fds = Some(stale_fds);
873 }
874 }
875
876 let dbus_stats = DBusStats {
877 serial: stats.serial(),
878 active_connections: stats.active_connections(),
879 incomplete_connections: stats.incomplete_connections(),
880 bus_names: stats.bus_names(),
881 peak_bus_names: stats.peak_bus_names(),
882 peak_bus_names_per_connection: stats.peak_bus_names_per_connection(),
883 match_rules: stats.match_rules(),
884 peak_match_rules: stats.peak_match_rules(),
885 peak_match_rules_per_connection: stats.peak_match_rules_per_connection(),
886 stale_fds,
887
888 dbus_broker_peer_accounting: filter_and_collect_peer_accounting(config, peers.as_ref()),
890 dbus_broker_cgroup_accounting: filter_and_collect_cgroup_accounting(config, peers.as_ref()),
891 dbus_broker_user_accounting,
892 };
893
894 Ok(dbus_stats)
895}
896
897pub async fn parse_dbus_stats(
899 config: &crate::config::Config,
900 connection: &zbus::Connection,
901) -> Result<DBusStats, MonitordDbusStatsError> {
902 parse_dbus_stats_inner(config, connection, true).await
903}
904
905pub async fn update_dbus_stats(
907 config: Arc<crate::config::Config>,
908 connection: zbus::Connection,
909 locked_machine_stats: Arc<RwLock<MachineStats>>,
910) -> anyhow::Result<()> {
911 update_dbus_stats_inner(config, connection, locked_machine_stats, true).await
912}
913
914pub async fn update_machine_dbus_stats(
919 config: Arc<crate::config::Config>,
920 connection: zbus::Connection,
921 locked_machine_stats: Arc<RwLock<MachineStats>>,
922) -> anyhow::Result<()> {
923 update_dbus_stats_inner(config, connection, locked_machine_stats, false).await
924}
925
926async fn update_dbus_stats_inner(
927 config: Arc<crate::config::Config>,
928 connection: zbus::Connection,
929 locked_machine_stats: Arc<RwLock<MachineStats>>,
930 collect_stale_fds: bool,
931) -> anyhow::Result<()> {
932 match parse_dbus_stats_inner(&config, &connection, collect_stale_fds).await {
933 Ok(dbus_stats) => {
934 let mut machine_stats = locked_machine_stats.write().await;
935 machine_stats.dbus_stats = Some(dbus_stats)
936 }
937 Err(err) => error!("dbus stats failed: {:?}", err),
938 }
939 Ok(())
940}
941
942#[cfg(test)]
943mod tests {
944 use super::*;
945 use std::os::unix::fs::symlink;
946 use zvariant::{Array, OwnedValue, Str, Structure, Value};
947
948 fn write_fake_proc_process(proc_root: &Path, pid: u32, ppid: u32, cmdline: &[&str]) {
949 let pid_dir = proc_root.join(pid.to_string());
950 fs::create_dir_all(pid_dir.join("fd")).expect("create fake fd dir");
951 fs::create_dir_all(pid_dir.join("fdinfo")).expect("create fake fdinfo dir");
952
953 let mut cmdline_bytes = Vec::new();
954 for arg in cmdline {
955 cmdline_bytes.extend_from_slice(arg.as_bytes());
956 cmdline_bytes.push(0);
957 }
958 fs::write(pid_dir.join("cmdline"), cmdline_bytes).expect("write fake cmdline");
959 fs::write(
960 pid_dir.join("stat"),
961 format!("{pid} (fake process) S {ppid} 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0"),
962 )
963 .expect("write fake stat");
964 }
965
966 fn write_fake_fd(proc_root: &Path, pid: u32, fd: u32, target: &str, fdinfo: &str) {
967 let pid_dir = proc_root.join(pid.to_string());
968 symlink(target, pid_dir.join("fd").join(fd.to_string())).expect("create fake fd link");
969 fs::write(pid_dir.join("fdinfo").join(fd.to_string()), fdinfo).expect("write fake fdinfo");
970 }
971
972 #[test]
973 fn test_cur_max_pair_usage() {
974 let p = CurMaxPair { cur: 10, max: 100 };
975 assert_eq!(p.get_usage(), 90);
976 }
977
978 #[test]
979 fn test_parse_ppid_from_stat_handles_comm_with_spaces() {
980 let stat = "42 (dbus broker worker) S 7 0 0 0 0";
981 assert_eq!(parse_ppid_from_stat(stat), Some(7));
982 }
983
984 #[test]
985 fn test_collect_system_dbus_broker_stale_fds_from_proc() {
986 let tempdir = tempfile::tempdir().expect("create tempdir");
987 let proc_root = tempdir.path();
988
989 write_fake_proc_process(
990 proc_root,
991 10,
992 1,
993 &[
994 "/usr/bin/dbus-broker-launch",
995 "--scope",
996 "system",
997 "--audit",
998 ],
999 );
1000 write_fake_proc_process(proc_root, 11, 10, &["dbus-broker", "--log", "10"]);
1001 write_fake_proc_process(
1002 proc_root,
1003 20,
1004 1,
1005 &["/usr/bin/dbus-broker-launch", "--scope", "user"],
1006 );
1007 write_fake_proc_process(proc_root, 21, 20, &["dbus-broker", "--log", "10"]);
1008
1009 write_fake_fd(
1010 proc_root,
1011 11,
1012 0,
1013 "anon_inode:[pidfd]",
1014 "Pid:\t-1\nNSpid:\t-1\n",
1015 );
1016 write_fake_fd(
1017 proc_root,
1018 11,
1019 1,
1020 "anon_inode:[pidfd]",
1021 "Pid:\t123\nNSpid:\t123\n",
1022 );
1023 write_fake_fd(proc_root, 11, 2, "socket:[123]", "scm_fds: 0\n");
1024 write_fake_fd(
1025 proc_root,
1026 21,
1027 0,
1028 "anon_inode:[pidfd]",
1029 "Pid:\t-1\nNSpid:\t-1\n",
1030 );
1031
1032 assert_eq!(
1033 collect_system_dbus_broker_stale_fds_from_proc(proc_root).expect("collect stale fds"),
1034 Some(1)
1035 );
1036 }
1037
1038 #[test]
1039 fn test_combine_with_peer_option_summing() {
1040 let mut cg = DBusBrokerCGroupAccounting {
1041 name: "cg1".to_string(),
1042 name_objects: Some(5),
1043 match_bytes: None,
1044 matches: Some(3),
1045 reply_objects: None,
1046 incoming_bytes: Some(10),
1047 incoming_fds: None,
1048 outgoing_bytes: Some(7),
1049 outgoing_fds: Some(2),
1050 activation_request_bytes: None,
1051 activation_request_fds: Some(1),
1052 };
1053
1054 let peer = DBusBrokerPeerAccounting {
1055 id: ":1.1".to_string(),
1056 well_known_name: Some("com.example".to_string()),
1057 unix_user_id: Some(1000),
1058 process_id: Some(1234),
1059 unix_group_ids: Some(vec![1000]),
1060 name_objects: Some(2),
1061 match_bytes: Some(4),
1062 matches: None,
1063 reply_objects: Some(1),
1064 incoming_bytes: None,
1065 incoming_fds: Some(5),
1066 outgoing_bytes: Some(3),
1067 outgoing_fds: None,
1068 activation_request_bytes: Some(8),
1069 activation_request_fds: None,
1070 };
1071
1072 cg.combine_with_peer(&peer);
1073
1074 assert_eq!(cg.name_objects, Some(7));
1075 assert_eq!(cg.match_bytes, Some(4));
1076 assert_eq!(cg.matches, Some(3));
1077 assert_eq!(cg.reply_objects, Some(1));
1078 assert_eq!(cg.incoming_bytes, Some(10));
1079 assert_eq!(cg.incoming_fds, Some(5));
1080 assert_eq!(cg.outgoing_bytes, Some(10));
1081 assert_eq!(cg.outgoing_fds, Some(2));
1082 assert_eq!(cg.activation_request_bytes, Some(8));
1083 assert_eq!(cg.activation_request_fds, Some(1));
1084 }
1085
1086 #[test]
1087 fn test_parse_user_accounting_gating_and_parse() {
1088 let mut cfg = crate::config::Config::default();
1090 cfg.dbus_stats.user_stats = false;
1091 let empty_val = Value::Array(Array::from(Vec::<Value>::new()));
1092 let empty_owned = OwnedValue::try_from(empty_val).expect("owned value conversion");
1093 assert!(parse_user_accounting(&cfg, &empty_owned).is_none());
1094
1095 cfg.dbus_stats.user_stats = true;
1097 let empty_val = Value::Array(Array::from(Vec::<Value>::new()));
1098 let owned = OwnedValue::try_from(empty_val).expect("should convert empty array");
1099 let parsed = parse_user_accounting(&cfg, &owned).expect("should parse empty");
1100 assert_eq!(parsed.len(), 0);
1101
1102 let non_array = OwnedValue::try_from(Value::U32(0)).expect("should convert u32 value");
1104 assert!(parse_user_accounting(&cfg, &non_array).is_none());
1105 }
1106
1107 #[test]
1108 fn test_parse_user_struct_invalid_returns_none() {
1109 let invalid = Value::Structure(Structure::from((
1111 Value::Str(Str::from_static("not_uid")),
1112 Value::U32(10),
1113 Value::U32(20),
1114 )));
1115 assert!(parse_user_struct(&invalid).is_none());
1116 }
1117
1118 #[test]
1119 fn test_user_username_fallback() {
1120 let mut user = DBusBrokerUserAccounting::new(999_999);
1122 user.bytes = Some(CurMaxPair { cur: 5, max: 10 });
1123 assert_eq!(&user.username, "999999");
1125 }
1126}