1use super::{
16 atomic_file::{self, exists},
17 bootstrap::{self, BootstrapLease},
18 creation, device_reset,
19 file_lock::FileLock,
20 lock,
21 protection::Protection,
22 settings, BootstrapDirectoryError, BootstrapStore, DeviceReset, DirectoryCreationError,
23 FileError, ReplacedBy, ReplacementPhase, ReplacementRecord, StoreDir, StoreDirAccess,
24 StoreLock, StoreLockError,
25};
26use crate::id_source::{IdSource, StoreId};
27use crate::locks::lock_unpoisoned;
28use std::{
29 fs, io,
30 path::{Path, PathBuf},
31 sync::Mutex,
32};
33
34pub struct ReplacementStore {
38 _closing: lock::StoreClosingGuard,
41 path: PathBuf,
42 id: StoreId,
43 reset: DeviceReset,
44 by: ReplacedBy,
45 lease: BootstrapLease,
46 writer: Mutex<Option<StoreLock>>,
48 readers: Mutex<Option<FileLock>>,
51 record: Mutex<Option<ReplacementRecord>>,
53 loading: Mutex<Option<BootstrapStore>>,
56 protection: Protection,
57 bugs: crate::task::BugSinkRef,
58}
59
60impl ReplacementStore {
61 pub fn device_reset(&self) -> DeviceReset {
63 self.reset
64 }
65
66 pub fn is_swapped(&self) -> bool {
68 lock_unpoisoned(&self.loading).is_none()
69 }
70
71 pub fn directory(&self) -> StoreDir {
74 match &*lock_unpoisoned(&self.loading) {
75 Some(loading) => loading.directory(),
76 None => StoreDir::bootstrapping(
77 self.path.clone(),
78 self.id,
79 self.lease.instance(),
80 self.protection,
81 self.bugs.clone(),
82 ),
83 }
84 }
85
86 pub fn discarded_database_path(&self) -> PathBuf {
89 match &*lock_unpoisoned(&self.loading) {
90 Some(_) => self.path.join(super::directory::DATABASE),
91 None => device_reset::discarded_database(&self.path, self.reset.old_device),
92 }
93 }
94
95 pub fn is_recorded(&self) -> bool {
97 lock_unpoisoned(&self.record).is_some()
98 }
99
100 pub fn record(&self, name: String) -> Result<(), BootstrapDirectoryError> {
107 let mut recorded = lock_unpoisoned(&self.record);
108 if recorded.is_some() {
109 return Ok(());
110 }
111 let readers = self.exclude_readers()?;
112 let record = ReplacementRecord {
113 reset: self.reset,
114 by: self.by,
115 name,
116 phase: ReplacementPhase::Registering,
117 };
118 let kept =
119 device_reset::record(&self.path, &record).map_err(DirectoryCreationError::from)?;
120 *lock_unpoisoned(&self.readers) = Some(readers);
121 *recorded = Some(kept);
122 Ok(())
123 }
124
125 pub fn swap(&self, name: String) -> Result<(), BootstrapDirectoryError> {
133 let mut loading = lock_unpoisoned(&self.loading);
134 if loading.is_none() {
135 return Ok(());
136 }
137 let held = lock_unpoisoned(&self.readers).take();
139 let _readers = match held {
140 Some(readers) => readers,
141 None => self.exclude_readers()?,
142 };
143 let recorded = lock_unpoisoned(&self.record).clone();
144 let record = match recorded {
145 Some(record) => record,
146 None => device_reset::record(
147 &self.path,
148 &ReplacementRecord {
149 reset: self.reset,
150 by: self.by,
151 name,
152 phase: ReplacementPhase::Swapping,
153 },
154 )
155 .map_err(DirectoryCreationError::from)?,
156 };
157 device_reset::swapping(&self.path, &record).map_err(DirectoryCreationError::from)?;
158 device_reset::forward(&self.path, self.id, self.reset)?;
159 drop(loading.take());
160 self.release_writer();
161 Ok(())
162 }
163
164 pub fn abandon(self) -> Result<(), BootstrapDirectoryError> {
171 assert!(!self.is_swapped(), "only an unswapped replacement is abandoned");
172 device_reset::finish(&self.path)?;
173 lock_unpoisoned(&self.record).take();
174 lock_unpoisoned(&self.readers).take();
175 self.cancel()
176 }
177
178 fn exclude_readers(&self) -> Result<FileLock, StoreLockError> {
179 lock::exclude_readers(&self.path, self.id)
180 }
181
182 pub async fn remove_replaced(&self) -> Result<(), BootstrapDirectoryError> {
187 assert!(self.is_swapped(), "the old installation is removed after the swap");
188 let path = self.path.clone();
189 let old = self.reset.old_device;
190 crate::task::blocking_completing(
191 "remove the replaced installation",
192 self.bugs.clone(),
193 move || device_reset::discard(&path, old),
194 )
195 .await
196 .map_err(FileError::from)??;
197 Ok(())
198 }
199
200 pub fn publish(&self) -> Result<(), BootstrapDirectoryError> {
205 assert!(self.is_swapped(), "publication follows the swap");
206 remove_area(&self.path)?;
207 let published = |source: io::Error| -> BootstrapDirectoryError {
208 DirectoryCreationError::Published { id: self.id, source }.into()
209 };
210 let result = bootstrap::remove_marker(&self.path, published);
211 self.lease.release();
212 result?;
213 device_reset::finish(&self.path).map_err(|error| published(io::Error::other(error)))
214 }
215
216 pub fn cancel(self) -> Result<(), BootstrapDirectoryError> {
219 assert!(!self.is_recorded(), "a recorded replacement only moves forward");
220 let loading = lock_unpoisoned(&self.loading)
221 .take()
222 .expect("only an unswapped replacement is cancelled");
223 loading.cancel()?;
224 drop(loading);
225 Ok(remove_area(&self.path)?)
226 }
227
228 fn release_writer(&self) {
229 lock_unpoisoned(&self.writer).take();
230 }
231}
232
233fn remove_area(directory: &Path) -> Result<(), FileError> {
234 let area = directory.join(device_reset::AREA);
235 if exists("inspect replacement area", &area)? {
236 fs::remove_dir_all(&area)
237 .map_err(|source| FileError::at("remove replacement area", &area, source))?;
238 atomic_file::sync_publication(&area)?;
239 }
240 Ok(())
241}
242
243pub(super) async fn begin(
247 root: &Path,
248 id: StoreId,
249 by: ReplacedBy,
250 ids: &dyn IdSource,
251 protection: Protection,
252 bugs: crate::task::BugSinkRef,
253) -> Result<ReplacementStore, BootstrapDirectoryError> {
254 let root = fs::canonicalize(root)
255 .map_err(|source| FileError::at("resolve store layout", root, source))?;
256 let path = root.join(id.to_string());
257 let lock = lock::acquire_bootstrap(&path, id, &bugs).await?;
258 let closing = lock.closing_guard();
259 let lease = BootstrapLease::new(lock);
260 let writer = lock::acquire_for_open(&path, id, &bugs).await?;
262 let current = settings::read(&path, id).map_err(DirectoryCreationError::Settings)?;
263 if super::deletion::read(&path)?.is_some() {
266 return Err(StoreLockError::DeletionUnfinished(id).into());
267 }
268 let pending = bootstrap::is_pending(&path)?;
269 let recorded = match device_reset::read(&path)? {
270 Some(record) if !pending && current.device_id == record.reset.new_device => {
273 device_reset::finish(&path)?;
274 None
275 }
276 recorded => recorded,
277 };
278 match &recorded {
279 Some(record) if record.by != by => {
282 return Err(StoreLockError::ReplacementUnfinished { store: id, by: record.by }.into())
283 }
284 None if pending => return Err(StoreLockError::BootstrapPending(id).into()),
285 _ => {}
286 }
287 protection.apply(&path)?;
290 let (reset, loading, readers) = match &recorded {
291 Some(record) if record.phase == ReplacementPhase::Swapping => {
293 let _readers = lock::exclude_readers(&path, id)?;
294 device_reset::forward(&path, id, record.reset)?;
295 (record.reset, None, None)
296 }
297 Some(record) => {
300 let readers = lock::exclude_readers(&path, id)?;
301 let area = path.join(device_reset::AREA);
302 let loading_path = device_reset::loading(&path, id);
303 if !exists("inspect recorded replacement", &loading_path)? {
304 return Err(FileError::at(
305 "resume a recorded replacement",
306 &loading_path,
307 io::Error::new(
308 io::ErrorKind::NotFound,
309 "the recorded replacement's installation is missing",
310 ),
311 )
312 .into());
313 }
314 let loading =
315 bootstrap::reserve_bootstrap_directory(&area, id, ids, protection, bugs.clone())
316 .await?;
317 let new =
318 loading.directory().settings().map_err(DirectoryCreationError::Settings)?.device_id;
319 if new != record.reset.new_device {
320 return Err(FileError::at(
321 "resume a recorded replacement",
322 &loading_path,
323 io::Error::new(
324 io::ErrorKind::InvalidData,
325 "the loaded installation is not the recorded fresh device",
326 ),
327 )
328 .into());
329 }
330 (record.reset, Some(loading), Some(readers))
331 }
332 None => {
333 let area = path.join(device_reset::AREA);
334 if !exists("inspect loading replacement", &device_reset::loading(&path, id))? {
336 remove_area(&path)?;
337 }
338 creation::create_directory(&area)
339 .map_err(|source| FileError::at("create replacement area", &area, source))?;
340 let loading =
341 bootstrap::reserve_bootstrap_directory(&area, id, ids, protection, bugs.clone())
342 .await?;
343 let new =
344 loading.directory().settings().map_err(DirectoryCreationError::Settings)?.device_id;
345 (DeviceReset { old_device: current.device_id, new_device: new }, Some(loading), None)
346 }
347 };
348 let writer = loading.is_some().then_some(writer);
350 Ok(ReplacementStore {
351 _closing: closing,
352 path,
353 id,
354 reset,
355 by,
356 lease,
357 writer: Mutex::new(writer),
358 readers: Mutex::new(readers),
359 record: Mutex::new(recorded),
360 loading: Mutex::new(loading),
361 protection,
362 bugs,
363 })
364}
365
366#[cfg(test)]
367#[path = "replacement_tests.rs"]
368mod tests;