pub mod registry;
pub mod single;
pub mod volumes;
pub mod watch;
use std::collections::BTreeSet;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use crate::enforce::{self, Backend, Plan};
use crate::matcher::Matcher;
use crate::policy::{Policy, POLICY_FILE};
use crate::scan;
use registry::{Registry, State, Workspace};
pub const SWEEP_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
Begin(PathBuf),
End(PathBuf),
Forget(PathBuf),
}
pub fn reconcile(
known: &[Workspace],
on_disk: &BTreeSet<PathBuf>,
live: &BTreeSet<PathBuf>,
now: u64,
retry_failed: bool,
) -> Vec<Action> {
let mut actions = Vec::new();
for root in on_disk {
match known.iter().find(|entry| &entry.root == root) {
None => actions.push(Action::Begin(root.clone())),
Some(entry) => match &entry.state {
State::Enforced => {
if !live.contains(root) {
actions.push(Action::Begin(root.clone()));
}
}
State::Paused { until: Some(until) } if *until <= now => {
actions.push(Action::Begin(root.clone()))
}
State::Paused { .. } => {
if live.contains(root) || !entry.applied.is_empty() {
actions.push(Action::End(root.clone()));
}
}
State::Failed { .. } => {
if retry_failed {
actions.push(Action::Begin(root.clone()));
}
}
},
}
}
for entry in known {
if on_disk.contains(&entry.root) {
continue;
}
if live.contains(&entry.root) || !entry.applied.is_empty() {
actions.push(Action::End(entry.root.clone()));
} else {
actions.push(Action::Forget(entry.root.clone()));
}
}
actions
}
pub struct Supervisor {
registry: Registry,
log: Option<std::fs::File>,
pub verbose: bool,
}
impl Supervisor {
pub fn load() -> Result<Supervisor> {
let registry = Registry::load()?;
Ok(Supervisor {
log: open_log(®istry),
registry,
verbose: false,
})
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn tick(&mut self, retry_failed: bool) -> Result<Vec<Action>> {
self.registry = Registry::load()?;
let on_disk = registry::sweep(&self.registry.config);
let live: BTreeSet<PathBuf> = on_disk
.iter()
.chain(self.registry.workspaces.iter().map(|entry| &entry.root))
.filter(|root| enforce::guard::running(root))
.cloned()
.collect();
let actions = reconcile(
&self.registry.workspaces,
&on_disk,
&live,
registry::now(),
retry_failed,
);
for action in &actions {
match action {
Action::Begin(root) => self.begin(root),
Action::End(root) => self.end(root),
Action::Forget(root) => self.registry.forget(root),
}
}
if !actions.is_empty() {
self.registry.save_workspaces()?;
}
Ok(actions)
}
pub fn tick_for(&mut self, changed: &[PathBuf]) -> Result<Vec<Action>> {
if changed.iter().any(|path| self.registry.config.covers(path)) {
return self.tick(false);
}
Ok(Vec::new())
}
pub fn add_scopes(
&mut self,
roots: &[PathBuf],
depth: Option<usize>,
hooks: bool,
) -> Result<Vec<registry::ScopeChange>> {
let changes = roots
.iter()
.map(|root| self.registry.config.add(root.clone()))
.collect();
self.registry.config.hooks = hooks;
if let Some(depth) = depth {
self.registry.config.max_depth = depth;
}
self.registry.save_config()?;
Ok(changes)
}
pub fn add_scope(&mut self, root: PathBuf) -> registry::ScopeChange {
self.registry.config.add(root)
}
pub fn remove_scope(&mut self, root: &Path) -> bool {
self.registry.config.remove(root)
}
pub fn save_config(&self) -> Result<()> {
self.registry.save_config()
}
pub fn pause(&mut self, root: &Path, until: Option<u64>) -> Result<()> {
self.end(root);
self.registry.set(root, State::Paused { until }, Vec::new());
self.registry.save_workspaces()
}
pub fn resume(&mut self, root: &Path) -> Result<()> {
self.registry.forget(root);
self.begin(root);
self.registry.save_workspaces()
}
pub fn release_all(&mut self) -> Result<Vec<PathBuf>> {
let roots: Vec<PathBuf> = self
.registry
.workspaces
.iter()
.map(|entry| entry.root.clone())
.collect();
for root in &roots {
self.end(root);
self.registry.forget(root);
}
self.registry.save_workspaces()?;
Ok(roots)
}
fn begin(&mut self, root: &Path) {
let applied = match self.plan_for(root) {
Ok(applied) => applied,
Err(error) => {
let reason = format!("{error:#}");
self.say(&format!(
"cannot enforce {}: {reason}",
registry::display(root)
));
self.registry
.set(root, State::Failed { reason }, Vec::new());
return;
}
};
self.configure_agents(root);
if enforce::guard::running(root) {
self.registry.set(root, State::Enforced, applied);
return;
}
match enforce::guard::detach(root) {
Ok(()) => {
self.say(&format!(
"enforcing {} ({} paths)",
registry::display(root),
applied.len()
));
self.registry.set(root, State::Enforced, applied);
}
Err(error) => {
if enforce::guard::running(root) {
self.registry.set(root, State::Enforced, applied);
return;
}
let reason = format!("{error:#}");
self.say(&format!("cannot enforce {}: {reason}", root.display()));
self.registry
.set(root, State::Failed { reason }, Vec::new());
}
}
}
fn end(&mut self, root: &Path) {
let applied = self
.registry
.find(root)
.map(|entry| entry.applied.clone())
.unwrap_or_default();
if let Err(error) = enforce::guard::stop(root) {
self.say(&format!(
"could not release {}: {error:#}",
registry::display(root)
));
}
let cleared = enforce::guard::clear_leftovers(&applied);
self.say(&format!(
"released {} ({} cleared)",
registry::display(root),
cleared.len()
));
match self.registry.find(root).map(|entry| entry.state.clone()) {
Some(State::Paused { until }) => {
self.registry.set(root, State::Paused { until }, Vec::new())
}
_ => self.registry.forget(root),
}
}
fn configure_agents(&mut self, root: &Path) {
if !self.registry.config.hooks {
return;
}
match crate::hook::install_for(root, crate::cli::Agent::All, false) {
Ok(installed) => {
let written = installed.iter().filter(|entry| !entry.replaced).count();
if written > 0 {
self.say(&format!(
"configured {written} agents in {}",
registry::display(root)
));
}
}
Err(error) => self.say(&format!(
"could not configure the agents in {}: {error:#} — the policy is \
still enforced, but an agent will see the raw filesystem error \
rather than being told why",
registry::display(root)
)),
}
}
fn plan_for(&self, root: &Path) -> Result<Vec<PathBuf>> {
let policy = Policy::load(root)?;
let matcher = Matcher::new(&policy.patterns)?;
let found = scan::scan(&policy.root, &matcher)?;
let protected = scan::canonical_targets(&found)?;
let canonical = std::fs::canonicalize(&policy.root)?;
let _ = Plan::build(Backend::Auto, &canonical, protected.clone());
Ok(protected)
}
fn say(&mut self, message: &str) {
if self.verbose {
println!("ralon: {message}");
}
if let Some(log) = &mut self.log {
let _ = writeln!(log, "{} {message}", registry::timestamp(registry::now()));
let _ = log.flush();
}
}
}
pub fn run(supervisor: &mut Supervisor) -> Result<()> {
let _claim = single::claim().context("another Ralon supervisor is already running")?;
let state = supervisor.registry.home().to_path_buf();
let state = std::fs::canonicalize(&state).unwrap_or(state);
let mut watched = supervisor.registry.config.roots.clone();
let mut watcher = watch::start(®istrations(&watched, &state));
supervisor.say(&format!("supervisor started — {}", watcher.describe()));
supervisor.tick(true)?;
let mut swept = Instant::now();
loop {
let changed = watcher.changes(SWEEP_INTERVAL.saturating_sub(swept.elapsed()));
let policy = changed.iter().any(|path| named(path, POLICY_FILE));
let scopes = changed
.iter()
.any(|path| named(path, registry::CONFIG_FILE));
if scopes || swept.elapsed() >= SWEEP_INTERVAL {
supervisor.tick(true)?;
swept = Instant::now();
} else if policy {
supervisor.tick_for(&changed)?;
}
if supervisor.registry.config.roots != watched {
watched = supervisor.registry.config.roots.clone();
watcher = watch::start(®istrations(&watched, &state));
supervisor.say(&format!("scopes changed — {}", watcher.describe()));
}
}
}
fn registrations(roots: &[PathBuf], state: &Path) -> Vec<PathBuf> {
let mut all = roots.to_vec();
if !all.iter().any(|root| state.starts_with(root)) {
all.push(state.to_path_buf());
}
all
}
fn named(path: &Path, name: &str) -> bool {
path.file_name().is_some_and(|actual| actual == name)
}
fn open_log(registry: &Registry) -> Option<std::fs::File> {
let path = registry.log_path();
let _ = std::fs::create_dir_all(registry.home());
if std::fs::metadata(&path).map(|data| data.len()).unwrap_or(0) > 1_000_000 {
let _ = std::fs::remove_file(&path);
}
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn known(root: &str, state: State, applied: &[&str]) -> Workspace {
Workspace {
root: PathBuf::from(root),
state,
applied: applied.iter().map(PathBuf::from).collect(),
}
}
fn paths(roots: &[&str]) -> BTreeSet<PathBuf> {
roots.iter().map(PathBuf::from).collect()
}
#[test]
fn a_new_policy_file_starts_enforcement() {
let actions = reconcile(&[], &paths(&["/a"]), &paths(&[]), 0, false);
assert_eq!(actions, [Action::Begin(PathBuf::from("/a"))]);
}
#[test]
fn a_workspace_that_is_enforced_and_live_is_left_alone() {
let known = [known("/a", State::Enforced, &["/a/.env"])];
assert!(reconcile(&known, &paths(&["/a"]), &paths(&["/a"]), 0, true).is_empty());
}
#[test]
fn enforcement_that_did_not_survive_a_reboot_is_started_again() {
let known = [known("/a", State::Enforced, &["/a/.env"])];
assert_eq!(
reconcile(&known, &paths(&["/a"]), &paths(&[]), 0, false),
[Action::Begin(PathBuf::from("/a"))]
);
}
#[test]
fn a_guard_that_was_killed_is_started_again() {
let known = [
known("/a", State::Enforced, &["/a/.env"]),
known("/b", State::Enforced, &["/b/.env"]),
];
assert_eq!(
reconcile(&known, &paths(&["/a", "/b"]), &paths(&["/b"]), 0, false),
[Action::Begin(PathBuf::from("/a"))]
);
}
#[test]
fn a_removed_policy_file_releases_what_was_applied() {
let known = [known("/a", State::Enforced, &["/a/.env"])];
let actions = reconcile(&known, &paths(&[]), &paths(&["/a"]), 0, false);
assert_eq!(actions, [Action::End(PathBuf::from("/a"))]);
}
#[test]
fn a_policy_removed_while_the_supervisor_was_down_is_still_released() {
let known = [known("/a", State::Enforced, &["/a/.env"])];
assert_eq!(
reconcile(&known, &paths(&[]), &paths(&[]), 0, false),
[Action::End(PathBuf::from("/a"))]
);
}
#[test]
fn a_removed_policy_with_nothing_applied_is_only_forgotten() {
let known = [known(
"/a",
State::Failed {
reason: "bad".into(),
},
&[],
)];
let actions = reconcile(&known, &paths(&[]), &paths(&[]), 0, false);
assert_eq!(actions, [Action::Forget(PathBuf::from("/a"))]);
}
#[test]
fn each_workspace_is_decided_on_its_own() {
let known = [
known("/a", State::Enforced, &["/a/.env"]),
known(
"/b",
State::Failed {
reason: "bad".into(),
},
&[],
),
];
let actions = reconcile(
&known,
&paths(&["/a", "/b", "/c"]),
&paths(&["/a"]),
0,
true,
);
assert_eq!(
actions,
[
Action::Begin(PathBuf::from("/b")),
Action::Begin(PathBuf::from("/c")),
]
);
}
#[test]
fn a_broken_policy_is_not_retried_on_every_event() {
let known = [known(
"/a",
State::Failed {
reason: "bad".into(),
},
&[],
)];
assert!(reconcile(&known, &paths(&["/a"]), &paths(&[]), 0, false).is_empty());
assert_eq!(
reconcile(&known, &paths(&["/a"]), &paths(&[]), 0, true),
[Action::Begin(PathBuf::from("/a"))]
);
}
#[test]
fn a_pause_holds_and_then_expires() {
let known = [known("/a", State::Paused { until: Some(100) }, &[])];
assert!(reconcile(&known, &paths(&["/a"]), &paths(&[]), 99, true).is_empty());
assert_eq!(
reconcile(&known, &paths(&["/a"]), &paths(&[]), 100, true),
[Action::Begin(PathBuf::from("/a"))]
);
}
#[test]
fn an_indefinite_pause_never_expires_on_its_own() {
let known = [known("/a", State::Paused { until: None }, &[])];
assert!(reconcile(&known, &paths(&["/a"]), &paths(&[]), u64::MAX, true).is_empty());
}
#[test]
fn a_pause_that_is_still_being_enforced_is_released() {
let known = [known("/a", State::Paused { until: None }, &[])];
assert_eq!(
reconcile(&known, &paths(&["/a"]), &paths(&["/a"]), 0, false),
[Action::End(PathBuf::from("/a"))]
);
}
}