cargo_reclaim/background/
service.rs1mod model;
2mod request;
3
4use std::fs::{self, File, OpenOptions};
5use std::path::{Path, PathBuf};
6use std::thread;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9use crate::active_process::platform_active_observation_provider;
10use crate::config::ReclaimConfig;
11use crate::persistence::PersistedTimestamp;
12use fs2::FileExt;
13
14use super::{BackgroundRunReport, BackgroundRunRequest, run_background_cleanup_cycle};
15use request::{BackgroundCycleRequestContext, scheduler_mode_from_config};
16
17pub use model::{
18 BACKGROUND_SERVICE_STATE_SCHEMA_VERSION, BackgroundServiceError, BackgroundServiceOptions,
19 BackgroundServicePaths, BackgroundServiceResult, BackgroundServiceRunSummary,
20 BackgroundServiceState, BackgroundServiceStatus, DEFAULT_BACKGROUND_CHECK_EVERY,
21};
22
23pub trait BackgroundServiceClock {
24 fn now(&mut self) -> SystemTime;
25}
26
27pub trait BackgroundServiceSleeper {
28 fn sleep(&mut self, duration: Duration);
29}
30
31pub trait BackgroundServiceCycleRunner {
32 fn run_cycle(
33 &mut self,
34 request: BackgroundRunRequest,
35 ) -> BackgroundServiceResult<BackgroundRunReport>;
36}
37
38#[derive(Debug, Default)]
39pub struct SystemBackgroundServiceClock;
40
41impl BackgroundServiceClock for SystemBackgroundServiceClock {
42 fn now(&mut self) -> SystemTime {
43 SystemTime::now()
44 }
45}
46
47#[derive(Debug, Default)]
48pub struct ThreadBackgroundServiceSleeper;
49
50impl BackgroundServiceSleeper for ThreadBackgroundServiceSleeper {
51 fn sleep(&mut self, duration: Duration) {
52 thread::sleep(duration);
53 }
54}
55
56#[derive(Debug, Default)]
57pub struct PlatformBackgroundServiceCycleRunner;
58
59impl BackgroundServiceCycleRunner for PlatformBackgroundServiceCycleRunner {
60 fn run_cycle(
61 &mut self,
62 request: BackgroundRunRequest,
63 ) -> BackgroundServiceResult<BackgroundRunReport> {
64 let provider = platform_active_observation_provider();
65 run_background_cleanup_cycle(request, &provider).map_err(BackgroundServiceError::from)
66 }
67}
68
69pub fn run_background_service(
70 options: BackgroundServiceOptions,
71 config: &ReclaimConfig,
72) -> BackgroundServiceResult<BackgroundServiceRunSummary> {
73 let mut clock = SystemBackgroundServiceClock;
74 let mut sleeper = ThreadBackgroundServiceSleeper;
75 let mut runner = PlatformBackgroundServiceCycleRunner;
76 run_background_service_with_runtime(options, config, &mut clock, &mut sleeper, &mut runner)
77}
78
79pub fn run_background_service_with_runtime(
80 options: BackgroundServiceOptions,
81 config: &ReclaimConfig,
82 clock: &mut impl BackgroundServiceClock,
83 sleeper: &mut impl BackgroundServiceSleeper,
84 runner: &mut impl BackgroundServiceCycleRunner,
85) -> BackgroundServiceResult<BackgroundServiceRunSummary> {
86 let paths = BackgroundServicePaths::new(&options.state_dir, &options.log_dir);
87 let _lock = ServiceLock::acquire(&paths.lock_path)?;
88 ensure_service_dirs(&paths)?;
89
90 let started_at = persisted_timestamp(clock.now())?;
91 let mut state = BackgroundServiceState::running(started_at);
92 write_background_service_state(&paths.state_path, &state)?;
93
94 let interval = config
95 .background
96 .check_every
97 .unwrap_or(DEFAULT_BACKGROUND_CHECK_EVERY);
98 let request_context = BackgroundCycleRequestContext::from_config(
99 config,
100 &options.config_path,
101 match options.mode {
102 Some(mode) => mode,
103 None => scheduler_mode_from_config(config)?,
104 },
105 )?;
106 let mut cycles_completed = 0;
107
108 loop {
109 let now = clock.now();
110 let stamp = build_service_timestamp(now);
111 let run_id = format!("scheduler-{stamp}");
112 let plan_path = paths.plans_dir.join(format!("cargo-reclaim-{stamp}.json"));
113 let request =
114 request_context.request(run_id.clone(), paths.runs_log_path.clone(), plan_path, now)?;
115
116 match runner.run_cycle(request) {
117 Ok(_) => {
118 state.last_problem = None;
119 state.consecutive_failures = 0;
120 }
121 Err(error) => {
122 state.consecutive_failures = state.consecutive_failures.saturating_add(1);
123 state.last_problem = Some(error.to_string());
124 }
125 }
126 cycles_completed += 1;
127
128 state.last_run_id = Some(run_id);
129 state.last_run_at = Some(persisted_timestamp(now)?);
130 let next_run_at = now + interval;
131 state.next_run_at = Some(persisted_timestamp(next_run_at)?);
132 write_background_service_state(&paths.state_path, &state)?;
133
134 if options
135 .max_cycles
136 .is_some_and(|max_cycles| cycles_completed >= max_cycles)
137 {
138 state.status = BackgroundServiceStatus::Stopped;
139 state.pid = None;
140 state.next_run_at = None;
141 write_background_service_state(&paths.state_path, &state)?;
142 return Ok(BackgroundServiceRunSummary {
143 state,
144 cycles_completed,
145 });
146 }
147
148 sleeper.sleep(interval);
149 }
150}
151
152pub fn read_background_service_state(
153 path: impl AsRef<Path>,
154) -> BackgroundServiceResult<Option<BackgroundServiceState>> {
155 let path = path.as_ref();
156 match fs::read(path) {
157 Ok(contents) => serde_json::from_slice(&contents)
158 .map(Some)
159 .map_err(|source| BackgroundServiceError::Json {
160 path: path.to_path_buf(),
161 source,
162 }),
163 Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None),
164 Err(source) => Err(BackgroundServiceError::Io {
165 path: path.to_path_buf(),
166 source,
167 }),
168 }
169}
170
171pub fn refresh_background_service_state(state: BackgroundServiceState) -> BackgroundServiceState {
172 if state.status != BackgroundServiceStatus::Running {
173 return state;
174 }
175
176 let Some(pid) = state.pid else {
177 return stale_service_state(state, "service state is running without a pid");
178 };
179
180 match process_liveness(pid) {
181 ProcessLiveness::Alive => state,
182 ProcessLiveness::Dead => stale_service_state(state, "service pid is not running"),
183 ProcessLiveness::Unknown => state,
184 }
185}
186
187pub fn write_background_service_state(
188 path: impl AsRef<Path>,
189 state: &BackgroundServiceState,
190) -> BackgroundServiceResult<()> {
191 let path = path.as_ref();
192 if let Some(parent) = path.parent() {
193 fs::create_dir_all(parent).map_err(|source| BackgroundServiceError::Io {
194 path: parent.to_path_buf(),
195 source,
196 })?;
197 }
198 let contents =
199 serde_json::to_vec_pretty(state).map_err(|source| BackgroundServiceError::Serialize {
200 path: path.to_path_buf(),
201 source,
202 })?;
203 fs::write(path, contents).map_err(|source| BackgroundServiceError::Io {
204 path: path.to_path_buf(),
205 source,
206 })
207}
208
209struct ServiceLock {
210 path: PathBuf,
211 _file: File,
212}
213
214impl ServiceLock {
215 fn acquire(path: &Path) -> BackgroundServiceResult<Self> {
216 if let Some(parent) = path.parent() {
217 fs::create_dir_all(parent).map_err(|source| BackgroundServiceError::Io {
218 path: parent.to_path_buf(),
219 source,
220 })?;
221 }
222 if path.exists() && !path.is_file() {
223 return Err(BackgroundServiceError::StaleLock {
224 lock_path: path.to_path_buf(),
225 });
226 }
227
228 let file = OpenOptions::new()
229 .read(true)
230 .write(true)
231 .create(true)
232 .truncate(false)
233 .open(path)
234 .map_err(|source| BackgroundServiceError::Io {
235 path: path.to_path_buf(),
236 source,
237 })?;
238 match file.try_lock_exclusive() {
239 Ok(()) => {
240 write_lock_file(&file, path)?;
241 Ok(Self {
242 path: path.to_path_buf(),
243 _file: file,
244 })
245 }
246 Err(source) if source.kind() == std::io::ErrorKind::WouldBlock => {
247 Err(BackgroundServiceError::AlreadyRunning {
248 lock_path: path.to_path_buf(),
249 })
250 }
251 Err(source) => Err(BackgroundServiceError::Io {
252 path: path.to_path_buf(),
253 source,
254 }),
255 }
256 }
257}
258
259impl Drop for ServiceLock {
260 fn drop(&mut self) {
261 let _ = fs::remove_file(&self.path);
262 }
263}
264
265fn write_lock_file(mut file: &File, path: &Path) -> BackgroundServiceResult<()> {
266 let state = serde_json::json!({
267 "schema_version": BACKGROUND_SERVICE_STATE_SCHEMA_VERSION,
268 "pid": std::process::id(),
269 });
270 file.set_len(0)
271 .map_err(|source| BackgroundServiceError::Io {
272 path: path.to_path_buf(),
273 source,
274 })?;
275 use std::io::{Seek, Write};
276 file.rewind().map_err(|source| BackgroundServiceError::Io {
277 path: path.to_path_buf(),
278 source,
279 })?;
280 serde_json::to_writer(&mut file, &state).map_err(|source| {
281 BackgroundServiceError::Serialize {
282 path: path.to_path_buf(),
283 source,
284 }
285 })?;
286 file.write_all(b"\n")
287 .map_err(|source| BackgroundServiceError::Io {
288 path: path.to_path_buf(),
289 source,
290 })?;
291 file.sync_all()
292 .map_err(|source| BackgroundServiceError::Io {
293 path: path.to_path_buf(),
294 source,
295 })
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299#[allow(dead_code)]
300enum ProcessLiveness {
301 Alive,
302 Dead,
303 Unknown,
304}
305
306fn process_liveness(pid: u32) -> ProcessLiveness {
307 if pid == 0 {
308 return ProcessLiveness::Dead;
309 }
310
311 #[cfg(target_os = "linux")]
312 {
313 if Path::new("/proc").join(pid.to_string()).exists() {
314 ProcessLiveness::Alive
315 } else {
316 ProcessLiveness::Unknown
317 }
318 }
319
320 #[cfg(not(target_os = "linux"))]
321 {
322 let _ = pid;
323 ProcessLiveness::Unknown
324 }
325}
326
327fn stale_service_state(mut state: BackgroundServiceState, problem: &str) -> BackgroundServiceState {
328 state.status = BackgroundServiceStatus::Stale;
329 state.pid = None;
330 state.next_run_at = None;
331 state.last_problem = Some(problem.to_owned());
332 state
333}
334
335fn ensure_service_dirs(paths: &BackgroundServicePaths) -> BackgroundServiceResult<()> {
336 for path in [&paths.state_dir, &paths.log_dir, &paths.plans_dir] {
337 fs::create_dir_all(path).map_err(|source| BackgroundServiceError::Io {
338 path: path.clone(),
339 source,
340 })?;
341 }
342 Ok(())
343}
344
345fn persisted_timestamp(time: SystemTime) -> BackgroundServiceResult<PersistedTimestamp> {
346 PersistedTimestamp::from_system_time(time).map_err(BackgroundServiceError::Timestamp)
347}
348
349fn build_service_timestamp(now: SystemTime) -> String {
350 let duration = now.duration_since(UNIX_EPOCH).unwrap_or_default();
351 let total_seconds = duration.as_secs();
352 let days = (total_seconds / 86_400) as i64;
353 let seconds_of_day = total_seconds % 86_400;
354 let (year, month, day) = civil_from_days(days);
355 let hour = seconds_of_day / 3_600;
356 let minute = (seconds_of_day % 3_600) / 60;
357 let second = seconds_of_day % 60;
358 format!("{year:04}{month:02}{day:02}T{hour:02}{minute:02}{second:02}Z")
359}
360
361fn civil_from_days(days_since_unix_epoch: i64) -> (i64, u32, u32) {
362 let shifted = days_since_unix_epoch + 719_468;
363 let era = if shifted >= 0 {
364 shifted
365 } else {
366 shifted - 146_096
367 } / 146_097;
368 let day_of_era = shifted - era * 146_097;
369 let year_of_era =
370 (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365;
371 let mut year = year_of_era + era * 400;
372 let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100);
373 let month_prime = (5 * day_of_year + 2) / 153;
374 let day = day_of_year - (153 * month_prime + 2) / 5 + 1;
375 let month = month_prime + if month_prime < 10 { 3 } else { -9 };
376 if month <= 2 {
377 year += 1;
378 }
379 (year, month as u32, day as u32)
380}