Replace all systemctl commands with zbus D-Bus API
All checks were successful
Build and Release / build-and-release (push) Successful in 1m31s

Complete migration from systemctl subprocess calls to native D-Bus communication:

**Removed systemctl commands:**
- systemctl is-active (fallback) - use D-Bus cache from ListUnits
- systemctl show --property=LoadState,ActiveState,SubState - use D-Bus cache
- systemctl show --property=WorkingDirectory - use D-Bus Properties.Get
- systemctl show --property=MemoryCurrent - use D-Bus Properties.Get
- systemctl show nginx --property=ExecStart - use D-Bus Properties.Get

**Implementation details:**
- Added get_unit_property() helper for D-Bus property access
- Made get_nginx_site_metrics() async to support D-Bus calls
- Made get_nginx_sites_internal() async
- Made discover_nginx_sites() async
- Made get_nginx_config_from_systemd() async
- Fixed RwLock guard Send issues by using scoped locks

**Remaining external commands:**
- smartctl (disk.rs) - No Rust alternative for SMART data
- sudo du (systemd.rs) - Directory size measurement
- nginx -T (systemd.rs) - Nginx config fallback
- timeout hostname (nixos.rs) - Rare fallback only

Version bump: 0.1.197 → 0.1.198
This commit is contained in:
Christoffer Martinsson 2025-11-28 11:46:28 +01:00
parent b444c88ea0
commit 7ad149bbe4
5 changed files with 93 additions and 93 deletions

6
Cargo.lock generated
View File

@ -493,7 +493,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]] [[package]]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.197" version = "0.1.198"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
@ -515,7 +515,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.197" version = "0.1.198"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -545,7 +545,7 @@ dependencies = [
[[package]] [[package]]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.197" version = "0.1.198"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde", "serde",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-agent" name = "cm-dashboard-agent"
version = "0.1.197" version = "0.1.198"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -97,7 +97,7 @@ impl SystemdCollector {
// Sub-service metrics for specific services (always include cached results) // Sub-service metrics for specific services (always include cached results)
if service_name.contains("nginx") && active_status == "active" { if service_name.contains("nginx") && active_status == "active" {
let nginx_sites = self.get_nginx_site_metrics(); let nginx_sites = self.get_nginx_site_metrics().await;
for (site_name, latency_ms) in nginx_sites { for (site_name, latency_ms) in nginx_sites {
let site_status = if latency_ms >= 0.0 && latency_ms < self.config.nginx_latency_critical_ms { let site_status = if latency_ms >= 0.0 && latency_ms < self.config.nginx_latency_critical_ms {
"active" "active"
@ -231,27 +231,35 @@ impl SystemdCollector {
} }
/// Get nginx site metrics, checking them if cache is expired (like old working version) /// Get nginx site metrics, checking them if cache is expired (like old working version)
fn get_nginx_site_metrics(&self) -> Vec<(String, f32)> { async fn get_nginx_site_metrics(&self) -> Vec<(String, f32)> {
let mut state = self.state.write().unwrap(); // Check if we need to refresh (read lock)
let needs_refresh = {
// Check if we need to refresh nginx site metrics let state = self.state.read().unwrap();
let needs_refresh = match state.last_nginx_check_time { match state.last_nginx_check_time {
None => true, // First time None => true,
Some(last_time) => { Some(last_time) => {
let elapsed = last_time.elapsed().as_secs(); let elapsed = last_time.elapsed().as_secs();
elapsed >= state.nginx_check_interval_seconds elapsed >= state.nginx_check_interval_seconds
}
} }
}; };
if needs_refresh { if needs_refresh {
// Only check nginx sites if nginx service is active // Check if nginx is active (read lock)
if state.monitored_services.iter().any(|s| s.contains("nginx")) { let has_nginx = {
let fresh_metrics = self.get_nginx_sites_internal(); let state = self.state.read().unwrap();
state.monitored_services.iter().any(|s| s.contains("nginx"))
};
if has_nginx {
let fresh_metrics = self.get_nginx_sites_internal().await;
let mut state = self.state.write().unwrap();
state.nginx_site_metrics = fresh_metrics; state.nginx_site_metrics = fresh_metrics;
state.last_nginx_check_time = Some(Instant::now()); state.last_nginx_check_time = Some(Instant::now());
} }
} }
let state = self.state.read().unwrap();
state.nginx_site_metrics.clone() state.nginx_site_metrics.clone()
} }
@ -322,9 +330,9 @@ impl SystemdCollector {
Ok((services, service_status_cache)) Ok((services, service_status_cache))
} }
/// Get service status from cache (if available) or fallback to systemctl /// Get service status from D-Bus cache
fn get_service_status(&self, service: &str) -> Result<(String, String)> { fn get_service_status(&self, service: &str) -> Result<(String, String)> {
// Try to get status from cache first // Get status from D-Bus cache (populated by discover_services_internal)
if let Ok(state) = self.state.read() { if let Ok(state) = self.state.read() {
if let Some(cached_info) = state.service_status_cache.get(service) { if let Some(cached_info) = state.service_status_cache.get(service) {
let active_status = cached_info.active_state.clone(); let active_status = cached_info.active_state.clone();
@ -338,20 +346,45 @@ impl SystemdCollector {
} }
} }
// Fallback to systemctl if not in cache (with 2 second timeout) // Service not found in D-Bus cache - treat as inactive
let output = Command::new("timeout") Ok(("inactive".to_string(), "LoadState=not-found\nActiveState=inactive\nSubState=dead".to_string()))
.args(&["2", "systemctl", "is-active", &format!("{}.service", service)]) }
.output()?;
let active_status = String::from_utf8(output.stdout)?.trim().to_string(); /// Get a unit property via D-Bus
async fn get_unit_property(&self, service_name: &str, property: &str) -> Option<zbus::zvariant::OwnedValue> {
// Connect to system D-Bus
let connection = Connection::system().await.ok()?;
// Get more detailed info (with 2 second timeout) // Get systemd manager proxy
let output = Command::new("timeout") let manager_proxy = zbus::Proxy::new(
.args(&["2", "systemctl", "show", &format!("{}.service", service), "--property=LoadState,ActiveState,SubState"]) &connection,
.output()?; "org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
).await.ok()?;
let detailed_info = String::from_utf8(output.stdout)?; // Get unit path for service
Ok((active_status, detailed_info)) let unit_name = format!("{}.service", service_name);
let unit_path: zbus::zvariant::OwnedObjectPath = manager_proxy
.call("GetUnit", &(unit_name,))
.await
.ok()?;
// Get property using standard D-Bus Properties interface
let prop_proxy = zbus::Proxy::new(
&connection,
"org.freedesktop.systemd1",
unit_path.as_str(),
"org.freedesktop.DBus.Properties",
).await.ok()?;
// Try Service interface first, fallback to Unit interface
// Get returns a Variant, we need to extract the inner value
if let Ok(variant) = prop_proxy.call("Get", &("org.freedesktop.systemd1.Service", property)).await {
return Some(variant);
}
prop_proxy.call("Get", &("org.freedesktop.systemd1.Unit", property)).await.ok()
} }
/// Check if service name matches pattern (supports wildcards like nginx*) /// Check if service name matches pattern (supports wildcards like nginx*)
@ -407,21 +440,12 @@ impl SystemdCollector {
return Ok(0.0); return Ok(0.0);
} }
// No configured path - try to get WorkingDirectory from systemctl (with 2 second timeout) // No configured path - try to get WorkingDirectory from D-Bus
let output = Command::new("timeout") if let Some(value) = self.get_unit_property(service_name, "WorkingDirectory").await {
.args(&["2", "systemctl", "show", &format!("{}.service", service_name), "--property=WorkingDirectory"]) // WorkingDirectory is a string property - try to extract as string
.output() if let Ok(dir_str) = <String>::try_from(value) {
.map_err(|e| CollectorError::SystemRead { if !dir_str.is_empty() && dir_str != "/" && dir_str != "[not set]" {
path: format!("WorkingDirectory for {}", service_name), return Ok(self.get_directory_size(&dir_str).await.unwrap_or(0.0));
error: e.to_string(),
})?;
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.starts_with("WorkingDirectory=") && !line.contains("[not set]") {
let dir = line.strip_prefix("WorkingDirectory=").unwrap_or("");
if !dir.is_empty() && dir != "/" {
return Ok(self.get_directory_size(dir).await.unwrap_or(0.0));
} }
} }
} }
@ -484,27 +508,13 @@ impl SystemdCollector {
} }
} }
/// Get memory usage for a specific service /// Get memory usage for a specific service via D-Bus
async fn get_service_memory_usage(&self, service_name: &str) -> Result<f32, CollectorError> { async fn get_service_memory_usage(&self, service_name: &str) -> Result<f32, CollectorError> {
let output = Command::new("systemctl") // Get MemoryCurrent property from D-Bus
.args(&["show", &format!("{}.service", service_name), "--property=MemoryCurrent"]) if let Some(value) = self.get_unit_property(service_name, "MemoryCurrent").await {
.output() // MemoryCurrent is a u64 property (bytes) - try to extract
.map_err(|e| CollectorError::SystemRead { if let Ok(memory_bytes) = <u64>::try_from(value) {
path: format!("memory usage for {}", service_name), return Ok(memory_bytes as f32 / (1024.0 * 1024.0)); // Convert to MB
error: e.to_string(),
})?;
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
if line.starts_with("MemoryCurrent=") {
if let Some(mem_str) = line.strip_prefix("MemoryCurrent=") {
if mem_str != "[not set]" {
if let Ok(memory_bytes) = mem_str.parse::<u64>() {
return Ok(memory_bytes as f32 / (1024.0 * 1024.0)); // Convert to MB
}
}
}
} }
} }
@ -535,11 +545,11 @@ impl SystemdCollector {
} }
/// Get nginx sites with latency checks (internal - no caching) /// Get nginx sites with latency checks (internal - no caching)
fn get_nginx_sites_internal(&self) -> Vec<(String, f32)> { async fn get_nginx_sites_internal(&self) -> Vec<(String, f32)> {
let mut sites = Vec::new(); let mut sites = Vec::new();
// Discover nginx sites from configuration // Discover nginx sites from configuration
let discovered_sites = self.discover_nginx_sites(); let discovered_sites = self.discover_nginx_sites().await;
// Always add all discovered sites, even if checks fail (like old version) // Always add all discovered sites, even if checks fail (like old version)
for (site_name, url) in &discovered_sites { for (site_name, url) in &discovered_sites {
@ -558,9 +568,9 @@ impl SystemdCollector {
} }
/// Discover nginx sites from configuration /// Discover nginx sites from configuration
fn discover_nginx_sites(&self) -> Vec<(String, String)> { async fn discover_nginx_sites(&self) -> Vec<(String, String)> {
// Use the same approach as the old working agent: get nginx config from systemd // Use the same approach as the old working agent: get nginx config from systemd
let config_content = match self.get_nginx_config_from_systemd() { let config_content = match self.get_nginx_config_from_systemd().await {
Some(content) => content, Some(content) => content,
None => { None => {
debug!("Could not get nginx config from systemd, trying nginx -T fallback"); debug!("Could not get nginx config from systemd, trying nginx -T fallback");
@ -593,30 +603,20 @@ impl SystemdCollector {
Some(String::from_utf8_lossy(&output.stdout).to_string()) Some(String::from_utf8_lossy(&output.stdout).to_string())
} }
/// Get nginx config from systemd service definition (NixOS compatible) /// Get nginx config from systemd service definition via D-Bus (NixOS compatible)
fn get_nginx_config_from_systemd(&self) -> Option<String> { async fn get_nginx_config_from_systemd(&self) -> Option<String> {
let output = Command::new("systemctl") // Get ExecStart property from D-Bus
.args(&["show", "nginx", "--property=ExecStart", "--no-pager"]) let value = self.get_unit_property("nginx", "ExecStart").await?;
.output()
.ok()?;
if !output.status.success() { // ExecStart is a complex structure: array of (path, args, unclean_exit)
debug!("Failed to get nginx ExecStart from systemd"); // For our purposes, we need to extract the command line
return None; let exec_start_str = format!("{:?}", value);
} debug!("nginx ExecStart from D-Bus: {}", exec_start_str);
let stdout = String::from_utf8_lossy(&output.stdout); // Extract config path from ExecStart structure
debug!("systemctl show nginx output: {}", stdout); if let Some(config_path) = self.extract_config_path_from_exec_start(&exec_start_str) {
debug!("Extracted config path: {}", config_path);
// Parse ExecStart to extract -c config path return std::fs::read_to_string(&config_path).ok();
for line in stdout.lines() {
if line.starts_with("ExecStart=") {
debug!("Found ExecStart line: {}", line);
if let Some(config_path) = self.extract_config_path_from_exec_start(line) {
debug!("Extracted config path: {}", config_path);
return std::fs::read_to_string(&config_path).ok();
}
}
} }
None None

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard" name = "cm-dashboard"
version = "0.1.197" version = "0.1.198"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "cm-dashboard-shared" name = "cm-dashboard-shared"
version = "0.1.197" version = "0.1.198"
edition = "2021" edition = "2021"
[dependencies] [dependencies]