Skip to main content

rlmctl_core/guard/
mod.rs

1//! Freeze-guard engine: watch memory pressure and proactively freeze/soft-cap
2//! the non-protected app driving the pressure before the system locks up, healing
3//! itself once pressure clears. Pure engine + sampler live here; the daemon loop
4//! lives in the `rlm-guard` binary.
5
6use std::path::PathBuf;
7
8pub mod cgfs;
9pub mod effector;
10pub mod history;
11pub mod journal;
12pub mod lock;
13pub mod policy;
14pub mod report;
15pub mod resolve;
16pub mod sampler;
17pub mod service;
18pub mod systemd;
19pub mod types;
20
21pub use effector::Effector;
22pub use journal::Journal;
23pub use policy::PolicyEngine;
24pub use sampler::Sampler;
25pub use systemd::SystemdUser;
26pub use types::{Action, Intervention, Level, ProcInfo, PsiSource, Sample, Target};
27
28/// Path of a guard state file named `name`, in a per-user dir.
29///
30/// Prefers `$XDG_STATE_HOME/rlm` (`~/.local/state/rlm` by default), matching
31/// the XDG-first convention `common::Config` uses for `config_dir()`. When no
32/// state dir can be resolved (e.g. `$HOME` unset) it falls back to
33/// `$XDG_RUNTIME_DIR/rlm`; that dir is per-user and mode 0700, and the
34/// journal is boot_id-guarded, so losing it at reboot costs nothing. It never
35/// falls back to a shared, world-writable dir such as `/tmp`, where another
36/// user could pre-create or read the files. `None` means neither dir is known.
37pub fn guard_file(name: &str) -> Option<PathBuf> {
38    guard_file_from(dirs::state_dir(), dirs::runtime_dir(), name)
39}
40
41fn guard_file_from(
42    state: Option<PathBuf>,
43    runtime: Option<PathBuf>,
44    name: &str,
45) -> Option<PathBuf> {
46    state.or(runtime).map(|d| d.join("rlm").join(name))
47}
48
49/// Path of the guard's write-ahead restore journal, or `None` when no
50/// per-user dir is known (see [`guard_file`]). Without it the guard must not
51/// freeze or cap, since it could not guarantee a restore.
52pub fn try_journal_path() -> Option<PathBuf> {
53    guard_file("guard-journal.jsonl")
54}
55
56/// [`try_journal_path`] for read-only callers such as `rlm guard status`.
57/// Returns an empty path when no per-user dir is known; reading it finds
58/// nothing, which is the right answer since no guard can have written it.
59pub fn journal_path() -> PathBuf {
60    try_journal_path().unwrap_or_default()
61}
62
63/// Path of the lock file that keeps a second `rlm-guard` from running. It
64/// sits next to the journal it protects. `None` means no per-user dir is
65/// known (see [`guard_file`]); the caller then runs without the lock.
66pub fn lock_path() -> Option<PathBuf> {
67    guard_file("rlm-guard.lock")
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn guard_files_prefer_the_state_dir() {
76        assert_eq!(
77            guard_file_from(Some("/s".into()), Some("/r".into()), "rlm-guard.lock"),
78            Some(PathBuf::from("/s/rlm/rlm-guard.lock"))
79        );
80    }
81
82    #[test]
83    fn guard_files_fall_back_to_the_runtime_dir_not_tmp() {
84        assert_eq!(
85            guard_file_from(None, Some("/run/user/1000".into()), "guard-journal.jsonl"),
86            Some(PathBuf::from("/run/user/1000/rlm/guard-journal.jsonl"))
87        );
88        assert_eq!(guard_file_from(None, None, "guard-journal.jsonl"), None);
89    }
90
91    #[test]
92    fn journal_reader_path_is_empty_without_a_dir() {
93        // An empty path reads as missing; it is never under /tmp.
94        let p = try_journal_path().unwrap_or_default();
95        assert!(!p.starts_with("/tmp"), "{p:?}");
96        assert!(Journal::read_entries(&PathBuf::new(), "boot").is_empty());
97    }
98}