1use std::collections::HashMap;
6use std::fs;
7use std::io;
8use std::sync::Arc;
9
10use thiserror::Error;
11use tokio::sync::RwLock;
12use tracing::error;
13use uzers::get_user_by_uid;
14use zbus::fdo::{DBusProxy, StatsProxy};
15use zbus::names::BusName;
16use zvariant::{Dict, OwnedValue, Value};
17
18use crate::MachineStats;
19
20#[derive(Error, Debug)]
21pub enum MonitordDbusStatsError {
22 #[error("D-Bus error: {0}")]
23 ZbusError(#[from] zbus::Error),
24 #[error("D-Bus fdo error: {0}")]
25 FdoError(#[from] zbus::fdo::Error),
26}
27
28#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
35pub struct DBusBrokerPeerAccounting {
36 pub id: String,
38 pub well_known_name: Option<String>,
40
41 pub unix_user_id: Option<u32>,
44 pub process_id: Option<u32>,
46 pub unix_group_ids: Option<Vec<u32>>,
48 pub name_objects: Option<u32>,
54 pub match_bytes: Option<u32>,
56 pub matches: Option<u32>,
58 pub reply_objects: Option<u32>,
60 pub incoming_bytes: Option<u32>,
62 pub incoming_fds: Option<u32>,
64 pub outgoing_bytes: Option<u32>,
66 pub outgoing_fds: Option<u32>,
68 pub activation_request_bytes: Option<u32>,
70 pub activation_request_fds: Option<u32>,
72}
73
74impl DBusBrokerPeerAccounting {
75 pub fn get_cgroup_name(&self) -> Result<String, io::Error> {
76 let pid = self
77 .process_id
78 .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "missing process_id"))?;
79
80 let path = format!("/proc/{}/cgroup", pid);
81 let content = fs::read_to_string(&path)?;
82
83 let cgroup = content.strip_prefix("0::").ok_or_else(|| {
85 io::Error::new(io::ErrorKind::InvalidData, "unexpected cgroup format")
86 })?;
87
88 Ok(cgroup.trim().trim_matches('/').replace('/', "-"))
89 }
90}
91
92#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
96pub struct DBusBrokerCGroupAccounting {
97 pub name: String,
99
100 pub name_objects: Option<u32>,
103 pub match_bytes: Option<u32>,
105 pub matches: Option<u32>,
107 pub reply_objects: Option<u32>,
109 pub incoming_bytes: Option<u32>,
111 pub incoming_fds: Option<u32>,
113 pub outgoing_bytes: Option<u32>,
115 pub outgoing_fds: Option<u32>,
117 pub activation_request_bytes: Option<u32>,
119 pub activation_request_fds: Option<u32>,
121}
122
123impl DBusBrokerCGroupAccounting {
124 pub fn combine_with_peer(&mut self, peer: &DBusBrokerPeerAccounting) {
125 fn sum(a: &mut Option<u32>, b: &Option<u32>) {
126 *a = match (a.take(), b) {
127 (Some(x), Some(y)) => Some(x + y),
128 (Some(x), None) => Some(x),
129 (None, Some(y)) => Some(*y),
130 (None, None) => None,
131 };
132 }
133
134 sum(&mut self.name_objects, &peer.name_objects);
135 sum(&mut self.match_bytes, &peer.match_bytes);
136 sum(&mut self.matches, &peer.matches);
137 sum(&mut self.reply_objects, &peer.reply_objects);
138 sum(&mut self.incoming_bytes, &peer.incoming_bytes);
139 sum(&mut self.incoming_fds, &peer.incoming_fds);
140 sum(&mut self.outgoing_bytes, &peer.outgoing_bytes);
141 sum(&mut self.outgoing_fds, &peer.outgoing_fds);
142 sum(
143 &mut self.activation_request_bytes,
144 &peer.activation_request_bytes,
145 );
146 sum(
147 &mut self.activation_request_fds,
148 &peer.activation_request_fds,
149 );
150 }
151}
152
153#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
156pub struct CurMaxPair {
157 pub cur: u32,
159 pub max: u32,
161}
162
163impl CurMaxPair {
164 pub fn get_usage(&self) -> u32 {
165 self.max - self.cur
168 }
169}
170
171#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
174pub struct DBusBrokerUserAccounting {
175 pub uid: u32,
177
178 pub bytes: Option<CurMaxPair>,
180 pub fds: Option<CurMaxPair>,
182 pub matches: Option<CurMaxPair>,
184 pub objects: Option<CurMaxPair>,
186 }
189
190impl DBusBrokerUserAccounting {
191 fn new(uid: u32) -> Self {
192 Self {
193 uid,
194 ..Default::default()
195 }
196 }
197
198 pub fn get_name_for_metric(&self) -> String {
199 match get_user_by_uid(self.uid) {
200 Some(user) => user.name().to_string_lossy().into_owned(),
201 None => self.uid.to_string(),
202 }
203 }
204}
205
206#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, Eq, PartialEq)]
209pub struct DBusStats {
210 pub serial: Option<u32>,
212 pub active_connections: Option<u32>,
214 pub incomplete_connections: Option<u32>,
216 pub bus_names: Option<u32>,
218 pub peak_bus_names: Option<u32>,
220 pub peak_bus_names_per_connection: Option<u32>,
222 pub match_rules: Option<u32>,
224 pub peak_match_rules: Option<u32>,
226 pub peak_match_rules_per_connection: Option<u32>,
228
229 pub dbus_broker_peer_accounting: Option<HashMap<String, DBusBrokerPeerAccounting>>,
231 pub dbus_broker_user_accounting: Option<HashMap<u32, DBusBrokerUserAccounting>>,
233
234 pub peer_stats: bool,
236 pub cgroup_stats: bool,
238}
239
240impl DBusStats {
241 pub fn peer_accounting(&self) -> Option<&HashMap<String, DBusBrokerPeerAccounting>> {
242 match self.peer_stats {
243 true => self.dbus_broker_peer_accounting.as_ref(),
244 false => None,
245 }
246 }
247
248 pub fn cgroup_accounting(&self) -> Option<HashMap<String, DBusBrokerCGroupAccounting>> {
249 if !self.cgroup_stats {
250 return None;
251 }
252
253 let peer_accounting = self.dbus_broker_peer_accounting.as_ref()?;
254 let mut result: HashMap<String, DBusBrokerCGroupAccounting> = HashMap::new();
255
256 for peer in peer_accounting.values() {
257 let cgroup_name = match peer.get_cgroup_name() {
258 Ok(name) => name,
259 Err(err) => {
260 error!("Failed to get cgroup name for peer {}: {}", peer.id, err);
261 continue;
262 }
263 };
264
265 let entry = result.entry(cgroup_name).or_insert_with_key(|cgroup_name| {
266 DBusBrokerCGroupAccounting {
267 name: cgroup_name.clone(),
268 ..Default::default()
269 }
270 });
271
272 entry.combine_with_peer(peer);
273 }
274
275 Some(result)
276 }
277
278 pub fn user_accounting(&self) -> Option<&HashMap<u32, DBusBrokerUserAccounting>> {
279 self.dbus_broker_user_accounting.as_ref()
280 }
281}
282
283fn get_u32(dict: &Dict, key: &str) -> Option<u32> {
284 let value_key: Value = key.into();
285 dict.get(&value_key).ok().and_then(|v| match v.flatten() {
286 Some(Value::U32(val)) => Some(*val),
287 _ => None,
288 })
289}
290
291fn get_u32_vec(dict: &Dict, key: &str) -> Option<Vec<u32>> {
292 let value_key: Value = key.into();
293 dict.get(&value_key).ok().and_then(|v| match v.flatten() {
294 Some(Value::Array(array)) => {
295 let vec: Vec<u32> = array
296 .iter()
297 .filter_map(|item| {
298 if let Value::U32(num) = item {
299 Some(*num)
300 } else {
301 None
302 }
303 })
304 .collect();
305
306 Some(vec)
307 }
308 _ => None,
309 })
310}
311
312fn parse_peer_struct(
334 peer_value: &Value,
335 well_known_to_peer_names: &HashMap<String, String>,
336) -> Option<DBusBrokerPeerAccounting> {
337 let peer_struct = match peer_value {
338 Value::Structure(peer_struct) => peer_struct,
339 _ => return None,
340 };
341
342 match peer_struct.fields() {
343 [Value::Str(id), Value::Dict(credentials), Value::Dict(stats), ..] => {
344 Some(DBusBrokerPeerAccounting {
345 id: id.to_string(),
346 well_known_name: well_known_to_peer_names.get(id.as_str()).cloned(),
347 unix_user_id: get_u32(credentials, "UnixUserID"),
348 process_id: get_u32(credentials, "ProcessID"),
349 unix_group_ids: get_u32_vec(credentials, "UnixGroupIDs"),
350 name_objects: get_u32(stats, "NameObjects"),
351 match_bytes: get_u32(stats, "MatchBytes"),
352 matches: get_u32(stats, "Matches"),
353 reply_objects: get_u32(stats, "ReplyObjects"),
354 incoming_bytes: get_u32(stats, "IncomingBytes"),
355 incoming_fds: get_u32(stats, "IncomingFds"),
356 outgoing_bytes: get_u32(stats, "OutgoingBytes"),
357 outgoing_fds: get_u32(stats, "OutgoingFds"),
358 activation_request_bytes: get_u32(stats, "ActivationRequestBytes"),
359 activation_request_fds: get_u32(stats, "ActivationRequestFds"),
360 })
361 }
362 _ => None,
363 }
364}
365
366fn parse_peer_accounting(
367 config: &crate::config::Config,
368 owned_value: &OwnedValue,
369 well_known_to_peer_names: &HashMap<String, String>,
370) -> Option<HashMap<String, DBusBrokerPeerAccounting>> {
371 if !config.dbus_stats.peer_stats && !config.dbus_stats.cgroup_stats {
374 return None;
375 }
376
377 let value: &Value = owned_value;
378 let peers_value = match value {
379 Value::Array(peers_value) => peers_value,
380 _ => return None,
381 };
382
383 let result = peers_value
384 .iter()
385 .filter_map(|peer| parse_peer_struct(peer, well_known_to_peer_names))
386 .map(|peer| (peer.id.clone(), peer))
387 .collect();
388
389 Some(result)
390}
391
392fn parse_user_struct(user_value: &Value) -> Option<DBusBrokerUserAccounting> {
423 let user_struct = match user_value {
424 Value::Structure(user_struct) => user_struct,
425 _ => return None,
426 };
427
428 match user_struct.fields() {
429 [Value::U32(uid), Value::Array(user_stats), ..] => {
430 let mut user = DBusBrokerUserAccounting::new(*uid);
431 for user_stat in user_stats.iter() {
432 if let Value::Structure(user_stat) = user_stat {
433 if let [Value::Str(name), Value::U32(cur), Value::U32(max), ..] =
434 user_stat.fields()
435 {
436 let pair = CurMaxPair {
437 cur: *cur,
438 max: *max,
439 };
440 match name.as_str() {
441 "Bytes" => user.bytes = Some(pair),
442 "Fds" => user.fds = Some(pair),
443 "Matches" => user.matches = Some(pair),
444 "Objects" => user.objects = Some(pair),
445 _ => {} }
447 }
448 }
449 }
450
451 Some(user)
452 }
453 _ => None,
454 }
455}
456
457fn parse_user_accounting(
458 config: &crate::config::Config,
459 owned_value: &OwnedValue,
460) -> Option<HashMap<u32, DBusBrokerUserAccounting>> {
461 if !config.dbus_stats.user_stats {
463 return None;
464 }
465
466 let value: &Value = owned_value;
467 let users_value = match value {
468 Value::Array(users_value) => users_value,
469 _ => return None,
470 };
471
472 let result = users_value
473 .iter()
474 .filter_map(parse_user_struct)
475 .map(|user| (user.uid, user))
476 .collect();
477
478 Some(result)
479}
480
481async fn get_well_known_to_peer_names(
482 dbus_proxy: &DBusProxy<'_>,
483) -> Result<HashMap<String, String>, MonitordDbusStatsError> {
484 let dbus_names = dbus_proxy.list_names().await?;
485 let mut result = HashMap::new();
486
487 for owned_busname in dbus_names.iter() {
488 let name: &BusName = owned_busname;
489 if let BusName::WellKnown(_) = name {
490 let owner = dbus_proxy.get_name_owner(name.clone()).await?;
492 result.insert(owner.to_string(), name.to_string());
493 }
494 }
495
496 Ok(result)
497}
498
499pub async fn parse_dbus_stats(
501 config: &crate::config::Config,
502 connection: &zbus::Connection,
503) -> Result<DBusStats, MonitordDbusStatsError> {
504 let dbus_proxy = DBusProxy::new(connection).await?;
505 let well_known_to_peer_names = get_well_known_to_peer_names(&dbus_proxy).await?;
506
507 let stats_proxy = StatsProxy::new(connection).await?;
508 let stats = stats_proxy.get_stats().await?;
509
510 let dbus_stats = DBusStats {
511 serial: stats.serial(),
512 active_connections: stats.active_connections(),
513 incomplete_connections: stats.incomplete_connections(),
514 bus_names: stats.bus_names(),
515 peak_bus_names: stats.peak_bus_names(),
516 peak_bus_names_per_connection: stats.peak_bus_names_per_connection(),
517 match_rules: stats.match_rules(),
518 peak_match_rules: stats.peak_match_rules(),
519 peak_match_rules_per_connection: stats.peak_match_rules_per_connection(),
520
521 dbus_broker_peer_accounting: stats
523 .rest()
524 .get("org.bus1.DBus.Debug.Stats.PeerAccounting")
525 .map(|peer| parse_peer_accounting(config, peer, &well_known_to_peer_names))
526 .unwrap_or_default(),
527 dbus_broker_user_accounting: stats
528 .rest()
529 .get("org.bus1.DBus.Debug.Stats.UserAccounting")
530 .map(|user| parse_user_accounting(config, user))
531 .unwrap_or_default(),
532
533 peer_stats: config.dbus_stats.peer_stats,
535 cgroup_stats: config.dbus_stats.cgroup_stats,
536 };
537
538 Ok(dbus_stats)
539}
540
541pub async fn update_dbus_stats(
543 config: Arc<crate::config::Config>,
544 connection: zbus::Connection,
545 locked_machine_stats: Arc<RwLock<MachineStats>>,
546) -> anyhow::Result<()> {
547 match parse_dbus_stats(&config, &connection).await {
548 Ok(dbus_stats) => {
549 let mut machine_stats = locked_machine_stats.write().await;
550 machine_stats.dbus_stats = Some(dbus_stats)
551 }
552 Err(err) => error!("dbus stats failed: {:?}", err),
553 }
554 Ok(())
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use zvariant::{Array, OwnedValue, Str, Structure, Value};
561
562 #[test]
563 fn test_cur_max_pair_usage() {
564 let p = CurMaxPair { cur: 10, max: 100 };
565 assert_eq!(p.get_usage(), 90);
566 }
567
568 #[test]
569 fn test_cgroup_accounting_gating_and_skip_errors() {
570 let disabled = DBusStats {
571 cgroup_stats: false,
572 peer_stats: true,
573 dbus_broker_peer_accounting: Some(HashMap::new()),
574 ..Default::default()
575 };
576 assert!(disabled.cgroup_accounting().is_none());
577
578 let mut peers: HashMap<String, DBusBrokerPeerAccounting> = HashMap::new();
579 peers.insert(
580 ":1.77".to_string(),
581 DBusBrokerPeerAccounting {
582 id: ":1.77".to_string(),
583 process_id: None,
584 ..Default::default()
585 },
586 );
587
588 let enabled = DBusStats {
589 cgroup_stats: true,
590 peer_stats: true,
591 dbus_broker_peer_accounting: Some(peers),
592 ..Default::default()
593 };
594
595 let cg_map = enabled.cgroup_accounting().expect("map should exist");
596 assert!(cg_map.is_empty());
597 }
598
599 #[test]
600 fn test_combine_with_peer_option_summing() {
601 let mut cg = DBusBrokerCGroupAccounting {
602 name: "cg1".to_string(),
603 name_objects: Some(5),
604 match_bytes: None,
605 matches: Some(3),
606 reply_objects: None,
607 incoming_bytes: Some(10),
608 incoming_fds: None,
609 outgoing_bytes: Some(7),
610 outgoing_fds: Some(2),
611 activation_request_bytes: None,
612 activation_request_fds: Some(1),
613 };
614
615 let peer = DBusBrokerPeerAccounting {
616 id: ":1.1".to_string(),
617 well_known_name: Some("com.example".to_string()),
618 unix_user_id: Some(1000),
619 process_id: Some(1234),
620 unix_group_ids: Some(vec![1000]),
621 name_objects: Some(2),
622 match_bytes: Some(4),
623 matches: None,
624 reply_objects: Some(1),
625 incoming_bytes: None,
626 incoming_fds: Some(5),
627 outgoing_bytes: Some(3),
628 outgoing_fds: None,
629 activation_request_bytes: Some(8),
630 activation_request_fds: None,
631 };
632
633 cg.combine_with_peer(&peer);
634
635 assert_eq!(cg.name_objects, Some(7));
636 assert_eq!(cg.match_bytes, Some(4));
637 assert_eq!(cg.matches, Some(3));
638 assert_eq!(cg.reply_objects, Some(1));
639 assert_eq!(cg.incoming_bytes, Some(10));
640 assert_eq!(cg.incoming_fds, Some(5));
641 assert_eq!(cg.outgoing_bytes, Some(10));
642 assert_eq!(cg.outgoing_fds, Some(2));
643 assert_eq!(cg.activation_request_bytes, Some(8));
644 assert_eq!(cg.activation_request_fds, Some(1));
645 }
646
647 #[test]
648 fn test_parse_user_accounting_gating_and_parse() {
649 let mut cfg = crate::config::Config::default();
651 cfg.dbus_stats.user_stats = false;
652 let empty_val = Value::Array(Array::from(Vec::<Value>::new()));
653 let empty_owned = OwnedValue::try_from(empty_val).expect("owned value conversion");
654 assert!(parse_user_accounting(&cfg, &empty_owned).is_none());
655
656 cfg.dbus_stats.user_stats = true;
658 let empty_val = Value::Array(Array::from(Vec::<Value>::new()));
659 let owned = OwnedValue::try_from(empty_val).expect("should convert empty array");
660 let parsed = parse_user_accounting(&cfg, &owned).expect("should parse empty");
661 assert_eq!(parsed.len(), 0);
662
663 let non_array = OwnedValue::try_from(Value::U32(0)).expect("should convert u32 value");
665 assert!(parse_user_accounting(&cfg, &non_array).is_none());
666 }
667
668 #[test]
669 fn test_parse_user_struct_invalid_returns_none() {
670 let invalid = Value::Structure(Structure::from((
672 Value::Str(Str::from_static("not_uid")),
673 Value::U32(10),
674 Value::U32(20),
675 )));
676 assert!(parse_user_struct(&invalid).is_none());
677 }
678
679 #[test]
680 fn test_user_metric_name_fallback() {
681 let user = DBusBrokerUserAccounting {
683 uid: 999_999,
684 bytes: Some(CurMaxPair { cur: 5, max: 10 }),
685 ..Default::default()
686 };
687 let name = user.get_name_for_metric();
689 assert_eq!(name, "999999");
690 }
691}