From 7f7e345ec71425a61a823912f6a439d83d4f7e75 Mon Sep 17 00:00:00 2001 From: Justin Schneck Date: Fri, 15 Aug 2025 12:51:29 -0400 Subject: [PATCH] fix status messages from seeing phantom extensions --- src/commands/ext.rs | 83 +++++++++++++++-------------- tests/ext_integration_tests.rs | 3 +- tests/fixtures/mock-systemd-confext | 11 ++-- tests/fixtures/mock-systemd-sysext | 17 +++--- 4 files changed, 63 insertions(+), 51 deletions(-) diff --git a/src/commands/ext.rs b/src/commands/ext.rs index 47e639f..7c43d16 100644 --- a/src/commands/ext.rs +++ b/src/commands/ext.rs @@ -402,61 +402,64 @@ struct MountedExtension { hierarchy: String, } -/// Get mounted extensions from systemd +/// Get mounted extensions from systemd using JSON format fn get_mounted_systemd_extensions(command: &str) -> Result, SystemdError> { let mut mounted = Vec::new(); - let output = run_systemd_command(command, &["status"])?; + let output = run_systemd_command(command, &["status", "--json=short"])?; if output.trim().is_empty() { return Ok(mounted); } - let lines: Vec<&str> = output.lines().collect(); - let mut current_hierarchy = String::new(); - let mut current_since = String::new(); - - // Skip header and process data lines - for line in lines - .iter() - .skip_while(|line| line.starts_with("HIERARCHY") || line.trim().is_empty()) - { - if line.trim().is_empty() { - continue; - } - - let parts: Vec<&str> = line.split_whitespace().collect(); + // Parse JSON output + let json_data: serde_json::Value = + serde_json::from_str(&output).map_err(|e| SystemdError::CommandFailed { + command: format!("{command} status --json=short"), + source: std::io::Error::new(std::io::ErrorKind::InvalidData, e), + })?; - if parts.is_empty() { - continue; - } + // Handle both single object and array formats + let hierarchies = if json_data.is_array() { + json_data.as_array().unwrap() + } else { + std::slice::from_ref(&json_data) + }; - // Check if this line starts with a hierarchy path (doesn't start with whitespace) - if !line.starts_with(' ') && !line.starts_with('\t') { - // Parse format: HIERARCHY EXTENSIONS SINCE - if parts.len() >= 3 { - current_hierarchy = parts[0].to_string(); - let extensions = parts[1]; - current_since = parts[2..].join(" "); + for hierarchy_obj in hierarchies { + let hierarchy = hierarchy_obj["hierarchy"] + .as_str() + .unwrap_or("unknown") + .to_string(); + + let since_timestamp = hierarchy_obj["since"].as_u64(); + let since = if let Some(ts) = since_timestamp { + // Convert microseconds timestamp to human readable format + let secs = ts / 1_000_000; + // For now, use a simple format. In the future we could add chrono for better formatting + format!("timestamp:{secs}") + } else { + "-".to_string() + }; - // Split multiple extensions (comma-separated) - for ext_name in extensions.split(',') { + // Handle extensions field - can be string "none" or array of strings + if let Some(extensions) = hierarchy_obj["extensions"].as_array() { + // Array of extension names + for ext in extensions { + if let Some(ext_name) = ext.as_str() { mounted.push(MountedExtension { - name: ext_name.trim().to_string(), - since: current_since.clone(), - hierarchy: current_hierarchy.clone(), + name: ext_name.to_string(), + since: since.clone(), + hierarchy: hierarchy.clone(), }); } } - } else { - // This line starts with whitespace - it's an extension for the current hierarchy - let extension_name = parts[0]; - - // Only add if we have a current hierarchy set - if !current_hierarchy.is_empty() { + } else if let Some(ext_str) = hierarchy_obj["extensions"].as_str() { + // Single string - skip if it's "none" + if ext_str != "none" { mounted.push(MountedExtension { - name: extension_name.trim().to_string(), - since: current_since.clone(), - hierarchy: current_hierarchy.clone(), + name: ext_str.to_string(), + since: since.clone(), + hierarchy: hierarchy.clone(), }); } } diff --git a/tests/ext_integration_tests.rs b/tests/ext_integration_tests.rs index 57517d9..bc6aad5 100644 --- a/tests/ext_integration_tests.rs +++ b/tests/ext_integration_tests.rs @@ -684,8 +684,7 @@ fn test_ext_merge_multiple_extensions_single_depmod() { assert!( has_network_modules || has_storage_modules || has_gpu_modules || has_sound_modules, - "Should load modules from multiple extensions. Stdout: {}", - stdout + "Should load modules from multiple extensions. Stdout: {stdout}" ); assert!( diff --git a/tests/fixtures/mock-systemd-confext b/tests/fixtures/mock-systemd-confext index e6fccba..d03fecf 100755 --- a/tests/fixtures/mock-systemd-confext +++ b/tests/fixtures/mock-systemd-confext @@ -44,9 +44,14 @@ case "$ACTION" in fi ;; status) - # Mock status output similar to real systemd-confext - echo "HIERARCHY EXTENSIONS SINCE " - echo "/etc config-ext-1 Tue 2025-01-14 15:30:10 UTC" + if [ "$JSON" = "short" ]; then + # JSON format similar to real systemd-confext + echo '[{"hierarchy":"/etc","extensions":["config-ext-1"],"since":1705243810000000}]' + else + # Mock status output similar to real systemd-confext + echo "HIERARCHY EXTENSIONS SINCE " + echo "/etc config-ext-1 Tue 2025-01-14 15:30:10 UTC" + fi ;; *) echo "Invalid action: $ACTION" >&2 diff --git a/tests/fixtures/mock-systemd-sysext b/tests/fixtures/mock-systemd-sysext index 2a2840e..18a4a35 100755 --- a/tests/fixtures/mock-systemd-sysext +++ b/tests/fixtures/mock-systemd-sysext @@ -44,12 +44,17 @@ case "$ACTION" in fi ;; status) - # Mock status output similar to real systemd-sysext - echo "HIERARCHY EXTENSIONS SINCE " - echo "/opt test-ext-1 Tue 2025-01-14 15:30:00 UTC" - echo " test-ext-2 " - echo "/usr test-ext-1 Tue 2025-01-14 15:30:05 UTC" - echo " test-ext-2 " + if [ "$JSON" = "short" ]; then + # JSON format similar to real systemd-sysext + echo '[{"hierarchy":"/opt","extensions":"none","since":null},{"hierarchy":"/usr","extensions":["test-ext-1","test-ext-2"],"since":1705243805000000}]' + else + # Mock status output similar to real systemd-sysext + echo "HIERARCHY EXTENSIONS SINCE " + echo "/opt test-ext-1 Tue 2025-01-14 15:30:00 UTC" + echo " test-ext-2 " + echo "/usr test-ext-1 Tue 2025-01-14 15:30:05 UTC" + echo " test-ext-2 " + fi ;; *) echo "Invalid action: $ACTION" >&2