Skip to main content

cargo_reclaim/background/
service.rs

1mod 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}