mod binaries;
mod command;
mod systemd;
mod units;
use std::collections::BTreeMap;
use std::path::Path;
use crate::state::modal::{ServiceImpact, ServiceRestartDecision};
use crate::state::{PackageItem, PreflightAction};
use binaries::collect_binaries_for_package;
use systemd::{fetch_active_service_binaries, fetch_active_units};
use units::collect_service_units_for_package;
pub fn resolve_service_impacts(
items: &[PackageItem],
action: PreflightAction,
) -> Vec<ServiceImpact> {
let _span = tracing::info_span!(
"resolve_service_impacts",
stage = "services",
item_count = items.len()
)
.entered();
let start_time = std::time::Instant::now();
let mut unit_to_providers: BTreeMap<String, Vec<String>> = BTreeMap::new();
for item in items {
match collect_service_units_for_package(&item.name, &item.source) {
Ok(units) => {
for unit in units {
let providers = unit_to_providers.entry(unit).or_default();
if !providers.iter().any(|name| name == &item.name) {
providers.push(item.name.clone());
}
}
}
Err(err) => {
if matches!(item.source, crate::state::types::Source::Official { .. }) {
tracing::warn!(
"Failed to resolve service units for package {}: {}",
item.name,
err
);
} else {
tracing::debug!(
"Could not resolve service units for AUR package {} (expected): {}",
item.name,
err
);
}
}
}
}
let active_units = fetch_active_units().unwrap_or_else(|err| {
tracing::warn!("Unable to query active services: {}", err);
std::collections::BTreeSet::new()
});
if !active_units.is_empty() {
let active_service_binaries = fetch_active_service_binaries(&active_units);
for item in items {
match collect_binaries_for_package(&item.name, &item.source) {
Ok(binaries) => {
for binary in binaries {
for (unit_name, service_binaries) in &active_service_binaries {
if service_binaries.iter().any(|sb| {
sb == &binary
|| binary.ends_with(sb)
|| sb.ends_with(&binary)
|| (binary.contains('/')
&& sb.contains('/')
&& Path::new(sb).file_name()
== Path::new(&binary).file_name())
}) {
let providers =
unit_to_providers.entry(unit_name.clone()).or_default();
if !providers.iter().any(|name| name == &item.name) {
providers.push(item.name.clone());
let action_desc = if matches!(action, PreflightAction::Install)
{
"installing"
} else {
"removing"
};
tracing::debug!(
"Detected binary impact: {} package {} provides {} used by active service {}",
action_desc,
item.name,
binary,
unit_name
);
}
}
}
}
}
Err(err) => {
tracing::debug!(
"Failed to collect binaries for package {}: {}",
item.name,
err
);
}
}
}
}
let results: Vec<ServiceImpact> = unit_to_providers
.into_iter()
.map(|(unit_name, mut providers)| {
providers.sort();
let is_active = active_units.contains(&unit_name);
let needs_restart = matches!(action, PreflightAction::Install) && is_active;
let recommended_decision = if needs_restart {
ServiceRestartDecision::Restart
} else {
ServiceRestartDecision::Defer
};
ServiceImpact {
unit_name,
providers,
is_active,
needs_restart,
recommended_decision,
restart_decision: recommended_decision,
}
})
.collect();
let elapsed = start_time.elapsed();
let duration_ms = u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX);
tracing::info!(
stage = "services",
item_count = items.len(),
result_count = results.len(),
duration_ms = duration_ms,
"Service resolution complete"
);
results
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::modal::ServiceRestartDecision;
#[test]
fn recommended_decision_default_is_defer_when_inactive() {
let impact = ServiceImpact {
unit_name: "example.service".into(),
providers: vec!["pkg".into()],
is_active: false,
needs_restart: false,
recommended_decision: ServiceRestartDecision::Defer,
restart_decision: ServiceRestartDecision::Defer,
};
assert_eq!(impact.recommended_decision, ServiceRestartDecision::Defer);
}
}