pitboard_core/switch/
uninstall.rs1use super::{Result, Settled, purge};
4use crate::context::Context;
5use crate::error::Error;
6use crate::state::Account;
7use crate::{home, schedule, state};
8
9#[non_exhaustive]
12pub struct Removed {
13 pub parks: usize,
15 pub pending: usize,
17 pub left: usize,
22 pub home_removed: bool,
24 pub schedule_removed: bool,
26}
27
28pub fn uninstall(settled: Settled) -> Result<Removed> {
41 let Settled {
42 _exclusive,
43 mut state,
44 ctx,
45 } = settled;
46 let schedule_removed = remove_schedule(&ctx)?;
47 let held = state.accounts.iter().filter_map(|a| a.parked.as_ref());
48 let left = held
49 .clone()
50 .filter(|park| state.is_foreign(&park.service))
51 .count();
52 let parks = held.count() - left;
53 for key in state.accounts.iter().map(Account::key).collect::<Vec<_>>() {
54 state.remove(&key);
55 }
56 state.active.clear();
57 state::save(&ctx, &state)?;
58 let pending = purge(&ctx, &mut state);
59 if pending == 0 {
62 crate::pending::clear(&ctx);
63 }
64 let home_removed = pending == 0 && remove_home(&ctx);
65 Ok(Removed {
66 parks: parks.saturating_sub(pending),
67 pending,
68 left,
69 home_removed,
70 schedule_removed,
71 })
72}
73
74fn remove_schedule(ctx: &Context) -> Result<bool> {
77 if !schedule::serves(ctx) {
78 return Ok(false);
79 }
80 match schedule::uninstall(ctx) {
81 Err(Error::ScheduleUnsupported) => Ok(false),
82 removed => removed,
83 }
84}
85
86fn remove_home(ctx: &Context) -> bool {
89 std::fs::remove_dir_all(home::dir(ctx)).is_ok()
90}
91
92#[cfg(test)]
93mod tests {
94 use super::super::harness::{codex_machine, machine};
95 use super::super::settle;
96 use super::*;
97
98 #[cfg(any(target_os = "macos", target_os = "linux"))]
99 #[test]
100 fn uninstalling_takes_the_renewal_schedule_with_it() {
101 for make in [machine, codex_machine] {
102 let m = make("uninstall-schedule");
103 schedule::install(&m.ctx).expect("scheduled");
104
105 let removed = uninstall(settle(&m.ctx, None).expect("nothing to recover").0)
106 .expect("uninstalled");
107
108 assert!(removed.schedule_removed);
109 assert!(removed.home_removed);
110 assert_eq!(schedule::status(&m.ctx), schedule::Installed::No);
111 }
112 }
113
114 #[cfg(any(target_os = "macos", target_os = "linux"))]
117 #[test]
118 fn a_schedule_that_cannot_be_taken_away_leaves_every_login_where_it_was() {
119 use std::os::unix::fs::PermissionsExt;
120 for make in [machine, codex_machine] {
121 let m = make("uninstall-stuck");
122 schedule::install(&m.ctx).expect("scheduled");
123 let dir = schedule::path(&m.ctx)
124 .and_then(|p| p.parent().map(std::path::Path::to_path_buf))
125 .expect("where the scheduler keeps it");
126 let mode = |mode| {
127 std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(mode))
128 .expect("its mode changed");
129 };
130
131 mode(0o555);
132 let refused = uninstall(settle(&m.ctx, None).expect("nothing to recover").0);
133 mode(0o755);
134
135 let Err(error) = refused else {
136 panic!("{:?}: uninstalled with the schedule still there", m.which);
137 };
138 assert_eq!(error.code(), "home_unwritable");
139 let state = state::load(&m.ctx).expect("the account list");
140 assert_eq!(state.accounts.len(), 2, "{:?}", m.which);
141 let there = state.get(&m.key("there")).expect("`there` is enrolled");
142 let park = there.parked.as_ref().expect("`there` is still parked");
143 crate::park::load(&m.ctx, &m.key("there"), park).expect("its parked login is kept");
144 assert!(home::dir(&m.ctx).is_dir());
145 assert!(matches!(
146 schedule::status(&m.ctx),
147 schedule::Installed::Yes { .. }
148 ));
149 }
150 }
151
152 #[test]
153 fn uninstalling_where_nothing_is_scheduled_is_not_a_failure() {
154 let m = machine("uninstall-unscheduled");
155 let removed =
156 uninstall(settle(&m.ctx, None).expect("nothing to recover").0).expect("uninstalled");
157 assert!(!removed.schedule_removed);
158 assert!(removed.home_removed);
159 }
160
161 #[cfg(any(target_os = "macos", target_os = "linux"))]
164 #[test]
165 fn uninstalling_another_home_leaves_the_schedule_alone() {
166 let m = machine("uninstall-elsewhere");
167 schedule::install(&m.ctx).expect("scheduled");
168 let elsewhere = m
169 .ctx
170 .clone()
171 .with_pitboard_home(m.ctx_home().join("elsewhere"));
172
173 let removed = uninstall(settle(&elsewhere, None).expect("nothing to recover").0)
174 .expect("uninstalled");
175
176 assert!(!removed.schedule_removed);
177 assert!(matches!(
178 schedule::status(&m.ctx),
179 schedule::Installed::Yes { .. }
180 ));
181 }
182}