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::{define_derive_deftly, Deftly};
180
use derive_more::{AsRef, Deref};
181
use itertools::chain;
182
use serde::{de::DeserializeOwned, Serialize};
183

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

            
189
use crate::err::{Action, ErrorSource, Resource};
190
use crate::load_store;
191
use crate::slug::{BadSlug, Slug, SlugRef, TryIntoSlug};
192
pub use crate::Error;
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
1170
        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
68
    pub fn new(state_dir: impl AsRef<Path>, mistrust: &Mistrust) -> Result<Self> {
387
        /// Implementation, taking non-generic path
388
834
        fn inner(path: &Path, mistrust: &Mistrust) -> Result<StateDirectory> {
389
834
            let resource = || Resource::Directory {
390
                dir: path.to_owned(),
391
            };
392
834
            let handle_err = |source| Error::new(source, Action::Initializing, resource());
393

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

            
399
834
            Ok(StateDirectory { dir })
400
834
        }
401
68
        inner(state_dir.as_ref(), mistrust)
402
68
    }
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
144
    pub fn acquire_instance<I: InstanceIdentity>(
409
144
        &self,
410
144
        identity: &I,
411
144
    ) -> Result<InstanceStateHandle> {
412
        /// Implementation, taking non-generic values for identity
413
617
        fn inner(
414
617
            sd: &StateDirectory,
415
617
            kind_str: &'static str,
416
617
            id_writer: InstanceIdWriter,
417
617
        ) -> Result<InstanceStateHandle> {
418
689
            sd.with_instance_path_pieces(kind_str, id_writer, |kind, id, resource| {
419
617
                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
1232
                let make_secure_directory = |parent: &CheckedDir, subdir| {
424
1232
                    let resource = || Resource::Directory {
425
                        dir: parent.as_path().join(subdir),
426
                    };
427
1232
                    parent
428
1232
                        .make_secure_directory(subdir)
429
1232
                        .map_err(|source| Error::new(source, Action::Initializing, resource()))
430
1232
                };
431

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

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

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

            
440
617
                let flock_guard = match LockFileGuard::try_lock(&lock_path) {
441
615
                    Ok(Some(y)) => {
442
615
                        trace!("locked {lock_path:?}");
443
615
                        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
615
                let dir = make_secure_directory(&kind_dir, id)?;
458

            
459
615
                touch_instance_dir(&dir)?;
460

            
461
615
                Ok(InstanceStateHandle { dir, flock_guard })
462
689
            })
463
617
        }
464

            
465
144
        inner(self, I::kind(), &|f| identity.write_identity(f))
466
144
    }
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
719
    fn with_instance_path_pieces<T>(
476
719
        self: &StateDirectory,
477
719
        kind_str: &'static str,
478
719
        id_writer: InstanceIdWriter,
479
719
        // fn call(kind: &SlugRef, id: &SlugRef, resource_for_error: &impl Fn) -> _
480
719
        call: impl FnOnce(&SlugRef, &SlugRef, &dyn Fn() -> Resource) -> Result<T>,
481
719
    ) -> 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
719
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
490
719
                (self.0)(f)
491
719
            }
492
        }
493
719
        let id_string = InstanceIdDisplay(id_writer).to_string();
494
719

            
495
719
        // Both we and caller use this for our error reporting
496
719
        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
719
        let handle_bad_slug = |source| Error::new(source, Action::Initializing, resource());
503

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

            
510
719
        call(kind, id, &resource)
511
719
    }
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>(&self) -> impl Iterator<Item = Result<Slug>> {
529
4
        self.list_instances_inner(I::kind())
530
4
    }
531

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

            
544
28
        // Error handling
545
28

            
546
28
        let resource = || Resource::InstanceState {
547
            state_dir: self.dir.as_path().into(),
548
            kind: kind.into(),
549
            identity: "*".into(),
550
        };
551

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

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

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

            
583
302
                            out.insert(id);
584
                        }
585
                    }
586
                }
587
            }
588
        }
589

            
590
28
        chain!(errs.into_iter().map(Err), out.into_iter().map(Ok),)
591
28
    }
592

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

            
657
94
        for id in self.list_instances_inner(kind) {
658
94
            let id = id?;
659
141
            self.with_instance_path_pieces(kind, &|f| write!(f, "{id}"), |kind, id, resource| {
660
94
                self.maybe_purge_instance(now, kind, id, resource, filter)
661
141
            })?;
662
        }
663

            
664
24
        Ok(())
665
24
    }
666

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

            
689
94
        check_liveness!(filter.name_filter(id)?);
690

            
691
58
        let dir_path = self.dir.as_path().join(kind).join(id);
692
58

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

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

            
709
            // 2. calculate the age
710
60
            let age = now.duration_since(mtime).unwrap_or(Duration::ZERO);
711

            
712
            // 3. do the age check
713
60
            let liveness = filter.age_filter(id, age)?;
714

            
715
60
            Ok((liveness, Some(mtime)))
716
84
        };
717

            
718
        // preliminary check, without locking yet
719
58
        check_liveness!(age_check()?.0);
720

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

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

            
749
        // recheck to see if anyone has updated it
750
26
        let (age, mtime) = age_check()?;
751
26
        check_liveness!(age);
752

            
753
        // We have locked it and the filters say to maybe purge it.
754

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

            
774
14
                filter.dispose(
775
14
                    &InstancePurgeInfo {
776
14
                        identity: id,
777
14
                        last_modified,
778
14
                    },
779
14
                    InstanceStateHandle { dir, flock_guard },
780
14
                )?;
781
            }
782
        }
783

            
784
26
        Ok(())
785
94
    }
786

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

            
812
8
                let rel_fname = format!(
813
8
                    "{}{PATH_SEPARATOR}{}{PATH_SEPARATOR}{}.json",
814
8
                    kind_slug, id_slug, key_slug,
815
8
                );
816
8

            
817
8
                let target = load_store::Target {
818
8
                    dir: &self.dir,
819
8
                    rel_fname: rel_fname.as_ref(),
820
8
                };
821
8

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

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

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

            
898
46
        let (instance_dir, leafname, flock_guard) = inner(self, key.try_into_slug())?;
899
46
        Ok(StorageHandle {
900
46
            instance_dir,
901
46
            leafname,
902
46
            marker: PhantomData,
903
46
            flock_guard,
904
46
        })
905
46
    }
906

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

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

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

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

            
994
14
            Ok::<_, ErrorSource>(())
995
14
        })()
996
14
        .map_err(|source| {
997
            Error::new(
998
                source,
999
                Action::Deleting,
                Resource::Directory { dir: dir.into() },
            )
14
        })
14
    }
}
/// Touch an instance the state directory, `dir`, for expiry purposes
1089
fn touch_instance_dir(dir: &CheckedDir) -> Result<()> {
1089
    let dir = dir.as_path();
1089
    let resource = || Resource::Directory { dir: dir.into() };
1089
    filetime::set_file_mtime(dir, filetime::FileTime::now())
1089
        .map_err(|source| Error::new(source, Action::Initializing, resource()))
1089
}
/// 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
28
    pub fn load(&self) -> Result<Option<T>> {
28
        self.with_load_store_target(Action::Loading, |t| t.load())
28
    }
    /// Store this persistent state
218
    pub fn store(&mut self, v: &T) -> Result<()> {
218
        // The renames will cause a directory mtime update
218
        self.with_load_store_target(Action::Storing, |t| t.store(v))
218
    }
    /// Delete this persistent state
2
    pub fn delete(&mut self) -> Result<()> {
2
        // 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`
248
    fn with_load_store_target<R, F>(&self, action: Action, f: F) -> Result<R>
248
    where
248
        F: FnOnce(load_store::Target<'_>) -> std::result::Result<R, ErrorSource>,
248
    {
248
        f(load_store::Target {
248
            dir: &self.instance_dir,
248
            rel_fname: self.leafname.as_ref(),
248
        })
248
        .map_err(self.map_err(action))
248
    }
    /// Helper to convert an `ErrorSource` to an `Error`, if we were performing `action`
248
    fn map_err(&self, action: Action) -> impl FnOnce(ErrorSource) -> Error {
248
        let resource = self.err_resource();
        move |source| crate::Error::new(source, action, resource)
248
    }
    /// Return the proper `Resource` for reporting errors
248
    fn err_resource(&self) -> Resource {
248
        Resource::File {
248
            // TODO ideally we would remember what proportion of instance_dir
248
            // came from the original state_dir, so we can put state_dir in the container
248
            container: self.instance_dir.as_path().to_owned(),
248
            file: self.leafname.clone().into(),
248
        }
248
    }
}
/// 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::{derive_deftly_adhoc, Deftly};
    use itertools::{iproduct, Itertools};
    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();
            });
        }
    }
}