running_process/cleanup/
mod.rs1use std::path::{Path, PathBuf};
8use std::time::{SystemTime, UNIX_EPOCH};
9
10use crate::broker::protocol::{CacheManifest, CacheRoot, CacheRootKind, StorageDisposition};
11
12pub mod instances;
14pub mod list;
16pub mod prune;
18pub mod uninstall;
20pub mod verify_artifacts;
22pub mod verify_basic;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct CleanupAction {
28 pub service_name: String,
30 pub service_version: String,
32 pub path: PathBuf,
34 pub reason: String,
36 pub deleted: bool,
38 pub skipped: bool,
40 pub skip_reason: Option<String>,
42}
43
44#[derive(Debug, thiserror::Error)]
46pub enum CleanupError {
47 #[error(transparent)]
49 Manifest(#[from] crate::broker::manifest::ManifestError),
50 #[error("cleanup I/O failed: {0}")]
52 Io(#[from] std::io::Error),
53 #[error("{0}")]
55 User(String),
56}
57
58pub fn now_unix_ms() -> u64 {
60 SystemTime::now()
61 .duration_since(UNIX_EPOCH)
62 .map(|d| d.as_millis() as u64)
63 .unwrap_or(0)
64}
65
66pub fn parse_duration_secs(input: &str) -> Result<u64, CleanupError> {
68 if input.is_empty() {
69 return Err(CleanupError::User("duration must not be empty".into()));
70 }
71 let (digits, suffix) = input.split_at(input.len() - 1);
72 let value: u64 = digits
73 .parse()
74 .map_err(|_| CleanupError::User(format!("invalid duration: {input}")))?;
75 match suffix {
76 "d" => Ok(value * 24 * 60 * 60),
77 "h" => Ok(value * 60 * 60),
78 "m" => Ok(value * 60),
79 "s" => Ok(value),
80 _ => Err(CleanupError::User(format!(
81 "duration must end with d, h, m, or s: {input}"
82 ))),
83 }
84}
85
86pub(crate) fn root_disposition(root: &CacheRoot) -> i32 {
87 root.disposition
88}
89
90pub(crate) fn root_kind(root: &CacheRoot) -> i32 {
91 root.kind
92}
93
94pub(crate) fn root_is_config(root: &CacheRoot) -> bool {
95 root_kind(root) == CacheRootKind::CacheConfig as i32
96}
97
98pub(crate) fn root_is_prunable(root: &CacheRoot) -> bool {
99 !matches!(
100 root_disposition(root),
101 x if x == StorageDisposition::NeverPrune as i32
102 || x == StorageDisposition::PreserveAcrossUninstall as i32
103 )
104}
105
106pub(crate) fn delete_path(path: &Path) -> Result<(), CleanupError> {
107 match std::fs::symlink_metadata(path) {
108 Ok(meta) if meta.is_dir() => std::fs::remove_dir_all(path)?,
109 Ok(_) => std::fs::remove_file(path)?,
110 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
111 Err(err) => return Err(CleanupError::Io(err)),
112 }
113 Ok(())
114}
115
116pub(crate) fn json_escape(s: &str) -> String {
117 let mut out = String::with_capacity(s.len());
118 for c in s.chars() {
119 match c {
120 '"' => out.push_str("\\\""),
121 '\\' => out.push_str("\\\\"),
122 '\n' => out.push_str("\\n"),
123 '\r' => out.push_str("\\r"),
124 '\t' => out.push_str("\\t"),
125 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
126 c => out.push(c),
127 }
128 }
129 out
130}
131
132pub(crate) fn manifest_json(manifest: &CacheManifest) -> String {
133 let roots = manifest
134 .roots
135 .iter()
136 .map(|root| {
137 format!(
138 "{{\"path\":\"{}\",\"kind\":{},\"disposition\":{},\"estimated_size_bytes\":{}}}",
139 json_escape(&root.path),
140 root.kind,
141 root.disposition,
142 root.estimated_size_bytes
143 )
144 })
145 .collect::<Vec<_>>()
146 .join(",");
147 format!(
148 "{{\"service_name\":\"{}\",\"service_version\":\"{}\",\"broker_instance\":\"{}\",\"last_active_unix_ms\":{},\"roots\":[{}]}}",
149 json_escape(&manifest.service_name),
150 json_escape(&manifest.service_version),
151 json_escape(&manifest.broker_instance),
152 manifest.last_active_unix_ms,
153 roots
154 )
155}
156
157pub fn actions_json(schema_version: u32, actions: &[CleanupAction]) -> String {
159 let actions = actions
160 .iter()
161 .map(|action| {
162 format!(
163 "{{\"service_name\":\"{}\",\"service_version\":\"{}\",\"path\":\"{}\",\"reason\":\"{}\",\"deleted\":{},\"skipped\":{},\"skip_reason\":{}}}",
164 json_escape(&action.service_name),
165 json_escape(&action.service_version),
166 json_escape(&action.path.to_string_lossy()),
167 json_escape(&action.reason),
168 action.deleted,
169 action.skipped,
170 match &action.skip_reason {
171 Some(reason) => format!("\"{}\"", json_escape(reason)),
172 None => "null".to_string(),
173 }
174 )
175 })
176 .collect::<Vec<_>>()
177 .join(",");
178 format!("{{\"schema_version\":{schema_version},\"actions\":[{actions}]}}")
179}