use crate::state::modal::ServiceImpact;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::ErrorKind;
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceCache {
pub install_list_signature: Vec<String>,
pub services: Vec<ServiceImpact>,
}
#[must_use]
pub fn compute_signature(packages: &[crate::state::PackageItem]) -> Vec<String> {
let mut names: Vec<String> = packages.iter().map(|p| p.name.clone()).collect();
names.sort();
names
}
pub fn load_cache(path: &PathBuf, current_signature: &[String]) -> Option<Vec<ServiceImpact>> {
let raw = match fs::read_to_string(path) {
Ok(contents) => contents,
Err(e) if e.kind() == ErrorKind::NotFound => {
tracing::debug!(path = %path.display(), "[Cache] Service cache not found");
return None;
}
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"[Cache] Failed to read service cache"
);
return None;
}
};
let cache: ServiceCache = match serde_json::from_str(&raw) {
Ok(cache) => cache,
Err(e) => {
tracing::warn!(
path = %path.display(),
error = %e,
"[Cache] Failed to parse service cache"
);
return None;
}
};
let mut cached_sig = cache.install_list_signature.clone();
cached_sig.sort();
let mut current_sig = current_signature.to_vec();
current_sig.sort();
if cached_sig == current_sig {
tracing::info!(path = %path.display(), count = cache.services.len(), "loaded service cache");
return Some(cache.services);
}
tracing::debug!(path = %path.display(), "service cache signature mismatch, ignoring");
None
}
pub fn save_cache(path: &PathBuf, signature: &[String], services: &[ServiceImpact]) {
let cache = ServiceCache {
install_list_signature: signature.to_vec(),
services: services.to_vec(),
};
if let Ok(s) = serde_json::to_string(&cache) {
let _ = fs::write(path, s);
tracing::debug!(path = %path.display(), count = services.len(), "saved service cache");
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::modal::{ServiceImpact, ServiceRestartDecision};
use crate::state::{PackageItem, Source};
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_path(label: &str) -> std::path::PathBuf {
let mut path = std::env::temp_dir();
path.push(format!(
"pacsea_services_cache_{label}_{}_{}.json",
std::process::id(),
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("System time is before UNIX epoch")
.as_nanos()
));
path
}
fn sample_packages() -> Vec<PackageItem> {
vec![
PackageItem {
name: "sshd".into(),
version: "9.0.0".into(),
description: String::new(),
source: Source::Official {
repo: "core".into(),
arch: "x86_64".into(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
PackageItem {
name: "nginx".into(),
version: "1.24.0".into(),
description: String::new(),
source: Source::Official {
repo: "extra".into(),
arch: "x86_64".into(),
},
popularity: None,
out_of_date: None,
orphaned: false,
},
]
}
fn sample_services() -> Vec<ServiceImpact> {
vec![ServiceImpact {
unit_name: "sshd.service".into(),
providers: vec!["sshd".into()],
is_active: true,
needs_restart: true,
recommended_decision: ServiceRestartDecision::Restart,
restart_decision: ServiceRestartDecision::Restart,
}]
}
#[test]
fn compute_signature_orders_package_names() {
let mut packages = sample_packages();
packages.reverse();
let signature = compute_signature(&packages);
assert_eq!(signature, vec![String::from("nginx"), String::from("sshd")]);
}
#[test]
fn load_cache_rejects_signature_mismatch() {
let path = temp_path("mismatch");
let packages = sample_packages();
let signature = compute_signature(&packages);
let services = sample_services();
save_cache(&path, &signature, &services);
let mismatched_signature = vec!["sshd".into(), "httpd".into()];
assert!(load_cache(&path, &mismatched_signature).is_none());
let _ = fs::remove_file(&path);
}
#[test]
fn save_and_load_cache_roundtrip() {
let path = temp_path("roundtrip");
let packages = sample_packages();
let signature = compute_signature(&packages);
let services = sample_services();
save_cache(&path, &signature, &services);
let reloaded = load_cache(&path, &signature).expect("expected cache to load");
assert_eq!(reloaded.len(), services.len());
assert_eq!(reloaded[0].unit_name, services[0].unit_name);
assert_eq!(reloaded[0].providers, services[0].providers);
assert_eq!(reloaded[0].is_active, services[0].is_active);
assert_eq!(reloaded[0].needs_restart, services[0].needs_restart);
let _ = fs::remove_file(&path);
}
}