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]]
name = "cm-dashboard"
version = "0.1.197"
version = "0.1.198"
dependencies = [
"anyhow",
"chrono",
@ -515,7 +515,7 @@ dependencies = [
[[package]]
name = "cm-dashboard-agent"
version = "0.1.197"
version = "0.1.198"
dependencies = [
"anyhow",
"async-trait",
@ -545,7 +545,7 @@ dependencies = [
[[package]]
name = "cm-dashboard-shared"
version = "0.1.197"
version = "0.1.198"
dependencies = [
"chrono",
"serde",

View File

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

View File

@ -97,7 +97,7 @@ impl SystemdCollector {
// Sub-service metrics for specific services (always include cached results)
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 {
let site_status = if latency_ms >= 0.0 && latency_ms < self.config.nginx_latency_critical_ms {
"active"
@ -231,27 +231,35 @@ impl SystemdCollector {
}
/// Get nginx site metrics, checking them if cache is expired (like old working version)
fn get_nginx_site_metrics(&self) -> Vec<(String, f32)> {
let mut state = self.state.write().unwrap();
// Check if we need to refresh nginx site metrics
let needs_refresh = match state.last_nginx_check_time {
None => true, // First time
Some(last_time) => {
let elapsed = last_time.elapsed().as_secs();
elapsed >= state.nginx_check_interval_seconds
async fn get_nginx_site_metrics(&self) -> Vec<(String, f32)> {
// Check if we need to refresh (read lock)
let needs_refresh = {
let state = self.state.read().unwrap();
match state.last_nginx_check_time {
None => true,
Some(last_time) => {
let elapsed = last_time.elapsed().as_secs();
elapsed >= state.nginx_check_interval_seconds
}
}
};
if needs_refresh {
// Only check nginx sites if nginx service is active
if state.monitored_services.iter().any(|s| s.contains("nginx")) {
let fresh_metrics = self.get_nginx_sites_internal();
// Check if nginx is active (read lock)
let has_nginx = {
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.last_nginx_check_time = Some(Instant::now());
}
}
let state = self.state.read().unwrap();
state.nginx_site_metrics.clone()
}
@ -322,9 +330,9 @@ impl SystemdCollector {
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)> {
// 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 Some(cached_info) = state.service_status_cache.get(service) {
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)
let output = Command::new("timeout")
.args(&["2", "systemctl", "is-active", &format!("{}.service", service)])
.output()?;
// Service not found in D-Bus cache - treat as inactive
Ok(("inactive".to_string(), "LoadState=not-found\nActiveState=inactive\nSubState=dead".to_string()))
}
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)
let output = Command::new("timeout")
.args(&["2", "systemctl", "show", &format!("{}.service", service), "--property=LoadState,ActiveState,SubState"])
.output()?;
// Get systemd manager proxy
let manager_proxy = zbus::Proxy::new(
&connection,
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
).await.ok()?;
let detailed_info = String::from_utf8(output.stdout)?;
Ok((active_status, detailed_info))
// Get unit path for service
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*)
@ -407,21 +440,12 @@ impl SystemdCollector {
return Ok(0.0);
}
// No configured path - try to get WorkingDirectory from systemctl (with 2 second timeout)
let output = Command::new("timeout")
.args(&["2", "systemctl", "show", &format!("{}.service", service_name), "--property=WorkingDirectory"])
.output()
.map_err(|e| CollectorError::SystemRead {
path: format!("WorkingDirectory for {}", service_name),
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));
// No configured path - try to get WorkingDirectory from D-Bus
if let Some(value) = self.get_unit_property(service_name, "WorkingDirectory").await {
// WorkingDirectory is a string property - try to extract as string
if let Ok(dir_str) = <String>::try_from(value) {
if !dir_str.is_empty() && dir_str != "/" && dir_str != "[not set]" {
return Ok(self.get_directory_size(&dir_str).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> {
let output = Command::new("systemctl")
.args(&["show", &format!("{}.service", service_name), "--property=MemoryCurrent"])
.output()
.map_err(|e| CollectorError::SystemRead {
path: format!("memory usage for {}", service_name),
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
}
}
}
// Get MemoryCurrent property from D-Bus
if let Some(value) = self.get_unit_property(service_name, "MemoryCurrent").await {
// MemoryCurrent is a u64 property (bytes) - try to extract
if let Ok(memory_bytes) = <u64>::try_from(value) {
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)
fn get_nginx_sites_internal(&self) -> Vec<(String, f32)> {
async fn get_nginx_sites_internal(&self) -> Vec<(String, f32)> {
let mut sites = Vec::new();
// 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)
for (site_name, url) in &discovered_sites {
@ -558,9 +568,9 @@ impl SystemdCollector {
}
/// 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
let config_content = match self.get_nginx_config_from_systemd() {
let config_content = match self.get_nginx_config_from_systemd().await {
Some(content) => content,
None => {
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())
}
/// Get nginx config from systemd service definition (NixOS compatible)
fn get_nginx_config_from_systemd(&self) -> Option<String> {
let output = Command::new("systemctl")
.args(&["show", "nginx", "--property=ExecStart", "--no-pager"])
.output()
.ok()?;
/// Get nginx config from systemd service definition via D-Bus (NixOS compatible)
async fn get_nginx_config_from_systemd(&self) -> Option<String> {
// Get ExecStart property from D-Bus
let value = self.get_unit_property("nginx", "ExecStart").await?;
if !output.status.success() {
debug!("Failed to get nginx ExecStart from systemd");
return None;
}
// ExecStart is a complex structure: array of (path, args, unclean_exit)
// For our purposes, we need to extract the command line
let exec_start_str = format!("{:?}", value);
debug!("nginx ExecStart from D-Bus: {}", exec_start_str);
let stdout = String::from_utf8_lossy(&output.stdout);
debug!("systemctl show nginx output: {}", stdout);
// Parse ExecStart to extract -c config path
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();
}
}
// Extract config path from ExecStart structure
if let Some(config_path) = self.extract_config_path_from_exec_start(&exec_start_str) {
debug!("Extracted config path: {}", config_path);
return std::fs::read_to_string(&config_path).ok();
}
None

View File

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

View File

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