use super::{Result, Settled, purge};
use crate::context::Context;
use crate::error::Error;
use crate::state::Account;
use crate::{home, schedule, state};
#[non_exhaustive]
pub struct Removed {
pub parks: usize,
pub pending: usize,
pub left: usize,
pub home_removed: bool,
pub schedule_removed: bool,
}
pub fn uninstall(settled: Settled) -> Result<Removed> {
let Settled {
_exclusive,
mut state,
ctx,
} = settled;
let schedule_removed = remove_schedule(&ctx)?;
let held = state.accounts.iter().filter_map(|a| a.parked.as_ref());
let left = held
.clone()
.filter(|park| state.is_foreign(&park.service))
.count();
let parks = held.count() - left;
for key in state.accounts.iter().map(Account::key).collect::<Vec<_>>() {
state.remove(&key);
}
state.active.clear();
state::save(&ctx, &state)?;
let pending = purge(&ctx, &mut state);
if pending == 0 {
crate::pending::clear(&ctx);
}
let home_removed = pending == 0 && remove_home(&ctx);
Ok(Removed {
parks: parks.saturating_sub(pending),
pending,
left,
home_removed,
schedule_removed,
})
}
fn remove_schedule(ctx: &Context) -> Result<bool> {
if !schedule::serves(ctx) {
return Ok(false);
}
match schedule::uninstall(ctx) {
Err(Error::ScheduleUnsupported) => Ok(false),
removed => removed,
}
}
fn remove_home(ctx: &Context) -> bool {
std::fs::remove_dir_all(home::dir(ctx)).is_ok()
}
#[cfg(test)]
mod tests {
use super::super::harness::{codex_machine, machine};
use super::super::settle;
use super::*;
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn uninstalling_takes_the_renewal_schedule_with_it() {
for make in [machine, codex_machine] {
let m = make("uninstall-schedule");
schedule::install(&m.ctx).expect("scheduled");
let removed = uninstall(settle(&m.ctx, None).expect("nothing to recover").0)
.expect("uninstalled");
assert!(removed.schedule_removed);
assert!(removed.home_removed);
assert_eq!(schedule::status(&m.ctx), schedule::Installed::No);
}
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn a_schedule_that_cannot_be_taken_away_leaves_every_login_where_it_was() {
use std::os::unix::fs::PermissionsExt;
for make in [machine, codex_machine] {
let m = make("uninstall-stuck");
schedule::install(&m.ctx).expect("scheduled");
let dir = schedule::path(&m.ctx)
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
.expect("where the scheduler keeps it");
let mode = |mode| {
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(mode))
.expect("its mode changed");
};
mode(0o555);
let refused = uninstall(settle(&m.ctx, None).expect("nothing to recover").0);
mode(0o755);
let Err(error) = refused else {
panic!("{:?}: uninstalled with the schedule still there", m.which);
};
assert_eq!(error.code(), "home_unwritable");
let state = state::load(&m.ctx).expect("the account list");
assert_eq!(state.accounts.len(), 2, "{:?}", m.which);
let there = state.get(&m.key("there")).expect("`there` is enrolled");
let park = there.parked.as_ref().expect("`there` is still parked");
crate::park::load(&m.ctx, &m.key("there"), park).expect("its parked login is kept");
assert!(home::dir(&m.ctx).is_dir());
assert!(matches!(
schedule::status(&m.ctx),
schedule::Installed::Yes { .. }
));
}
}
#[test]
fn uninstalling_where_nothing_is_scheduled_is_not_a_failure() {
let m = machine("uninstall-unscheduled");
let removed =
uninstall(settle(&m.ctx, None).expect("nothing to recover").0).expect("uninstalled");
assert!(!removed.schedule_removed);
assert!(removed.home_removed);
}
#[cfg(any(target_os = "macos", target_os = "linux"))]
#[test]
fn uninstalling_another_home_leaves_the_schedule_alone() {
let m = machine("uninstall-elsewhere");
schedule::install(&m.ctx).expect("scheduled");
let elsewhere = m
.ctx
.clone()
.with_pitboard_home(m.ctx_home().join("elsewhere"));
let removed = uninstall(settle(&elsewhere, None).expect("nothing to recover").0)
.expect("uninstalled");
assert!(!removed.schedule_removed);
assert!(matches!(
schedule::status(&m.ctx),
schedule::Installed::Yes { .. }
));
}
}