1
//! State helper utility
2
//!
3
//! All the methods in this module perform appropriate mistrust checks.
4
//!
5
//! All the methods arrange to ensure suitably-finegrained exclusive access.
6
//! "Read-only" or "shared" mode is not supported.
7
//!
8
//! ### Differences from `tor_persist::StorageHandle`
9
//!
10
//!  * Explicit provision is made for multiple instances of a single facility.
11
//!    For example, multiple hidden services,
12
//!    each with their own state, and own lock.
13
//!
14
//!  * Locking (via filesystem locks) is mandatory, rather than optional -
15
//!    there is no "shared" mode.
16
//!
17
//!  * Locked state is represented in the Rust type system.
18
//!
19
//!  * We don't use traits to support multiple implementations.
20
//!    Platform support would be done in the future with `#[cfg]`.
21
//!    Testing is done by temporary directories (as currently with `tor_persist`).
22
//!
23
//!  * The serde-based `StorageHandle` requires `&mut` for writing.
24
//!    This ensures proper serialisation of 1. read-modify-write cycles
25
//!    and 2. use of the temporary file.
26
//!    Or to put it another way, we model `StorageHandle`
27
//!    as *containing* a `T` without interior mutability.
28
//!
29
//!  * There's a way to get a raw directory for filesystem operations
30
//!    (currently, will be used for IPT replay logs).
31
//!
32
//! ### Implied filesystem structure
33
//!
34
//! ```text
35
//! STATE_DIR/
36
//! STATE_DIR/KIND/INSTANCE_ID/
37
//! STATE_DIR/KIND/INSTANCE_ID/lock
38
//! STATE_DIR/KIND/INSTANCE_ID/KEY.json
39
//! STATE_DIR/KIND/INSTANCE_ID/KEY.new
40
//! STATE_DIR/KIND/INSTANCE_ID/KEY/
41
//!
42
//! eg
43
//!
44
//! STATE_DIR/hss/allium-cepa.lock
45
//! STATE_DIR/hss/allium-cepa/ipts.json
46
//! STATE_DIR/hss/allium-cepa/iptpub.json
47
//! STATE_DIR/hss/allium-cepa/iptreplay/
48
//! STATE_DIR/hss/allium-cepa/iptreplay/9aa9517e6901c280a550911d3a3c679630403db1c622eedefbdf1715297f795f.bin
49
//! ```
50
//!
51
// The instance's last modification time (see `purge_instances`) is the mtime of
52
// the INSTANCE_ID directory.  The lockfile mtime is not meaningful.
53
//
54
//! (The lockfile is outside the instance directory to facilitate
55
//! concurrency-correct deletion.)
56
//!
57
// Specifically:
58
//
59
// The situation where there is only the lockfile, is an out-of-course but legal one.
60
// Likewise, a lockfile plus a *partially* deleted instance state, is also legal.
61
// Having an existing directory without associated lockfile is forbidden,
62
// but if it should occur we handle it properly.
63
//
64
//! ### Comprehensive example
65
//!
66
//! ```
67
//! use std::{collections::HashSet, fmt, time::{Duration, SystemTime}};
68
//! use tor_error::{into_internal, Bug};
69
//! use tor_persist::slug::SlugRef;
70
//! use tor_persist::state_dir;
71
//! use state_dir::{InstanceIdentity, InstancePurgeHandler};
72
//! use state_dir::{InstancePurgeInfo, InstanceStateHandle, StateDirectory, StorageHandle};
73
//! #
74
//! # // fake up some things; we do this rather than using real ones
75
//! # // since this example will move, with the module, to a lower level crate.
76
//! # struct OnionService { }
77
//! # #[derive(derive_more::Display)] struct HsNickname(String);
78
//! # type Error = anyhow::Error;
79
//! # mod ipt_mgr { pub mod persist {
80
//! #     #[derive(serde::Serialize, serde::Deserialize)] pub struct StateRecord {}
81
//! # } }
82
//!
83
//! impl InstanceIdentity for HsNickname {
84
//!     fn kind() -> &'static str { "hss" }
85
//!     fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
86
//!         write!(f, "{self}")
87
//!     }
88
//! }
89
//!
90
//! impl OnionService {
91
//!     fn new(
92
//!         nick: HsNickname,
93
//!         state_dir: &StateDirectory,
94
//!     ) -> Result<Self, Error> {
95
//!         let instance_state = state_dir.acquire_instance(&nick)?;
96
//!         let replay_log_dir = instance_state.raw_subdir("ipt_replay")?;
97
//!         let ipts_storage: StorageHandle<ipt_mgr::persist::StateRecord> =
98
//!             instance_state.storage_handle("ipts")?;
99
//!         // ..
100
//! #       Ok(OnionService { })
101
//!     }
102
//! }
103
//!
104
//! struct PurgeHandler<'h>(&'h HashSet<&'h str>, Duration);
105
//! impl InstancePurgeHandler for PurgeHandler<'_> {
106
//!     fn kind(&self) -> &'static str {
107
//!         <HsNickname as InstanceIdentity>::kind()
108
//!     }
109
//!     fn name_filter(&mut self, id: &SlugRef) -> state_dir::Result<state_dir::Liveness> {
110
//!         Ok(if self.0.contains(id.as_str()) {
111
//!             state_dir::Liveness::Live
112
//!         } else {
113
//!             state_dir::Liveness::PossiblyUnused
114
//!         })
115
//!     }
116
//!     fn age_filter(&mut self, id: &SlugRef, age: Duration)
117
//!              -> state_dir::Result<state_dir::Liveness>
118
//!     {
119
//!         Ok(if age > self.1 {
120
//!             state_dir::Liveness::PossiblyUnused
121
//!         } else {
122
//!             state_dir::Liveness::Live
123
//!         })
124
//!     }
125
//!     fn dispose(&mut self, _info: &InstancePurgeInfo, handle: InstanceStateHandle)
126
//!                -> state_dir::Result<()> {
127
//!         // here might be a good place to delete keys too
128
//!         handle.purge()
129
//!     }
130
//! }
131
//! pub fn expire_hidden_services(
132
//!     state_dir: &StateDirectory,
133
//!     currently_configured_nicks: &HashSet<&str>,
134
//!     retain_for: Duration,
135
//! ) -> Result<(), Error> {
136
//!     state_dir.purge_instances(
137
//!         SystemTime::now(),
138
//!         &mut PurgeHandler(currently_configured_nicks, retain_for),
139
//!     )?;
140
//!     Ok(())
141
//! }
142
//! ```
143
//!
144
//! ### Platforms without a filesystem
145
//!
146
//! The implementation and (in places) the documentation
147
//! is in terms of filesystems.
148
//! But, everything except `InstanceStateHandle::raw_subdir`
149
//! is abstract enough to implement some other way.
150
//!
151
//! If we wish to support such platforms, the approach is:
152
//!
153
//!  * Decide on an approach for `StorageHandle`
154
//!    and for each caller of `raw_subdir`.
155
//!
156
//!  * Figure out how the startup code will look.
157
//!    (Currently everything is in terms of `fs_mistrust` and filesystems.)
158
//!
159
//!  * Provide a version of this module with a compatible API
160
//!    in terms of whatever underlying facilities are available.
161
//!    Use `#[cfg]` to select it.
162
//!    Don't implement `raw_subdir`.
163
//!
164
//!  * Call sites using `raw_subdir` will no longer compile.
165
//!    Use `#[cfg]` at call sites to replace the `raw_subdir`
166
//!    with whatever is appropriate for the platform.
167

            
168
#![forbid(unsafe_code)] // if you remove this, enable (or write) miri tests (git grep miri)
169

            
170
use std::collections::HashSet;
171
use std::fmt::{self, Display};
172
use std::fs;
173
use std::io;
174
use std::marker::PhantomData;
175
use std::path::Path;
176
use std::sync::Arc;
177
use std::time::{Duration, SystemTime};
178

            
179
use derive_deftly::{Deftly, define_derive_deftly};
180
use derive_more::{AsRef, Deref};
181
use itertools::chain;
182
use serde::{Serialize, de::DeserializeOwned};
183

            
184
use fs_mistrust::{CheckedDir, Mistrust};
185
use tor_error::ErrorReport as _;
186
use tor_error::bad_api_usage;
187
use tracing::trace;
188

            
189
pub use crate::Error;
190
use crate::err::{Action, ErrorSource, Resource};
191
use crate::load_store;
192
use crate::slug::{BadSlug, Slug, SlugRef, TryIntoSlug};
193

            
194
#[allow(unused_imports)] // Simplifies a lot of references in our docs
195
use crate::slug;
196

            
197
define_derive_deftly! {
198
    ContainsInstanceStateGuard:
199

            
200
    impl<$tgens> ContainsInstanceStateGuard for $ttype where $twheres {
201
1470
        fn raw_lock_guard(&self) -> Arc<LockFileGuard> {
202
            self.flock_guard.clone()
203
        }
204
    }
205
}
206

            
207
/// Re-export of the lock guard type, as obtained via [`ContainsInstanceStateGuard`]
208
pub use fslock_guard::LockFileGuard;
209

            
210
use std::result::Result as StdResult;
211

            
212
use std::path::MAIN_SEPARATOR as PATH_SEPARATOR;
213

            
214
/// [`Result`](StdResult) throwing a [`state_dir::Error`](Error)
215
pub type Result<T> = StdResult<T, Error>;
216

            
217
/// Extension for lockfiles
218
const LOCK_EXTN: &str = "lock";
219
/// Suffix for lockfiles, precisely `"." + LOCK_EXTN`
220
// There's no way to concatenate constant strings with names!
221
// We could use the const_format crate maybe?
222
const DOT_LOCK: &str = ".lock";
223

            
224
/// The whole program's state directory
225
///
226
/// Representation of `[storage] state_dir` and `permissions`
227
/// from the Arti configuration.
228
///
229
/// This type does not embody any subpaths relating to
230
/// any particular facility within Arti.
231
///
232
/// Constructing a `StateDirectory` may involve filesystem permissions checks,
233
/// so ideally it would be created once per process for performance reasons.
234
///
235
/// Existence of a `StateDirectory` also does not imply exclusive access.
236
///
237
/// This type is passed to each facility's constructor;
238
/// the facility implements [`InstanceIdentity`]
239
/// and calls [`acquire_instance`](StateDirectory::acquire_instance).
240
///
241
/// ### Use for caches
242
///
243
/// In principle this type and the methods and subtypes available
244
/// would be suitable for cache data as well as state data.
245
///
246
/// However the locking scheme does not tolerate random removal of files.
247
/// And cache directories are sometimes configured to point to locations
248
/// with OS-supplied automatic file cleaning.
249
/// That would not be correct,
250
/// since the automatic file cleaner might remove an in-use lockfile,
251
/// effectively unlocking the instance state
252
/// even while a process exists that thinks it still has the lock.
253
#[derive(Debug, Clone)]
254
pub struct StateDirectory {
255
    /// The actual directory, including mistrust config
256
    dir: CheckedDir,
257
}
258

            
259
/// An instance of a facility that wants to save persistent state (caller-provided impl)
260
///
261
/// Each value of a type implementing `InstanceIdentity`
262
/// designates a specific instance of a specific facility.
263
///
264
/// For example, `HsNickname` implements `state_dir::InstanceIdentity`.
265
///
266
/// The kind and identity are [`slug`]s.
267
pub trait InstanceIdentity {
268
    /// Return the kind.  For example `hss` for a Tor Hidden Service.
269
    ///
270
    /// This must return a fixed string,
271
    /// since usually all instances represented the same Rust type
272
    /// are also the same kind.
273
    ///
274
    /// The returned value must be valid as a [`slug`].
275
    //
276
    // This precludes dynamically chosen instance kind identifiers.
277
    // If we ever want that, we'd need an InstanceKind trait that is implemented
278
    // not for actual instances, but for values representing a kind.
279
    fn kind() -> &'static str;
280

            
281
    /// Obtain identity
282
    ///
283
    /// The instance identity distinguishes different instances of the same kind.
284
    ///
285
    /// For example, for a Tor Hidden Service the identity is the nickname.
286
    ///
287
    /// The generated string must be valid as a [`slug`].
288
    /// If it is not, the functions in this module will throw `Bug` errors.
289
    /// (Returning `fmt::Error` will cause a panic, as is usual with the fmt API.)
290
    fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result;
291
}
292

            
293
/// For a facility to be expired using [`purge_instances`](StateDirectory::purge_instances) (caller-provided impl)
294
///
295
/// A filter which decides which instances to delete,
296
/// and deletes them if appropriate.
297
///
298
/// See [`purge_instances`](StateDirectory::purge_instances) for full documentation.
299
pub trait InstancePurgeHandler {
300
    /// What kind to iterate over
301
    fn kind(&self) -> &'static str;
302

            
303
    /// Can we tell by its name that this instance is still live ?
304
    fn name_filter(&mut self, identity: &SlugRef) -> Result<Liveness>;
305

            
306
    /// Can we tell by recent modification that this instance is still live ?
307
    ///
308
    /// Many implementations won't need to use the `identity` parameter.
309
    ///
310
    /// ### Concurrency
311
    ///
312
    /// The `age` passed to this callback might
313
    /// sometimes not be the most recent modification time of the instance.
314
    /// But. before calling `dispose`, `purge_instances` will call this
315
    /// function at least once with a fully up-to-date modification time.
316
    fn age_filter(&mut self, identity: &SlugRef, age: Duration) -> Result<Liveness>;
317

            
318
    /// Decide whether to keep this instance
319
    ///
320
    /// When it has made its decision, `dispose` should
321
    /// either call [`delete`](InstanceStateHandle::purge),
322
    /// or simply drop `handle`.
323
    ///
324
    /// Called only after `name_filter` and `age_filter`
325
    /// both returned [`Liveness::PossiblyUnused`].
326
    ///
327
    /// `info` includes the instance name and other useful information
328
    /// such as the last modification time.
329
    ///
330
    /// Note that although the existence of `handle` implies
331
    /// there can be no other `InstanceStateHandle`s for this instance,
332
    /// the last modification time of this instance has *not* been updated,
333
    /// as it would be by [`acquire_instance`](StateDirectory::acquire_instance).
334
    fn dispose(&mut self, info: &InstancePurgeInfo, handle: InstanceStateHandle) -> Result<()>;
335
}
336

            
337
/// Information about an instance, passed to [`InstancePurgeHandler::dispose`]
338
#[derive(Debug, Clone, amplify::Getters, AsRef)]
339
pub struct InstancePurgeInfo<'i> {
340
    /// The instance's identity string
341
    #[as_ref]
342
    identity: &'i SlugRef,
343

            
344
    /// When the instance state was last updated, according to the filesystem timestamps
345
    ///
346
    /// See `[InstanceStateHandle::purge_instances]`
347
    /// for details of what kinds of events count as modifications.
348
    last_modified: SystemTime,
349
}
350

            
351
/// Is an instance still relevant?
352
///
353
/// Returned by [`InstancePurgeHandler::name_filter`].
354
///
355
/// See [`StateDirectory::purge_instances`] for details of the semantics.
356
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
357
#[allow(clippy::exhaustive_enums)] // this is a boolean
358
pub enum Liveness {
359
    /// This instance is not known to be interesting
360
    ///
361
    /// It could be perhaps expired, if it's been long enough
362
    PossiblyUnused,
363
    /// This instance is still wanted
364
    Live,
365
}
366

            
367
/// Objects that co-own a lock on an instance
368
///
369
/// Each type implementing this trait mutually excludes independently-acquired
370
/// [`InstanceStateHandle`]s, and anything derived from them
371
/// (including, therefore, `ContainsInstanceStateGuard` implementors
372
/// with independent provenance.)
373
pub trait ContainsInstanceStateGuard {
374
    /// Obtain a raw clone of the underlying filesystem lock
375
    ///
376
    /// This lock (and clones of it) will mutually exclude
377
    /// re-acquisition of the same instance.
378
    fn raw_lock_guard(&self) -> Arc<LockFileGuard>;
379
}
380

            
381
/// Instance identity string formatter, type-erased
382
type InstanceIdWriter<'i> = &'i dyn Fn(&mut fmt::Formatter) -> fmt::Result;
383

            
384
impl StateDirectory {
385
    /// Create a new `StateDirectory` from a directory and mistrust configuration
386
126
    pub fn new(state_dir: impl AsRef<Path>, mistrust: &Mistrust) -> Result<Self> {
387
        /// Implementation, taking non-generic path
388
1739
        fn inner(path: &Path, mistrust: &Mistrust) -> Result<StateDirectory> {
389
1739
            let resource = || Resource::Directory {
390
                dir: path.to_owned(),
391
            };
392
1739
            let handle_err = |source| Error::new(source, Action::Initializing, resource());
393

            
394
1739
            let dir = mistrust
395
1739
                .verifier()
396
1739
                .make_secure_dir(path)
397
1739
                .map_err(handle_err)?;
398

            
399
1739
            Ok(StateDirectory { dir })
400
1739
        }
401
126
        inner(state_dir.as_ref(), mistrust)
402
126
    }
403

            
404
    /// Acquires (creates and locks) a storage for an instance
405
    ///
406
    /// Ensures the existence and suitability of a subdirectory named `kind/identity`,
407
    /// and locks it for exclusive access.
408
160
    pub fn acquire_instance<I: InstanceIdentity>(
409
160
        &self,
410
160
        identity: &I,
411
160
    ) -> Result<InstanceStateHandle> {
412
        /// Implementation, taking non-generic values for identity
413
1053
        fn inner(
414
1053
            sd: &StateDirectory,
415
1053
            kind_str: &'static str,
416
1053
            id_writer: InstanceIdWriter,
417
1053
        ) -> Result<InstanceStateHandle> {
418
1133
            sd.with_instance_path_pieces(kind_str, id_writer, |kind, id, resource| {
419
1053
                let handle_err =
420
2
                    |action, source: ErrorSource| Error::new(source, action, resource());
421

            
422
                // Obtain (creating if necessary) a subdir for a Checked
423
2104
                let make_secure_directory = |parent: &CheckedDir, subdir| {
424
2104
                    let resource = || Resource::Directory {
425
                        dir: parent.as_path().join(subdir),
426
                    };
427
2104
                    parent
428
2104
                        .make_secure_directory(subdir)
429
2104
                        .map_err(|source| Error::new(source, Action::Initializing, resource()))
430
2104
                };
431

            
432
                // ---- obtain the lock ----
433

            
434
1053
                let kind_dir = make_secure_directory(&sd.dir, kind)?;
435

            
436
1053
                let lock_path = kind_dir
437
1053
                    .join(format!("{id}.{LOCK_EXTN}"))
438
1053
                    .map_err(|source| handle_err(Action::Initializing, source.into()))?;
439

            
440
1053
                let flock_guard = match LockFileGuard::try_lock(&lock_path) {
441
1051
                    Ok(Some(y)) => {
442
1051
                        trace!("locked {lock_path:?}");
443
1051
                        y.into()
444
                    }
445
                    Err(source) => {
446
                        trace!("locking {lock_path:?}, error {}", source.report());
447
                        return Err(handle_err(Action::Locking, source.into()));
448
                    }
449
                    Ok(None) => {
450
2
                        trace!("locking {lock_path:?}, in use",);
451
2
                        return Err(handle_err(Action::Locking, ErrorSource::AlreadyLocked));
452
                    }
453
                };
454

            
455
                // ---- we have the lock, calculate the directory (creating it if need be) ----
456

            
457
1051
                let dir = make_secure_directory(&kind_dir, id)?;
458

            
459
1051
                touch_instance_dir(&dir)?;
460

            
461
1051
                Ok(InstanceStateHandle { dir, flock_guard })
462
1053
            })
463
1053
        }
464

            
465
160
        inner(self, I::kind(), &|f| identity.write_identity(f))
466
160
    }
467

            
468
    /// Given a kind and id, obtain pieces of its path and call a "doing work" callback
469
    ///
470
    /// This function factors out common functionality needed by
471
    /// [`StateDirectory::acquire_instance`] and [`StateDirectory::instance_peek_storage`],
472
    /// particularly relating to instance kind and id, and errors.
473
    ///
474
    /// `kind` and `id` are from an `InstanceIdentity`.
475
1155
    fn with_instance_path_pieces<T>(
476
1155
        self: &StateDirectory,
477
1155
        kind_str: &'static str,
478
1155
        id_writer: InstanceIdWriter,
479
1155
        // fn call(kind: &SlugRef, id: &SlugRef, resource_for_error: &impl Fn) -> _
480
1155
        call: impl FnOnce(&SlugRef, &SlugRef, &dyn Fn() -> Resource) -> Result<T>,
481
1155
    ) -> Result<T> {
482
        /// Struct that impls `Display` for formatting an instance id
483
        //
484
        // This exists because we want implementors of InstanceIdentity to be able to
485
        // use write! to format their identity string.
486
        struct InstanceIdDisplay<'i>(InstanceIdWriter<'i>);
487

            
488
        impl Display for InstanceIdDisplay<'_> {
489
1155
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
490
1155
                (self.0)(f)
491
1155
            }
492
        }
493
1155
        let id_string = InstanceIdDisplay(id_writer).to_string();
494

            
495
        // Both we and caller use this for our error reporting
496
1155
        let resource = || Resource::InstanceState {
497
2
            state_dir: self.dir.as_path().to_owned(),
498
2
            kind: kind_str.to_string(),
499
2
            identity: id_string.clone(),
500
2
        };
501

            
502
1155
        let handle_bad_slug = |source| Error::new(source, Action::Initializing, resource());
503

            
504
1155
        if kind_str.is_empty() {
505
            return Err(handle_bad_slug(BadSlug::EmptySlugNotAllowed));
506
1155
        }
507
1155
        let kind = SlugRef::new(kind_str).map_err(handle_bad_slug)?;
508
1155
        let id = SlugRef::new(&id_string).map_err(handle_bad_slug)?;
509

            
510
1155
        call(kind, id, &resource)
511
1155
    }
512

            
513
    /// List the instances of a particular kind
514
    ///
515
    /// Returns the instance identities.
516
    ///
517
    /// (The implementation lists subdirectories named `kind_*`.)
518
    ///
519
    /// Concurrency:
520
    /// An instance which is not being removed or created will be
521
    /// listed (or not) according to whether it's present.
522
    /// But, in the presence of concurrent calls to `acquire_instance` and `delete`
523
    /// on different instances,
524
    /// is not guaranteed to provide a snapshot:
525
    /// serialisation is not guaranteed across different instances.
526
    ///
527
    /// It *is* guaranteed to list each instance only once.
528
4
    pub fn list_instances<I: InstanceIdentity>(
529
4
        &self,
530
4
    ) -> impl Iterator<Item = Result<Slug>> + use<I> {
531
4
        self.list_instances_inner(I::kind())
532
4
    }
533

            
534
    /// List the instances of a kind, where the kind is supplied as a value
535
    ///
536
    /// Used by `list_instances` and `purge_instances`.
537
    ///
538
    /// *Includes* instances that exists only as a stale lockfile.
539
    #[allow(clippy::blocks_in_conditions)] // TODO #1176 this wants to be global
540
    #[allow(clippy::redundant_closure_call)] // false positive, re handle_err
541
28
    fn list_instances_inner(
542
28
        &self,
543
28
        kind: &'static str,
544
28
    ) -> impl Iterator<Item = Result<Slug>> + use<> {
545
        // We collect the output into these
546
28
        let mut out = HashSet::new();
547
28
        let mut errs = Vec::new();
548

            
549
        // Error handling
550

            
551
28
        let resource = || Resource::InstanceState {
552
            state_dir: self.dir.as_path().into(),
553
            kind: kind.into(),
554
            identity: "*".into(),
555
        };
556

            
557
        /// `fn handle_err!()(source: impl Into<ErrorSource>) -> Error`
558
        //
559
        // (Generic, so can't be a closure.  Uses local bindings, so can't be a fn.)
560
        macro_rules! handle_err { { } => {
561
            |source| Error::new(source, Action::Enumerating, resource())
562
        } }
563

            
564
        // Obtain an iterator of Result<DirEntry>
565
28
        match (|| {
566
28
            let kind = SlugRef::new(kind).map_err(handle_err!())?;
567
28
            self.dir.read_directory(kind).map_err(handle_err!())
568
        })() {
569
            Err(e) => errs.push(e),
570
28
            Ok(ents) => {
571
330
                for ent in ents {
572
302
                    match ent {
573
                        Err(e) => errs.push(handle_err!()(e)),
574
302
                        Ok(ent) => {
575
                            // Actually handle a directory entry!
576

            
577
453
                            let Some(id) = (|| {
578
                                // look for either ID or ID.lock
579
302
                                let id = ent.file_name();
580
302
                                let id = id.to_str()?; // ignore non-UTF-8
581
302
                                let id = id.strip_suffix(DOT_LOCK).unwrap_or(id);
582
302
                                let id = SlugRef::new(id).ok()?; // ignore other things
583
302
                                Some(id.to_owned())
584
                            })() else {
585
                                continue;
586
                            };
587

            
588
302
                            out.insert(id);
589
                        }
590
                    }
591
                }
592
            }
593
        }
594

            
595
28
        chain!(errs.into_iter().map(Err), out.into_iter().map(Ok),)
596
28
    }
597

            
598
    /// Delete instances according to selections made by the caller
599
    ///
600
    /// Each instance is considered in three stages.
601
    ///
602
    /// Firstly, it is passed to [`name_filter`](InstancePurgeHandler::name_filter).
603
    /// If `name_filter` returns `Live`,
604
    /// further consideration is skipped and the instance is retained.
605
    ///
606
    /// Secondly, the last time the instance was written to is determined,
607
    // This must be done with the lock held, for correctness
608
    // but the lock must be acquired in a way that doesn't itself update the modification time.
609
    // On Unix this is straightforward because opening for write doesn't update the mtime.
610
    // If this is hard on another platform, we'll need a separate stamp file updated
611
    // by an explicit Acquire operation.
612
    // This is tested by `test_reset_expiry`.
613
    /// and passed to
614
    /// [`age_filter`](InstancePurgeHandler::age_filter).
615
    /// Again, this might mean ensure the instance is retained.
616
    ///
617
    /// Thirdly, the resulting `InstanceStateHandle` is passed to
618
    /// [`dispose`](InstancePurgeHandler::dispose).
619
    /// `dispose` may choose to call `handle.delete()`,
620
    /// or simply drop the handle.
621
    ///
622
    /// Concurrency:
623
    /// In the presence of multiple concurrent calls to `acquire_instance` and `delete`:
624
    /// `filter` may be called for an instance which is being created or deleted
625
    /// by another task.
626
    /// `dispose` will be properly serialised with other activities on the same instance,
627
    /// as implied by it receiving an `InstanceStateHandle`.
628
    ///
629
    /// The expiry time is reset by calls to `acquire_instance`,
630
    /// `StorageHandle::store` and `InstanceStateHandle::raw_subdir`;
631
    /// it *may* be reset by calls to `StorageHandle::delete`.
632
    ///
633
    /// Instances that are currently locked by another task will not be purged,
634
    /// but the expiry time is *not* reset by *unlocking* an instance
635
    /// (dropping the last clone of an `InstanceStateHandle`).
636
    ///
637
    /// ### Sequencing of `InstancePurgeHandler` callbacks
638
    ///
639
    /// Each instance will be processed
640
    /// (and callbacks made for it) at most once;
641
    /// and calls for different instances will not be interleaved.
642
    ///
643
    /// During the processing of a particular instance
644
    /// The callbacks will be made in order,
645
    /// progressing monotonically through the methods in the order listed.
646
    /// But `name_filter` and `age_filter` might each be called
647
    /// more than once for the same instance.
648
    // We don't actually call name_filter more than once.
649
    ///
650
    /// Between each stage,
651
    /// the purge implementation may discover that the instance
652
    /// ought not to be processed further.
653
    /// So returning `Liveness::PossiblyUnused` from a filter does not
654
    /// guarantee that the next callback will be made.
655
24
    pub fn purge_instances(
656
24
        &self,
657
24
        now: SystemTime,
658
24
        filter: &mut (dyn InstancePurgeHandler + '_),
659
24
    ) -> Result<()> {
660
24
        let kind = filter.kind();
661

            
662
94
        for id in self.list_instances_inner(kind) {
663
94
            let id = id?;
664
141
            self.with_instance_path_pieces(kind, &|f| write!(f, "{id}"), |kind, id, resource| {
665
94
                self.maybe_purge_instance(now, kind, id, resource, filter)
666
94
            })?;
667
        }
668

            
669
24
        Ok(())
670
24
    }
671

            
672
    /// Consider whether to purge an instance
673
    ///
674
    /// Performs all the necessary steps, including liveness checks,
675
    /// passing an InstanceStateHandle to filter.dispose,
676
    /// and deleting stale lockfiles without associated state.
677
    #[allow(clippy::cognitive_complexity)] // splitting this would be more, not less, confusing
678
94
    fn maybe_purge_instance(
679
94
        &self,
680
94
        now: SystemTime,
681
94
        kind: &SlugRef,
682
94
        id: &SlugRef,
683
94
        resource: &dyn Fn() -> Resource,
684
94
        filter: &mut (dyn InstancePurgeHandler + '_),
685
94
    ) -> Result<()> {
686
        /// If `$l` is `Liveness::Live`, returns early with `Ok(())`.
687
        macro_rules! check_liveness { { $l:expr } => {
688
            match $l {
689
                Liveness::Live => return Ok(()),
690
                Liveness::PossiblyUnused => {},
691
            }
692
        } }
693

            
694
94
        check_liveness!(filter.name_filter(id)?);
695

            
696
58
        let dir_path = self.dir.as_path().join(kind).join(id);
697

            
698
        // Checks whether it should be kept due to being recently modified.
699
        // None::<SystemTime> means the instance directory is ENOENT
700
        // (which must mean that the instance exists only as a stale lockfile).
701
113
        let mut age_check = || -> Result<(Liveness, Option<SystemTime>)> {
702
84
            let handle_io_error = |source| Error::new(source, Action::Enumerating, resource());
703

            
704
            // 1. stat the instance dir
705
84
            let md = match fs::metadata(&dir_path) {
706
                // If instance dir is ENOENT, treat as old (maybe there was just a lockfile)
707
24
                Err(e) if e.kind() == io::ErrorKind::NotFound => {
708
24
                    return Ok((Liveness::PossiblyUnused, None));
709
                }
710
60
                other => other.map_err(handle_io_error)?,
711
            };
712
60
            let mtime = md.modified().map_err(handle_io_error)?;
713

            
714
            // 2. calculate the age
715
60
            let age = now.duration_since(mtime).unwrap_or(Duration::ZERO);
716

            
717
            // 3. do the age check
718
60
            let liveness = filter.age_filter(id, age)?;
719

            
720
60
            Ok((liveness, Some(mtime)))
721
84
        };
722

            
723
        // preliminary check, without locking yet
724
58
        check_liveness!(age_check()?.0);
725

            
726
        // ok we're probably doing to pass it to dispose (for possible deletion)
727

            
728
26
        let lock_path = dir_path.with_extension(LOCK_EXTN);
729
26
        let flock_guard = match LockFileGuard::try_lock(&lock_path) {
730
26
            Ok(Some(y)) => {
731
26
                trace!("locked {lock_path:?} (for purge)");
732
26
                y
733
            }
734
            Err(source) if source.kind() == io::ErrorKind::NotFound => {
735
                // We couldn't open the lockfile due to ENOENT
736
                // (Presumably) a containing directory is gone, so we don't need to do anything.
737
                trace!("locking {lock_path:?} (for purge), not found");
738
                return Ok(());
739
            }
740
            Ok(None) => {
741
                // Someone else has it locked.  Skip purging it.
742
                trace!("locking {lock_path:?} (for purge), in use");
743
                return Ok(());
744
            }
745
            Err(source) => {
746
                trace!(
747
                    "locking {lock_path:?} (for purge), error {}",
748
                    source.report()
749
                );
750
                return Err(Error::new(source, Action::Locking, resource()));
751
            }
752
        };
753

            
754
        // recheck to see if anyone has updated it
755
26
        let (age, mtime) = age_check()?;
756
26
        check_liveness!(age);
757

            
758
        // We have locked it and the filters say to maybe purge it.
759

            
760
26
        match mtime {
761
            None => {
762
                // And it doesn't even exist!  All we have is a leftover lockfile.  Delete it.
763
12
                let lockfile_rsrc = || Resource::File {
764
                    container: lock_path.parent().expect("no /!").into(),
765
                    file: lock_path.file_name().expect("no /!").into(),
766
                };
767
12
                flock_guard
768
12
                    .delete_lock_file(&lock_path)
769
12
                    .map_err(|source| Error::new(source, Action::Deleting, lockfile_rsrc()))?;
770
            }
771
14
            Some(last_modified) => {
772
                // Construct a state handle.
773
14
                let dir = self
774
14
                    .dir
775
14
                    .make_secure_directory(format!("{kind}/{id}"))
776
14
                    .map_err(|source| Error::new(source, Action::Enumerating, resource()))?;
777
14
                let flock_guard = Arc::new(flock_guard);
778

            
779
14
                filter.dispose(
780
14
                    &InstancePurgeInfo {
781
14
                        identity: id,
782
14
                        last_modified,
783
14
                    },
784
14
                    InstanceStateHandle { dir, flock_guard },
785
                )?;
786
            }
787
        }
788

            
789
26
        Ok(())
790
94
    }
791

            
792
    /// Tries to peek at something written by [`StorageHandle::store`]
793
    ///
794
    /// It is guaranteed that this will return either the `T` that was stored,
795
    /// or `None` if `store` was never called,
796
    /// or `StorageHandle::delete` was called
797
    ///
798
    /// So the operation is atomic, but there is no further synchronisation.
799
    //
800
    // Not sure if we need this, but it's logically permissible
801
8
    pub fn instance_peek_storage<I: InstanceIdentity, T: DeserializeOwned>(
802
8
        &self,
803
8
        identity: &I,
804
8
        key: &(impl TryIntoSlug + ?Sized),
805
8
    ) -> Result<Option<T>> {
806
8
        self.with_instance_path_pieces(
807
8
            I::kind(),
808
8
            &|f| identity.write_identity(f),
809
            // This closure is generic over T, so with_instance_path_pieces will be too;
810
            // this isn't desirable (code bloat) but avoiding it would involves some contortions.
811
8
            |kind_slug: &SlugRef, id_slug: &SlugRef, _resource| {
812
                // Throwing this error here will give a slightly wrong Error for this Bug
813
                // (because with_instance_path_pieces has its own notion of Action & Resource)
814
                // but that seems OK.
815
8
                let key_slug = key.try_into_slug()?;
816

            
817
8
                let rel_fname = format!(
818
8
                    "{}{PATH_SEPARATOR}{}{PATH_SEPARATOR}{}.json",
819
                    kind_slug, id_slug, key_slug,
820
                );
821

            
822
8
                let target = load_store::Target {
823
8
                    dir: &self.dir,
824
8
                    rel_fname: rel_fname.as_ref(),
825
8
                };
826

            
827
8
                target
828
8
                    .load()
829
                    // This Resource::File isn't consistent with those from StorageHandle:
830
                    // StorageHandle's `container` is the instance directory;
831
                    // here `container` is the top-level `state_dir`,
832
                    // and `file` is `KIND+INSTANCE/STORAGE.json".
833
8
                    .map_err(|source| {
834
                        Error::new(
835
                            source,
836
                            Action::Loading,
837
                            Resource::File {
838
                                container: self.dir.as_path().to_owned(),
839
                                file: rel_fname.into(),
840
                            },
841
                        )
842
                    })
843
8
            },
844
        )
845
8
    }
846
}
847

            
848
/// State or cache directory for an instance of a facility
849
///
850
/// Implies exclusive access:
851
/// there is only one `InstanceStateHandle` at a time,
852
/// across any number of processes, tasks, and threads,
853
/// for the same instance.
854
///
855
/// # Key uniqueness and syntactic restrictions
856
///
857
/// Methods on `InstanceStateHandle` typically take a [`TryIntoSlug`].
858
///
859
/// **It is important that keys are distinct within an instance.**
860
///
861
/// Specifically:
862
/// each key provided to a method on the same [`InstanceStateHandle`]
863
/// (or a clone of it)
864
/// must be different.
865
/// Violating this rule does not result in memory-unsafety,
866
/// but might result in incorrect operation due to concurrent filesystem access,
867
/// including possible data loss and corruption.
868
/// (Typically, the key is fixed, and the [`StorageHandle`]s are usually
869
/// obtained during instance construction, so ensuring this is straightforward.)
870
///
871
/// There are also syntactic restrictions on keys.  See [slug].
872
// We could implement a runtime check for this by retaining a table of in-use keys,
873
// possibly only with `cfg(debug_assertions)`.  However I think this isn't worth the code:
874
// it would involve an Arc<Mutex<SlugsInUseTable>> in InstanceStateHnndle and StorageHandle,
875
// and Drop impls to remove unused entries (and `raw_subdir` would have imprecise checking
876
// unless it returned a Drop newtype around CheckedDir).
877
#[derive(Debug, Clone, Deftly)]
878
#[derive_deftly(ContainsInstanceStateGuard)]
879
pub struct InstanceStateHandle {
880
    /// The directory
881
    dir: CheckedDir,
882
    /// Lock guard
883
    flock_guard: Arc<LockFileGuard>,
884
}
885

            
886
impl InstanceStateHandle {
887
    /// Obtain a [`StorageHandle`], usable for storing/retrieving a `T`
888
    ///
889
    /// [`key` has syntactic and uniqueness restrictions.](InstanceStateHandle#key-uniqueness-and-syntactic-restrictions)
890
62
    pub fn storage_handle<T>(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<StorageHandle<T>> {
891
        /// Implementation, not generic over `slug` and `T`
892
861
        fn inner(
893
861
            ih: &InstanceStateHandle,
894
861
            key: StdResult<Slug, BadSlug>,
895
861
        ) -> Result<(CheckedDir, String, Arc<LockFileGuard>)> {
896
861
            let key = key?;
897
861
            let instance_dir = ih.dir.clone();
898
861
            let leafname = format!("{key}.json");
899
861
            let flock_guard = ih.flock_guard.clone();
900
861
            Ok((instance_dir, leafname, flock_guard))
901
861
        }
902

            
903
62
        let (instance_dir, leafname, flock_guard) = inner(self, key.try_into_slug())?;
904
62
        Ok(StorageHandle {
905
62
            instance_dir,
906
62
            leafname,
907
62
            marker: PhantomData,
908
62
            flock_guard,
909
62
        })
910
62
    }
911

            
912
    /// Obtain a raw filesystem subdirectory, within the directory for this instance
913
    ///
914
    /// This API is unsuitable platforms without a filesystem accessible via `std::fs`.
915
    /// May therefore only be used within Arti for features
916
    /// where we're happy to not to support such platforms (eg WASM without WASI)
917
    /// without substantial further work.
918
    ///
919
    /// [`key` has syntactic and uniqueness restrictions.](InstanceStateHandle#key-uniqueness-and-syntactic-restrictions)
920
52
    pub fn raw_subdir(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<InstanceRawSubdir> {
921
        /// Implementation, not generic over `slug`
922
710
        fn inner(
923
710
            ih: &InstanceStateHandle,
924
710
            key: StdResult<Slug, BadSlug>,
925
710
        ) -> Result<InstanceRawSubdir> {
926
710
            let key = key?;
927
736
            let irs = (|| {
928
710
                trace!("ensuring/using {:?}/{:?}", ih.dir.as_path(), key.as_str());
929
710
                let dir = ih.dir.make_secure_directory(&key)?;
930
710
                let flock_guard = ih.flock_guard.clone();
931
710
                Ok::<_, ErrorSource>(InstanceRawSubdir { dir, flock_guard })
932
            })()
933
710
            .map_err(|source| {
934
                Error::new(
935
                    source,
936
                    Action::Initializing,
937
                    Resource::Directory {
938
                        dir: ih.dir.as_path().join(key),
939
                    },
940
                )
941
            })?;
942
710
            touch_instance_dir(&ih.dir)?;
943
710
            Ok(irs)
944
710
        }
945
52
        inner(self, key.try_into_slug())
946
52
    }
947

            
948
    /// Unconditionally delete this instance directory
949
    ///
950
    /// For expiry, use `StateDirectory::purge_instances`,
951
    /// and then call this in the `dispose` method.
952
    ///
953
    /// Will return a `BadAPIUsage` if other clones of this `InstanceStateHandle` exist.
954
    ///
955
    /// ### Deletion is *not* atomic
956
    ///
957
    /// If a deletion operation doesn't complete for any reason
958
    /// (maybe it was interrupted, or there was a filesystem access problem),
959
    /// *part* of the instance contents may remain.
960
    ///
961
    /// After such an interrupted deletion,
962
    /// storage items ([`StorageHandle`]) are might each independently
963
    /// be deleted ([`load`](StorageHandle::load) returns `None`)
964
    /// or retained (`Some`).
965
    ///
966
    /// Deletion of the contents of raw subdirectories
967
    /// ([`InstanceStateHandle::raw_subdir`])
968
    /// is done with `std::fs::remove_dir_all`.
969
    /// If deletion is interrupted, the raw subdirectory may contain partial contents.
970
    //
971
    // In principle we could provide atomic deletion, but it would lead to instances
972
    // that were in "limbo": they exist, but wouldn't appear in list_instances,
973
    // and the deletion would need to be completed next time they were acquired
974
    // (or during a purge_instances run).
975
    //
976
    // In practice we expect that callers will not try to use a partially-deleted instance,
977
    // and that if they do they will fail with a "state corrupted" error, which would be fine.
978
14
    pub fn purge(self) -> Result<()> {
979
14
        let dir = self.dir.as_path();
980

            
981
14
        (|| {
982
            // use Arc::into_inner on the lock object,
983
            // to make sure we're actually the only surviving InstanceStateHandle
984
14
            let flock_guard = Arc::into_inner(self.flock_guard).ok_or_else(|| {
985
                bad_api_usage!(
986
 "InstanceStateHandle::purge called for {:?}, but other clones of the handle exist",
987
                    self.dir.as_path(),
988
                )
989
            })?;
990

            
991
14
            trace!("purging {:?} (and {})", dir, DOT_LOCK);
992
14
            fs::remove_dir_all(dir)?;
993
14
            flock_guard.delete_lock_file(
994
                // dir.with_extension is right because the last component of dir
995
                // is KIND+ID which doesn't contain `.` so no extension will be stripped
996
14
                dir.with_extension(LOCK_EXTN),
997
            )?;
998

            
999
14
            Ok::<_, ErrorSource>(())
        })()
14
        .map_err(|source| {
            Error::new(
                source,
                Action::Deleting,
                Resource::Directory { dir: dir.into() },
            )
        })
14
    }
}
/// Touch an instance the state directory, `dir`, for expiry purposes
1761
fn touch_instance_dir(dir: &CheckedDir) -> Result<()> {
1761
    let dir = dir.as_path();
1761
    let resource = || Resource::Directory { dir: dir.into() };
1761
    filetime::set_file_mtime(dir, filetime::FileTime::now())
1761
        .map_err(|source| Error::new(source, Action::Initializing, resource()))
1761
}
/// A place in the state or cache directory, where we can load/store a serialisable type
///
/// Implies exclusive access.
///
/// Rust mutability-xor-sharing rules enforce proper synchronisation,
/// unless multiple `StorageHandle`s are created
/// using the same [`InstanceStateHandle`] and key.
#[derive(Deftly, Debug)] // not Clone, to enforce mutability rules (see above)
#[derive_deftly(ContainsInstanceStateGuard)]
pub struct StorageHandle<T> {
    /// The directory and leafname
    instance_dir: CheckedDir,
    /// `KEY.json`
    leafname: String,
    /// We can load and store a `T`.
    ///
    /// Invariant in `T`.  But we're `Sync` and `Send` regardless of `T`.
    /// (From the Table of PhantomData patterns in the Nomicon.)
    marker: PhantomData<fn(T) -> T>,
    /// Clone of the InstanceStateHandle's lock
    flock_guard: Arc<LockFileGuard>,
}
// Like tor_persist, but writing needs `&mut`
impl<T: Serialize + DeserializeOwned> StorageHandle<T> {
    /// Load this persistent state
    ///
    /// `None` means the state was most recently [`delete`](StorageHandle::delete)ed
44
    pub fn load(&self) -> Result<Option<T>> {
44
        self.with_load_store_target(Action::Loading, |t| t.load())
44
    }
    /// Store this persistent state
362
    pub fn store(&mut self, v: &T) -> Result<()> {
        // The renames will cause a directory mtime update
362
        self.with_load_store_target(Action::Storing, |t| t.store(v))
362
    }
    /// Delete this persistent state
2
    pub fn delete(&mut self) -> Result<()> {
        // Only counts as a recent modification if this state *did* exist
2
        self.with_load_store_target(Action::Deleting, |t| t.delete())
2
    }
    /// Operate using a `load_store::Target`
408
    fn with_load_store_target<R, F>(&self, action: Action, f: F) -> Result<R>
408
    where
408
        F: FnOnce(load_store::Target<'_>) -> std::result::Result<R, ErrorSource>,
    {
408
        f(load_store::Target {
408
            dir: &self.instance_dir,
408
            rel_fname: self.leafname.as_ref(),
408
        })
408
        .map_err(self.map_err(action))
408
    }
    /// Helper to convert an `ErrorSource` to an `Error`, if we were performing `action`
408
    fn map_err(&self, action: Action) -> impl FnOnce(ErrorSource) -> Error + use<T> {
408
        let resource = self.err_resource();
        move |source| crate::Error::new(source, action, resource)
408
    }
    /// Return the proper `Resource` for reporting errors
408
    fn err_resource(&self) -> Resource {
408
        Resource::File {
408
            // TODO ideally we would remember what proportion of instance_dir
408
            // came from the original state_dir, so we can put state_dir in the container
408
            container: self.instance_dir.as_path().to_owned(),
408
            file: self.leafname.clone().into(),
408
        }
408
    }
}
/// Subdirectory within an instance's state, for raw filesystem operations
///
/// Dereferences to `fs_mistrust::CheckedDir` and can be used mostly like one.
/// Obtained from [`InstanceStateHandle::raw_subdir`].
///
/// Existence of this value implies exclusive access to the instance.
///
/// If you need to manage the lock, and the directory path, separately,
/// [`raw_lock_guard`](ContainsInstanceStateGuard::raw_lock_guard)
///  will help.
#[derive(Deref, Clone, Debug, Deftly)]
#[derive_deftly(ContainsInstanceStateGuard)]
pub struct InstanceRawSubdir {
    /// The actual directory, as a [`fs_mistrust::CheckedDir`]
    #[deref]
    dir: CheckedDir,
    /// Clone of the InstanceStateHandle's lock
    flock_guard: Arc<LockFileGuard>,
}
#[cfg(all(test, not(miri) /* filesystem access */))]
mod test {
    // @@ begin test lint list maintained by maint/add_warning @@
    #![allow(clippy::bool_assert_comparison)]
    #![allow(clippy::clone_on_copy)]
    #![allow(clippy::dbg_macro)]
    #![allow(clippy::mixed_attributes_style)]
    #![allow(clippy::print_stderr)]
    #![allow(clippy::print_stdout)]
    #![allow(clippy::single_char_pattern)]
    #![allow(clippy::unwrap_used)]
    #![allow(clippy::unchecked_duration_subtraction)]
    #![allow(clippy::useless_vec)]
    #![allow(clippy::needless_pass_by_value)]
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
    use super::*;
    use derive_deftly::{Deftly, derive_deftly_adhoc};
    use itertools::{Itertools, iproduct};
    use serde::{Deserialize, Serialize};
    use std::collections::BTreeSet;
    use std::fmt::Display;
    use std::fs::File;
    use std::io;
    use std::str::FromStr;
    use test_temp_dir::test_temp_dir;
    use tor_basic_utils::PathExt as _;
    use tor_error::HasKind as _;
    use tracing_test::traced_test;
    use tor_error::ErrorKind as TEK;
    type AgeDays = i8;
    fn days(days: AgeDays) -> Duration {
        Duration::from_secs(86400 * u64::try_from(days).unwrap())
    }
    fn now() -> SystemTime {
        SystemTime::now()
    }
    struct Garlic(Slug);
    impl InstanceIdentity for Garlic {
        fn kind() -> &'static str {
            "garlic"
        }
        fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
            Display::fmt(&self.0, f)
        }
    }
    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
    struct StoredData {
        some_value: i32,
    }
    fn mk_state_dir(dir: &Path) -> StateDirectory {
        StateDirectory::new(
            dir,
            &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
        )
        .unwrap()
    }
    #[test]
    #[traced_test]
    fn test_api() {
        test_temp_dir!().used_by(|dir| {
            let sd = mk_state_dir(dir);
            let garlic = Garlic("wild".try_into_slug().unwrap());
            let acquire_instance = || sd.acquire_instance(&garlic);
            let ih = acquire_instance().unwrap();
            let inst_path = dir.join("garlic/wild");
            assert!(fs::metadata(&inst_path).unwrap().is_dir());
            assert_eq!(
                acquire_instance().unwrap_err().kind(),
                TEK::LocalResourceAlreadyInUse,
            );
            let irsd = ih.raw_subdir("raw").unwrap();
            assert!(fs::metadata(irsd.as_path()).unwrap().is_dir());
            assert_eq!(irsd.as_path(), dir.join("garlic").join("wild").join("raw"));
            let mut sh = ih.storage_handle::<StoredData>("stored_data").unwrap();
            let storage_path = dir.join("garlic/wild/stored_data.json");
            let peek = || sd.instance_peek_storage(&garlic, "stored_data");
            let expect_load = |sh: &StorageHandle<_>, expect| {
                let check_loaded = |what, loaded: Result<Option<StoredData>>| {
                    assert_eq!(loaded.unwrap().as_ref(), expect, "{what}");
                };
                check_loaded("load", sh.load());
                check_loaded("peek", peek());
            };
            expect_load(&sh, None);
            let to_store = StoredData { some_value: 42 };
            sh.store(&to_store).unwrap();
            assert!(fs::metadata(storage_path).unwrap().is_file());
            expect_load(&sh, Some(&to_store));
            sh.delete().unwrap();
            expect_load(&sh, None);
            drop(sh);
            drop(irsd);
            ih.purge().unwrap();
            assert_eq!(peek().unwrap(), None);
            assert_eq!(
                fs::metadata(&inst_path).unwrap_err().kind(),
                io::ErrorKind::NotFound
            );
        });
    }
    #[test]
    #[traced_test]
    #[allow(clippy::comparison_chain)]
    #[allow(clippy::expect_fun_call)]
    fn test_iter() {
        // Tests list_instances and purge_instances.
        //
        //  1. Set up a single state directory containing a number of instances,
        //    enumerating all the possible situations that purge_instance might find.
        //    The instance is identified by a `Which` which specifies its properties,
        //    and which is representable as the instance id slug.
        //  1b. Put some junk in the state directory too, that we expect to be ignored.
        //
        //  2. Call list_instances and check that we see what we expect.
        //
        //  3. Call purge_instances and check that all the callbacks happen as we expect.
        //
        //  4. Call list_instances again and check that we see what we now expect.
        //
        //  5. Check that the junk is still present.
        let temp_dir = test_temp_dir!();
        let state_dir = temp_dir.used_by(mk_state_dir);
        /// Reified test case spec for expiry
        //
        // For non-`bool` fields, `#[deftly(test = )]` gives the set of values to test.
        #[derive(Deftly, Eq, PartialEq, Debug)]
        #[derive_deftly_adhoc]
        struct Which {
            /// Does `name_filter` return `Live`?
            namefilter_live: bool,
            /// What is the oldest does `age_filter` will return `Live` for?
            #[deftly(test = "0, 2")]
            max_age: AgeDays,
            /// How long ago was the instance dir actually modified?
            #[deftly(test = "-1, 1, 3")]
            age: AgeDays,
            /// Does the instance dir exist?
            dir: bool,
            /// Does the instance !lockfile exist?
            lockfile: bool,
        }
        /// Ad-hoc (de)serialisation scheme of `Which` as an instance id (a `Slug`)
        ///
        /// The serialisation is `n<namefilter_live>_m<max_age>_...`,
        /// ie, for each field, the initial letter of its name, followed by the value.
        /// (We don't bother suppressing the trailiong `_`).
        impl Display for Which {
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
                derive_deftly_adhoc! {
                    Which:
                    $(
                        write!(
                            f, "{}{}_",
                            stringify!($fname).chars().next().unwrap(),
                            self.$fname,
                        )?;
                    )
                }
                Ok(())
            }
        }
        impl FromStr for Which {
            type Err = Error;
            fn from_str(s: &str) -> Result<Self> {
                let mut fields = s.split('_');
                derive_deftly_adhoc! {
                    Which:
                    Ok(Which { $(
                        $fname: fields.next().unwrap()
                            .split_at(1).1
                            .parse().unwrap(),
                    )})
                }
            }
        }
        impl InstanceIdentity for Which {
            fn kind() -> &'static str {
                "which"
            }
            fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
                Display::fmt(self, f)
            }
        }
        // 0. Calculate all possible whiches
        let whiches = {
            derive_deftly_adhoc!(
                Which:
                iproduct!(
                    $(
                        ${if fmeta(test) { [ ${fmeta(test) as token_stream} ] }
                          else { [false, true] }},
                    )
                    // iproduct hates a trailing comma, so add a dummy element
                    // https://github.com/rust-itertools/itertools/issues/868
                    [()]
                )
            )
            .map(derive_deftly_adhoc!(
                Which:
                //
                |($( $fname, ) ())| Which { $( $fname, ) }
            ))
            // if you want to debug one test case, you can do this:
            // .filter(|wh| wh.to_string() == "nfalse_r2_a3_lfalse_dtrue_")
            .collect_vec()
        };
        // 1. Create all the test instances, according to the specifications
        for which in &whiches {
            let s = which.to_string();
            println!("{s}");
            assert_eq!(&s.parse::<Which>().unwrap(), which);
            let inst = state_dir.acquire_instance(which).unwrap();
            if !which.dir {
                fs::remove_dir_all(inst.dir.as_path()).unwrap();
            } else {
                let now = now();
                let set_mtime = |mtime: SystemTime| {
                    filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
                };
                if which.age > 0 {
                    set_mtime(now - days(which.age));
                } else if which.age < 0 {
                    set_mtime(now + days(-which.age));
                };
            }
            if !which.lockfile {
                let lock_path = inst.dir.as_path().with_extension(LOCK_EXTN);
                let flock_guard = Arc::into_inner(inst.flock_guard).unwrap();
                flock_guard
                    .delete_lock_file(&lock_path)
                    .expect(&lock_path.display_lossy().to_string());
            }
        }
        // 1b. Create some junk that should be ignored
        let junk = {
            let mut junk = Vec::new();
            let base = state_dir.dir.as_path();
            for rhs in ["+bad", &format!("+bad{DOT_LOCK}"), ".tmp"] {
                let mut mk = |lhs, is_dir| {
                    let p = base.join(format!("{lhs}{rhs}"));
                    junk.push((p.clone(), is_dir));
                    p
                };
                File::create(mk("file", false)).unwrap();
                fs::create_dir(mk("dir", true)).unwrap();
            }
            junk
        };
        // 2. Check that we see the ones we expect
        let list_instances = || {
            state_dir
                .list_instances::<Which>()
                .map(Result::unwrap)
                .collect::<BTreeSet<_>>()
        };
        let found = list_instances();
        let expected: BTreeSet<_> = whiches
            .iter()
            .filter(|which| which.dir || which.lockfile)
            .map(|which| Slug::new(which.to_string()).unwrap())
            .collect();
        itertools::assert_equal(&found, &expected);
        // 3. Run a purge and check that we see the expected callbacks
        struct PurgeHandler<'r> {
            expected: &'r BTreeSet<Slug>,
        }
        impl Which {
            fn old_enough_to_vanish(&self) -> bool {
                self.age > self.max_age
            }
        }
        impl InstancePurgeHandler for PurgeHandler<'_> {
            fn kind(&self) -> &'static str {
                "which"
            }
            fn name_filter(&mut self, id: &SlugRef) -> Result<Liveness> {
                eprintln!("{id} - name_filter");
                assert!(self.expected.contains(id));
                let which: Which = id.as_str().parse().unwrap();
                Ok(if which.namefilter_live {
                    Liveness::Live
                } else {
                    Liveness::PossiblyUnused
                })
            }
            fn age_filter(&mut self, id: &SlugRef, age: Duration) -> Result<Liveness> {
                eprintln!("{id} - age_filter({age:?})");
                let which: Which = id.as_str().parse().unwrap();
                assert!(!which.namefilter_live);
                Ok(if age <= days(which.max_age) {
                    Liveness::Live
                } else {
                    Liveness::PossiblyUnused
                })
            }
            fn dispose(
                &mut self,
                info: &InstancePurgeInfo,
                handle: InstanceStateHandle,
            ) -> Result<()> {
                let id = info.identity();
                eprintln!("{id} - dispose");
                let which: Which = id.as_str().parse().unwrap();
                assert!(!which.namefilter_live);
                assert!(which.old_enough_to_vanish());
                assert!(which.dir);
                handle.purge()
            }
        }
        state_dir
            .purge_instances(
                now(),
                &mut PurgeHandler {
                    expected: &expected,
                },
            )
            .unwrap();
        // 4. List the instances again and check the results
        let found = list_instances();
        let expected: BTreeSet<_> = whiches
            .iter()
            .filter(|which| {
                if which.namefilter_live {
                    // things filtered by the name filter are left alone;
                    // we see them if any bits of them existed, even a stale lockfile
                    which.dir || which.lockfile
                } else {
                    // things *not* filtered by the name filter are retained
                    // iff the directory exists and is new enough
                    which.dir && !which.old_enough_to_vanish()
                }
            })
            .map(|which| Slug::new(which.to_string()).unwrap())
            .collect();
        itertools::assert_equal(&found, &expected);
        // 5. Check that the junk was ignored
        for (p, is_dir) in junk {
            let md = fs::metadata(&p).unwrap();
            assert_eq!(md.is_dir(), is_dir, "{}", p.display_lossy());
        }
    }
    #[test]
    #[traced_test]
    fn test_reset_expiry() {
        // Tests that things that should update the instance mtime do so,
        // and that things that shouldn't, don't.
        //
        // For each test case, we:
        //   1. create a new subdirectory of our temp dir, making a new StateDirectory.
        //   2. (optionally) set up one instance within it, containing one pre-prepared
        //      existing storage file and one pre-prepared (empty) raw subdir
        //   3. perform test-case specific actions on the instance
        //   4. run a stunt `purge_instances` call that merely checks
        //      that the right value was passed to age_filter
        let temp_dir = test_temp_dir!();
        const KIND: &str = "kind";
        // keys for various sub-objects
        const S_EXISTS: &str = "state-existing";
        const S_ABSENT: &str = "state-initially-absent";
        const R_EXISTS: &str = "raw-subdir-existing";
        const R_ABSENT: &str = "raw-subdir-initially-absent";
        struct FixedId;
        impl InstanceIdentity for FixedId {
            fn kind() -> &'static str {
                KIND
            }
            fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
                write!(f, "id")
            }
        }
        /// Did we expect this test case's actions to change the mtime?
        #[derive(PartialEq, Debug)]
        enum Expect {
            /// mtime should be updated
            New,
            /// mtime should be unchanged
            Old,
        }
        use Expect as Ex;
        /// Callbacks for stunt purge
        ///
        /// `self == None` means we've called `age_filter` already.
        #[allow(non_local_definitions)] // rust-lang/rust#125068
        impl InstancePurgeHandler for Option<&'_ Expect> {
            fn kind(&self) -> &'static str {
                KIND
            }
            fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
                Ok(Liveness::PossiblyUnused)
            }
            fn age_filter(&mut self, _identity: &SlugRef, age: Duration) -> Result<Liveness> {
                let did_reset = if age < days(1) { Ex::New } else { Ex::Old };
                assert_eq!(&did_reset, self.unwrap());
                *self = None;
                // Stop processing the instance
                Ok(Liveness::Live)
            }
            fn dispose(
                &mut self,
                _info: &InstancePurgeInfo<'_>,
                _handle: InstanceStateHandle,
            ) -> Result<()> {
                panic!("disposed live")
            }
        }
        /// Helper for test that purge iteration doesn't itself update the mtime
        ///
        /// Says `PossiblyUnused` so that `dispose` gets called,
        /// but then just drops the handle and doesn't delete.
        struct ExamineAll;
        impl InstancePurgeHandler for ExamineAll {
            fn kind(&self) -> &'static str {
                KIND
            }
            fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
                Ok(Liveness::PossiblyUnused)
            }
            fn age_filter(&mut self, _identity: &SlugRef, _age: Duration) -> Result<Liveness> {
                Ok(Liveness::PossiblyUnused)
            }
            fn dispose(
                &mut self,
                _info: &InstancePurgeInfo<'_>,
                _handle: InstanceStateHandle,
            ) -> Result<()> {
                Ok(())
            }
        }
        // Run a check (raw - doesn't creating an initial instance state)
        let chk_without_create = |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory)| {
            temp_dir.subdir_used_by(which, |dir| {
                let state_dir = mk_state_dir(&dir);
                acts(&state_dir);
                let mut exp = Some(&exp);
                state_dir.purge_instances(now(), &mut exp).unwrap();
                assert!(exp.is_none(), "age_filter not called, instance missing?");
            });
        };
        // Run a check with a prepared instance state
        //
        // The prepared instance:
        //  - has an existing storage at key S_EXISTS
        //  - has an existing empty raw subdir at key R_EXISTS
        //  - has been acquired, so `acts` gets an handle
        //  - but all of this (looks like it) happened 2 days ago
        let chk =
            |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory, InstanceStateHandle)| {
                chk_without_create(exp, which, &|state_dir| {
                    let inst = state_dir.acquire_instance(&FixedId).unwrap();
                    inst.storage_handle(S_EXISTS)
                        .unwrap()
                        .store(&StoredData { some_value: 1 })
                        .unwrap();
                    inst.raw_subdir(R_EXISTS).unwrap();
                    let mtime = now() - days(2);
                    filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
                    acts(state_dir, inst);
                });
            };
        // Test things that shouldn't count for keeping an instance alive
        chk(Ex::Old, "just-releasing-acquired", &|_, inst| {
            drop(inst);
        });
        chk(Ex::Old, "loading", &|_, inst| {
            let load = |key| {
                inst.storage_handle::<StoredData>(key)
                    .unwrap()
                    .load()
                    .unwrap()
            };
            assert!(load(S_EXISTS).is_some());
            assert!(load(S_ABSENT).is_none());
        });
        chk(Ex::Old, "messing-in-subdir", &|_, inst| {
            // we don't have a raw subdir path here, but we know what it is
            let in_raw = inst.dir.as_path().join(R_EXISTS).join("new");
            let _: File = File::create(in_raw).unwrap();
        });
        chk(Ex::Old, "purge-iter-no-delete", &|state_dir, inst| {
            drop(inst);
            // ExamineAll looks at everything but never calls InstanceStateHandle::purge.
            // It it causes every instance to be locked, but not mtime-updated.
            state_dir.purge_instances(now(), &mut ExamineAll).unwrap();
        });
        // Test things that *should* count for keeping an instance alive
        chk_without_create(Ex::New, "acquire-new-instance", &|state_dir| {
            state_dir.acquire_instance(&FixedId).unwrap();
        });
        chk(Ex::New, "acquire-existing-instance", &|state_dir, inst| {
            drop(inst);
            state_dir.acquire_instance(&FixedId).unwrap();
        });
        for storage_key in [S_EXISTS, S_ABSENT] {
            chk(Ex::New, &format!("store-{}", storage_key), &|_, inst| {
                inst.storage_handle(storage_key)
                    .unwrap()
                    .store(&StoredData { some_value: 2 })
                    .unwrap();
            });
        }
        for raw_dir in [R_EXISTS, R_ABSENT] {
            chk(Ex::New, &format!("raw_subdir-{}", raw_dir), &|_, inst| {
                let _: InstanceRawSubdir = inst.raw_subdir(raw_dir).unwrap();
            });
        }
    }
}