tor_persist/
state_dir.rs

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
170use std::collections::HashSet;
171use std::fmt::{self, Display};
172use std::fs;
173use std::io;
174use std::marker::PhantomData;
175use std::path::Path;
176use std::sync::Arc;
177use std::time::{Duration, SystemTime};
178
179use derive_deftly::{define_derive_deftly, Deftly};
180use derive_more::{AsRef, Deref};
181use itertools::chain;
182use serde::{de::DeserializeOwned, Serialize};
183
184use fs_mistrust::{CheckedDir, Mistrust};
185use tor_error::bad_api_usage;
186use tor_error::ErrorReport as _;
187use tracing::trace;
188
189use crate::err::{Action, ErrorSource, Resource};
190use crate::load_store;
191use crate::slug::{BadSlug, Slug, SlugRef, TryIntoSlug};
192pub use crate::Error;
193
194#[allow(unused_imports)] // Simplifies a lot of references in our docs
195use crate::slug;
196
197define_derive_deftly! {
198    ContainsInstanceStateGuard:
199
200    impl<$tgens> ContainsInstanceStateGuard for $ttype where $twheres {
201        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`]
208pub use fslock_guard::LockFileGuard;
209
210use std::result::Result as StdResult;
211
212use std::path::MAIN_SEPARATOR as PATH_SEPARATOR;
213
214/// [`Result`](StdResult) throwing a [`state_dir::Error`](Error)
215pub type Result<T> = StdResult<T, Error>;
216
217/// Extension for lockfiles
218const 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?
222const 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)]
254pub 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.
267pub 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.
299pub 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)]
339pub 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
358pub 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.)
373pub 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
382type InstanceIdWriter<'i> = &'i dyn Fn(&mut fmt::Formatter) -> fmt::Result;
383
384impl StateDirectory {
385    /// Create a new `StateDirectory` from a directory and mistrust configuration
386    pub fn new(state_dir: impl AsRef<Path>, mistrust: &Mistrust) -> Result<Self> {
387        /// Implementation, taking non-generic path
388        fn inner(path: &Path, mistrust: &Mistrust) -> Result<StateDirectory> {
389            let resource = || Resource::Directory {
390                dir: path.to_owned(),
391            };
392            let handle_err = |source| Error::new(source, Action::Initializing, resource());
393
394            let dir = mistrust
395                .verifier()
396                .make_secure_dir(path)
397                .map_err(handle_err)?;
398
399            Ok(StateDirectory { dir })
400        }
401        inner(state_dir.as_ref(), mistrust)
402    }
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    pub fn acquire_instance<I: InstanceIdentity>(
409        &self,
410        identity: &I,
411    ) -> Result<InstanceStateHandle> {
412        /// Implementation, taking non-generic values for identity
413        fn inner(
414            sd: &StateDirectory,
415            kind_str: &'static str,
416            id_writer: InstanceIdWriter,
417        ) -> Result<InstanceStateHandle> {
418            sd.with_instance_path_pieces(kind_str, id_writer, |kind, id, resource| {
419                let handle_err =
420                    |action, source: ErrorSource| Error::new(source, action, resource());
421
422                // Obtain (creating if necessary) a subdir for a Checked
423                let make_secure_directory = |parent: &CheckedDir, subdir| {
424                    let resource = || Resource::Directory {
425                        dir: parent.as_path().join(subdir),
426                    };
427                    parent
428                        .make_secure_directory(subdir)
429                        .map_err(|source| Error::new(source, Action::Initializing, resource()))
430                };
431
432                // ---- obtain the lock ----
433
434                let kind_dir = make_secure_directory(&sd.dir, kind)?;
435
436                let lock_path = kind_dir
437                    .join(format!("{id}.{LOCK_EXTN}"))
438                    .map_err(|source| handle_err(Action::Initializing, source.into()))?;
439
440                let flock_guard = match LockFileGuard::try_lock(&lock_path) {
441                    Ok(Some(y)) => {
442                        trace!("locked {lock_path:?}");
443                        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                        trace!("locking {lock_path:?}, in use",);
451                        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                let dir = make_secure_directory(&kind_dir, id)?;
458
459                touch_instance_dir(&dir)?;
460
461                Ok(InstanceStateHandle { dir, flock_guard })
462            })
463        }
464
465        inner(self, I::kind(), &|f| identity.write_identity(f))
466    }
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    fn with_instance_path_pieces<T>(
476        self: &StateDirectory,
477        kind_str: &'static str,
478        id_writer: InstanceIdWriter,
479        // fn call(kind: &SlugRef, id: &SlugRef, resource_for_error: &impl Fn) -> _
480        call: impl FnOnce(&SlugRef, &SlugRef, &dyn Fn() -> Resource) -> Result<T>,
481    ) -> 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            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
490                (self.0)(f)
491            }
492        }
493        let id_string = InstanceIdDisplay(id_writer).to_string();
494
495        // Both we and caller use this for our error reporting
496        let resource = || Resource::InstanceState {
497            state_dir: self.dir.as_path().to_owned(),
498            kind: kind_str.to_string(),
499            identity: id_string.clone(),
500        };
501
502        let handle_bad_slug = |source| Error::new(source, Action::Initializing, resource());
503
504        if kind_str.is_empty() {
505            return Err(handle_bad_slug(BadSlug::EmptySlugNotAllowed));
506        }
507        let kind = SlugRef::new(kind_str).map_err(handle_bad_slug)?;
508        let id = SlugRef::new(&id_string).map_err(handle_bad_slug)?;
509
510        call(kind, id, &resource)
511    }
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    pub fn list_instances<I: InstanceIdentity>(&self) -> impl Iterator<Item = Result<Slug>> {
529        self.list_instances_inner(I::kind())
530    }
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    fn list_instances_inner(&self, kind: &'static str) -> impl Iterator<Item = Result<Slug>> {
540        // We collect the output into these
541        let mut out = HashSet::new();
542        let mut errs = Vec::new();
543
544        // Error handling
545
546        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        match (|| {
561            let kind = SlugRef::new(kind).map_err(handle_err!())?;
562            self.dir.read_directory(kind).map_err(handle_err!())
563        })() {
564            Err(e) => errs.push(e),
565            Ok(ents) => {
566                for ent in ents {
567                    match ent {
568                        Err(e) => errs.push(handle_err!()(e)),
569                        Ok(ent) => {
570                            // Actually handle a directory entry!
571
572                            let Some(id) = (|| {
573                                // look for either ID or ID.lock
574                                let id = ent.file_name();
575                                let id = id.to_str()?; // ignore non-UTF-8
576                                let id = id.strip_suffix(DOT_LOCK).unwrap_or(id);
577                                let id = SlugRef::new(id).ok()?; // ignore other things
578                                Some(id.to_owned())
579                            })() else {
580                                continue;
581                            };
582
583                            out.insert(id);
584                        }
585                    }
586                }
587            }
588        }
589
590        chain!(errs.into_iter().map(Err), out.into_iter().map(Ok),)
591    }
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    pub fn purge_instances(
651        &self,
652        now: SystemTime,
653        filter: &mut (dyn InstancePurgeHandler + '_),
654    ) -> Result<()> {
655        let kind = filter.kind();
656
657        for id in self.list_instances_inner(kind) {
658            let id = id?;
659            self.with_instance_path_pieces(kind, &|f| write!(f, "{id}"), |kind, id, resource| {
660                self.maybe_purge_instance(now, kind, id, resource, filter)
661            })?;
662        }
663
664        Ok(())
665    }
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    fn maybe_purge_instance(
674        &self,
675        now: SystemTime,
676        kind: &SlugRef,
677        id: &SlugRef,
678        resource: &dyn Fn() -> Resource,
679        filter: &mut (dyn InstancePurgeHandler + '_),
680    ) -> 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        check_liveness!(filter.name_filter(id)?);
690
691        let dir_path = self.dir.as_path().join(kind).join(id);
692
693        // Checks whether it should be kept due to being recently modified.
694        // None::<SystemTime> means the instance directory is ENOENT
695        // (which must mean that the instance exists only as a stale lockfile).
696        let mut age_check = || -> Result<(Liveness, Option<SystemTime>)> {
697            let handle_io_error = |source| Error::new(source, Action::Enumerating, resource());
698
699            // 1. stat the instance dir
700            let md = match fs::metadata(&dir_path) {
701                // If instance dir is ENOENT, treat as old (maybe there was just a lockfile)
702                Err(e) if e.kind() == io::ErrorKind::NotFound => {
703                    return Ok((Liveness::PossiblyUnused, None))
704                }
705                other => other.map_err(handle_io_error)?,
706            };
707            let mtime = md.modified().map_err(handle_io_error)?;
708
709            // 2. calculate the age
710            let age = now.duration_since(mtime).unwrap_or(Duration::ZERO);
711
712            // 3. do the age check
713            let liveness = filter.age_filter(id, age)?;
714
715            Ok((liveness, Some(mtime)))
716        };
717
718        // preliminary check, without locking yet
719        check_liveness!(age_check()?.0);
720
721        // ok we're probably doing to pass it to dispose (for possible deletion)
722
723        let lock_path = dir_path.with_extension(LOCK_EXTN);
724        let flock_guard = match LockFileGuard::try_lock(&lock_path) {
725            Ok(Some(y)) => {
726                trace!("locked {lock_path:?} (for purge)");
727                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        let (age, mtime) = age_check()?;
751        check_liveness!(age);
752
753        // We have locked it and the filters say to maybe purge it.
754
755        match mtime {
756            None => {
757                // And it doesn't even exist!  All we have is a leftover lockfile.  Delete it.
758                let lockfile_rsrc = || Resource::File {
759                    container: lock_path.parent().expect("no /!").into(),
760                    file: lock_path.file_name().expect("no /!").into(),
761                };
762                flock_guard
763                    .delete_lock_file(&lock_path)
764                    .map_err(|source| Error::new(source, Action::Deleting, lockfile_rsrc()))?;
765            }
766            Some(last_modified) => {
767                // Construct a state handle.
768                let dir = self
769                    .dir
770                    .make_secure_directory(format!("{kind}/{id}"))
771                    .map_err(|source| Error::new(source, Action::Enumerating, resource()))?;
772                let flock_guard = Arc::new(flock_guard);
773
774                filter.dispose(
775                    &InstancePurgeInfo {
776                        identity: id,
777                        last_modified,
778                    },
779                    InstanceStateHandle { dir, flock_guard },
780                )?;
781            }
782        }
783
784        Ok(())
785    }
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    pub fn instance_peek_storage<I: InstanceIdentity, T: DeserializeOwned>(
797        &self,
798        identity: &I,
799        key: &(impl TryIntoSlug + ?Sized),
800    ) -> Result<Option<T>> {
801        self.with_instance_path_pieces(
802            I::kind(),
803            &|f| identity.write_identity(f),
804            // This closure is generic over T, so with_instance_path_pieces will be too;
805            // this isn't desirable (code bloat) but avoiding it would involves some contortions.
806            |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                let key_slug = key.try_into_slug()?;
811
812                let rel_fname = format!(
813                    "{}{PATH_SEPARATOR}{}{PATH_SEPARATOR}{}.json",
814                    kind_slug, id_slug, key_slug,
815                );
816
817                let target = load_store::Target {
818                    dir: &self.dir,
819                    rel_fname: rel_fname.as_ref(),
820                };
821
822                target
823                    .load()
824                    // This Resource::File isn't consistent with those from StorageHandle:
825                    // StorageHandle's `container` is the instance directory;
826                    // here `container` is the top-level `state_dir`,
827                    // and `file` is `KIND+INSTANCE/STORAGE.json".
828                    .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                    })
838            },
839        )
840    }
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)]
874pub struct InstanceStateHandle {
875    /// The directory
876    dir: CheckedDir,
877    /// Lock guard
878    flock_guard: Arc<LockFileGuard>,
879}
880
881impl 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    pub fn storage_handle<T>(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<StorageHandle<T>> {
886        /// Implementation, not generic over `slug` and `T`
887        fn inner(
888            ih: &InstanceStateHandle,
889            key: StdResult<Slug, BadSlug>,
890        ) -> Result<(CheckedDir, String, Arc<LockFileGuard>)> {
891            let key = key?;
892            let instance_dir = ih.dir.clone();
893            let leafname = format!("{key}.json");
894            let flock_guard = ih.flock_guard.clone();
895            Ok((instance_dir, leafname, flock_guard))
896        }
897
898        let (instance_dir, leafname, flock_guard) = inner(self, key.try_into_slug())?;
899        Ok(StorageHandle {
900            instance_dir,
901            leafname,
902            marker: PhantomData,
903            flock_guard,
904        })
905    }
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    pub fn raw_subdir(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<InstanceRawSubdir> {
916        /// Implementation, not generic over `slug`
917        fn inner(
918            ih: &InstanceStateHandle,
919            key: StdResult<Slug, BadSlug>,
920        ) -> Result<InstanceRawSubdir> {
921            let key = key?;
922            let irs = (|| {
923                trace!("ensuring/using {:?}/{:?}", ih.dir.as_path(), key.as_str());
924                let dir = ih.dir.make_secure_directory(&key)?;
925                let flock_guard = ih.flock_guard.clone();
926                Ok::<_, ErrorSource>(InstanceRawSubdir { dir, flock_guard })
927            })()
928            .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            })?;
937            touch_instance_dir(&ih.dir)?;
938            Ok(irs)
939        }
940        inner(self, key.try_into_slug())
941    }
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    pub fn purge(self) -> Result<()> {
974        let dir = self.dir.as_path();
975
976        (|| {
977            // use Arc::into_inner on the lock object,
978            // to make sure we're actually the only surviving InstanceStateHandle
979            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            })?;
985
986            trace!("purging {:?} (and {})", dir, DOT_LOCK);
987            fs::remove_dir_all(dir)?;
988            flock_guard.delete_lock_file(
989                // dir.with_extension is right because the last component of dir
990                // is KIND+ID which doesn't contain `.` so no extension will be stripped
991                dir.with_extension(LOCK_EXTN),
992            )?;
993
994            Ok::<_, ErrorSource>(())
995        })()
996        .map_err(|source| {
997            Error::new(
998                source,
999                Action::Deleting,
1000                Resource::Directory { dir: dir.into() },
1001            )
1002        })
1003    }
1004}
1005
1006/// Touch an instance the state directory, `dir`, for expiry purposes
1007fn touch_instance_dir(dir: &CheckedDir) -> Result<()> {
1008    let dir = dir.as_path();
1009    let resource = || Resource::Directory { dir: dir.into() };
1010
1011    filetime::set_file_mtime(dir, filetime::FileTime::now())
1012        .map_err(|source| Error::new(source, Action::Initializing, resource()))
1013}
1014
1015/// A place in the state or cache directory, where we can load/store a serialisable type
1016///
1017/// Implies exclusive access.
1018///
1019/// Rust mutability-xor-sharing rules enforce proper synchronisation,
1020/// unless multiple `StorageHandle`s are created
1021/// using the same [`InstanceStateHandle`] and key.
1022#[derive(Deftly, Debug)] // not Clone, to enforce mutability rules (see above)
1023#[derive_deftly(ContainsInstanceStateGuard)]
1024pub struct StorageHandle<T> {
1025    /// The directory and leafname
1026    instance_dir: CheckedDir,
1027    /// `KEY.json`
1028    leafname: String,
1029    /// We can load and store a `T`.
1030    ///
1031    /// Invariant in `T`.  But we're `Sync` and `Send` regardless of `T`.
1032    /// (From the Table of PhantomData patterns in the Nomicon.)
1033    marker: PhantomData<fn(T) -> T>,
1034    /// Clone of the InstanceStateHandle's lock
1035    flock_guard: Arc<LockFileGuard>,
1036}
1037
1038// Like tor_persist, but writing needs `&mut`
1039impl<T: Serialize + DeserializeOwned> StorageHandle<T> {
1040    /// Load this persistent state
1041    ///
1042    /// `None` means the state was most recently [`delete`](StorageHandle::delete)ed
1043    pub fn load(&self) -> Result<Option<T>> {
1044        self.with_load_store_target(Action::Loading, |t| t.load())
1045    }
1046    /// Store this persistent state
1047    pub fn store(&mut self, v: &T) -> Result<()> {
1048        // The renames will cause a directory mtime update
1049        self.with_load_store_target(Action::Storing, |t| t.store(v))
1050    }
1051    /// Delete this persistent state
1052    pub fn delete(&mut self) -> Result<()> {
1053        // Only counts as a recent modification if this state *did* exist
1054        self.with_load_store_target(Action::Deleting, |t| t.delete())
1055    }
1056
1057    /// Operate using a `load_store::Target`
1058    fn with_load_store_target<R, F>(&self, action: Action, f: F) -> Result<R>
1059    where
1060        F: FnOnce(load_store::Target<'_>) -> std::result::Result<R, ErrorSource>,
1061    {
1062        f(load_store::Target {
1063            dir: &self.instance_dir,
1064            rel_fname: self.leafname.as_ref(),
1065        })
1066        .map_err(self.map_err(action))
1067    }
1068
1069    /// Helper to convert an `ErrorSource` to an `Error`, if we were performing `action`
1070    fn map_err(&self, action: Action) -> impl FnOnce(ErrorSource) -> Error {
1071        let resource = self.err_resource();
1072        move |source| crate::Error::new(source, action, resource)
1073    }
1074
1075    /// Return the proper `Resource` for reporting errors
1076    fn err_resource(&self) -> Resource {
1077        Resource::File {
1078            // TODO ideally we would remember what proportion of instance_dir
1079            // came from the original state_dir, so we can put state_dir in the container
1080            container: self.instance_dir.as_path().to_owned(),
1081            file: self.leafname.clone().into(),
1082        }
1083    }
1084}
1085
1086/// Subdirectory within an instance's state, for raw filesystem operations
1087///
1088/// Dereferences to `fs_mistrust::CheckedDir` and can be used mostly like one.
1089/// Obtained from [`InstanceStateHandle::raw_subdir`].
1090///
1091/// Existence of this value implies exclusive access to the instance.
1092///
1093/// If you need to manage the lock, and the directory path, separately,
1094/// [`raw_lock_guard`](ContainsInstanceStateGuard::raw_lock_guard)
1095///  will help.
1096#[derive(Deref, Clone, Debug, Deftly)]
1097#[derive_deftly(ContainsInstanceStateGuard)]
1098pub struct InstanceRawSubdir {
1099    /// The actual directory, as a [`fs_mistrust::CheckedDir`]
1100    #[deref]
1101    dir: CheckedDir,
1102    /// Clone of the InstanceStateHandle's lock
1103    flock_guard: Arc<LockFileGuard>,
1104}
1105
1106#[cfg(all(test, not(miri) /* filesystem access */))]
1107mod test {
1108    // @@ begin test lint list maintained by maint/add_warning @@
1109    #![allow(clippy::bool_assert_comparison)]
1110    #![allow(clippy::clone_on_copy)]
1111    #![allow(clippy::dbg_macro)]
1112    #![allow(clippy::mixed_attributes_style)]
1113    #![allow(clippy::print_stderr)]
1114    #![allow(clippy::print_stdout)]
1115    #![allow(clippy::single_char_pattern)]
1116    #![allow(clippy::unwrap_used)]
1117    #![allow(clippy::unchecked_duration_subtraction)]
1118    #![allow(clippy::useless_vec)]
1119    #![allow(clippy::needless_pass_by_value)]
1120    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1121
1122    use super::*;
1123    use derive_deftly::{derive_deftly_adhoc, Deftly};
1124    use itertools::{iproduct, Itertools};
1125    use serde::{Deserialize, Serialize};
1126    use std::collections::BTreeSet;
1127    use std::fmt::Display;
1128    use std::fs::File;
1129    use std::io;
1130    use std::str::FromStr;
1131    use test_temp_dir::test_temp_dir;
1132    use tor_basic_utils::PathExt as _;
1133    use tor_error::HasKind as _;
1134    use tracing_test::traced_test;
1135
1136    use tor_error::ErrorKind as TEK;
1137
1138    type AgeDays = i8;
1139
1140    fn days(days: AgeDays) -> Duration {
1141        Duration::from_secs(86400 * u64::try_from(days).unwrap())
1142    }
1143
1144    fn now() -> SystemTime {
1145        SystemTime::now()
1146    }
1147
1148    struct Garlic(Slug);
1149
1150    impl InstanceIdentity for Garlic {
1151        fn kind() -> &'static str {
1152            "garlic"
1153        }
1154        fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1155            Display::fmt(&self.0, f)
1156        }
1157    }
1158
1159    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1160    struct StoredData {
1161        some_value: i32,
1162    }
1163
1164    fn mk_state_dir(dir: &Path) -> StateDirectory {
1165        StateDirectory::new(
1166            dir,
1167            &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
1168        )
1169        .unwrap()
1170    }
1171
1172    #[test]
1173    #[traced_test]
1174    fn test_api() {
1175        test_temp_dir!().used_by(|dir| {
1176            let sd = mk_state_dir(dir);
1177
1178            let garlic = Garlic("wild".try_into_slug().unwrap());
1179
1180            let acquire_instance = || sd.acquire_instance(&garlic);
1181
1182            let ih = acquire_instance().unwrap();
1183            let inst_path = dir.join("garlic/wild");
1184            assert!(fs::metadata(&inst_path).unwrap().is_dir());
1185
1186            assert_eq!(
1187                acquire_instance().unwrap_err().kind(),
1188                TEK::LocalResourceAlreadyInUse,
1189            );
1190
1191            let irsd = ih.raw_subdir("raw").unwrap();
1192            assert!(fs::metadata(irsd.as_path()).unwrap().is_dir());
1193            assert_eq!(irsd.as_path(), dir.join("garlic").join("wild").join("raw"));
1194
1195            let mut sh = ih.storage_handle::<StoredData>("stored_data").unwrap();
1196            let storage_path = dir.join("garlic/wild/stored_data.json");
1197
1198            let peek = || sd.instance_peek_storage(&garlic, "stored_data");
1199
1200            let expect_load = |sh: &StorageHandle<_>, expect| {
1201                let check_loaded = |what, loaded: Result<Option<StoredData>>| {
1202                    assert_eq!(loaded.unwrap().as_ref(), expect, "{what}");
1203                };
1204                check_loaded("load", sh.load());
1205                check_loaded("peek", peek());
1206            };
1207
1208            expect_load(&sh, None);
1209
1210            let to_store = StoredData { some_value: 42 };
1211            sh.store(&to_store).unwrap();
1212            assert!(fs::metadata(storage_path).unwrap().is_file());
1213
1214            expect_load(&sh, Some(&to_store));
1215
1216            sh.delete().unwrap();
1217
1218            expect_load(&sh, None);
1219
1220            drop(sh);
1221            drop(irsd);
1222            ih.purge().unwrap();
1223
1224            assert_eq!(peek().unwrap(), None);
1225            assert_eq!(
1226                fs::metadata(&inst_path).unwrap_err().kind(),
1227                io::ErrorKind::NotFound
1228            );
1229        });
1230    }
1231
1232    #[test]
1233    #[traced_test]
1234    #[allow(clippy::comparison_chain)]
1235    #[allow(clippy::expect_fun_call)]
1236    fn test_iter() {
1237        // Tests list_instances and purge_instances.
1238        //
1239        //  1. Set up a single state directory containing a number of instances,
1240        //    enumerating all the possible situations that purge_instance might find.
1241        //    The instance is identified by a `Which` which specifies its properties,
1242        //    and which is representable as the instance id slug.
1243        //  1b. Put some junk in the state directory too, that we expect to be ignored.
1244        //
1245        //  2. Call list_instances and check that we see what we expect.
1246        //
1247        //  3. Call purge_instances and check that all the callbacks happen as we expect.
1248        //
1249        //  4. Call list_instances again and check that we see what we now expect.
1250        //
1251        //  5. Check that the junk is still present.
1252
1253        let temp_dir = test_temp_dir!();
1254        let state_dir = temp_dir.used_by(mk_state_dir);
1255
1256        /// Reified test case spec for expiry
1257        //
1258        // For non-`bool` fields, `#[deftly(test = )]` gives the set of values to test.
1259        #[derive(Deftly, Eq, PartialEq, Debug)]
1260        #[derive_deftly_adhoc]
1261        struct Which {
1262            /// Does `name_filter` return `Live`?
1263            namefilter_live: bool,
1264            /// What is the oldest does `age_filter` will return `Live` for?
1265            #[deftly(test = "0, 2")]
1266            max_age: AgeDays,
1267            /// How long ago was the instance dir actually modified?
1268            #[deftly(test = "-1, 1, 3")]
1269            age: AgeDays,
1270            /// Does the instance dir exist?
1271            dir: bool,
1272            /// Does the instance !lockfile exist?
1273            lockfile: bool,
1274        }
1275
1276        /// Ad-hoc (de)serialisation scheme of `Which` as an instance id (a `Slug`)
1277        ///
1278        /// The serialisation is `n<namefilter_live>_m<max_age>_...`,
1279        /// ie, for each field, the initial letter of its name, followed by the value.
1280        /// (We don't bother suppressing the trailiong `_`).
1281        impl Display for Which {
1282            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1283                derive_deftly_adhoc! {
1284                    Which:
1285                    $(
1286                        write!(
1287                            f, "{}{}_",
1288                            stringify!($fname).chars().next().unwrap(),
1289                            self.$fname,
1290                        )?;
1291                    )
1292                }
1293                Ok(())
1294            }
1295        }
1296        impl FromStr for Which {
1297            type Err = Error;
1298            fn from_str(s: &str) -> Result<Self> {
1299                let mut fields = s.split('_');
1300                derive_deftly_adhoc! {
1301                    Which:
1302                    Ok(Which { $(
1303                        $fname: fields.next().unwrap()
1304                            .split_at(1).1
1305                            .parse().unwrap(),
1306                    )})
1307                }
1308            }
1309        }
1310
1311        impl InstanceIdentity for Which {
1312            fn kind() -> &'static str {
1313                "which"
1314            }
1315            fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1316                Display::fmt(self, f)
1317            }
1318        }
1319
1320        // 0. Calculate all possible whiches
1321
1322        let whiches = {
1323            derive_deftly_adhoc!(
1324                Which:
1325                iproduct!(
1326                    $(
1327                        ${if fmeta(test) { [ ${fmeta(test) as token_stream} ] }
1328                          else { [false, true] }},
1329                    )
1330                    // iproduct hates a trailing comma, so add a dummy element
1331                    // https://github.com/rust-itertools/itertools/issues/868
1332                    [()]
1333                )
1334            )
1335            .map(derive_deftly_adhoc!(
1336                Which:
1337                //
1338                |($( $fname, ) ())| Which { $( $fname, ) }
1339            ))
1340            // if you want to debug one test case, you can do this:
1341            // .filter(|wh| wh.to_string() == "nfalse_r2_a3_lfalse_dtrue_")
1342            .collect_vec()
1343        };
1344
1345        // 1. Create all the test instances, according to the specifications
1346
1347        for which in &whiches {
1348            let s = which.to_string();
1349            println!("{s}");
1350            assert_eq!(&s.parse::<Which>().unwrap(), which);
1351
1352            let inst = state_dir.acquire_instance(which).unwrap();
1353
1354            if !which.dir {
1355                fs::remove_dir_all(inst.dir.as_path()).unwrap();
1356            } else {
1357                let now = now();
1358                let set_mtime = |mtime: SystemTime| {
1359                    filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
1360                };
1361                if which.age > 0 {
1362                    set_mtime(now - days(which.age));
1363                } else if which.age < 0 {
1364                    set_mtime(now + days(-which.age));
1365                };
1366            }
1367
1368            if !which.lockfile {
1369                let lock_path = inst.dir.as_path().with_extension(LOCK_EXTN);
1370                let flock_guard = Arc::into_inner(inst.flock_guard).unwrap();
1371                flock_guard
1372                    .delete_lock_file(&lock_path)
1373                    .expect(&lock_path.display_lossy().to_string());
1374            }
1375        }
1376
1377        // 1b. Create some junk that should be ignored
1378
1379        let junk = {
1380            let mut junk = Vec::new();
1381            let base = state_dir.dir.as_path();
1382            for rhs in ["+bad", &format!("+bad{DOT_LOCK}"), ".tmp"] {
1383                let mut mk = |lhs, is_dir| {
1384                    let p = base.join(format!("{lhs}{rhs}"));
1385                    junk.push((p.clone(), is_dir));
1386                    p
1387                };
1388                File::create(mk("file", false)).unwrap();
1389                fs::create_dir(mk("dir", true)).unwrap();
1390            }
1391            junk
1392        };
1393
1394        // 2. Check that we see the ones we expect
1395
1396        let list_instances = || {
1397            state_dir
1398                .list_instances::<Which>()
1399                .map(Result::unwrap)
1400                .collect::<BTreeSet<_>>()
1401        };
1402
1403        let found = list_instances();
1404
1405        let expected: BTreeSet<_> = whiches
1406            .iter()
1407            .filter(|which| which.dir || which.lockfile)
1408            .map(|which| Slug::new(which.to_string()).unwrap())
1409            .collect();
1410
1411        itertools::assert_equal(&found, &expected);
1412
1413        // 3. Run a purge and check that we see the expected callbacks
1414
1415        struct PurgeHandler<'r> {
1416            expected: &'r BTreeSet<Slug>,
1417        }
1418
1419        impl Which {
1420            fn old_enough_to_vanish(&self) -> bool {
1421                self.age > self.max_age
1422            }
1423        }
1424
1425        impl InstancePurgeHandler for PurgeHandler<'_> {
1426            fn kind(&self) -> &'static str {
1427                "which"
1428            }
1429            fn name_filter(&mut self, id: &SlugRef) -> Result<Liveness> {
1430                eprintln!("{id} - name_filter");
1431                assert!(self.expected.contains(id));
1432                let which: Which = id.as_str().parse().unwrap();
1433                Ok(if which.namefilter_live {
1434                    Liveness::Live
1435                } else {
1436                    Liveness::PossiblyUnused
1437                })
1438            }
1439            fn age_filter(&mut self, id: &SlugRef, age: Duration) -> Result<Liveness> {
1440                eprintln!("{id} - age_filter({age:?})");
1441                let which: Which = id.as_str().parse().unwrap();
1442                assert!(!which.namefilter_live);
1443                Ok(if age <= days(which.max_age) {
1444                    Liveness::Live
1445                } else {
1446                    Liveness::PossiblyUnused
1447                })
1448            }
1449            fn dispose(
1450                &mut self,
1451                info: &InstancePurgeInfo,
1452                handle: InstanceStateHandle,
1453            ) -> Result<()> {
1454                let id = info.identity();
1455                eprintln!("{id} - dispose");
1456                let which: Which = id.as_str().parse().unwrap();
1457                assert!(!which.namefilter_live);
1458                assert!(which.old_enough_to_vanish());
1459                assert!(which.dir);
1460                handle.purge()
1461            }
1462        }
1463
1464        state_dir
1465            .purge_instances(
1466                now(),
1467                &mut PurgeHandler {
1468                    expected: &expected,
1469                },
1470            )
1471            .unwrap();
1472
1473        // 4. List the instances again and check the results
1474
1475        let found = list_instances();
1476
1477        let expected: BTreeSet<_> = whiches
1478            .iter()
1479            .filter(|which| {
1480                if which.namefilter_live {
1481                    // things filtered by the name filter are left alone;
1482                    // we see them if any bits of them existed, even a stale lockfile
1483                    which.dir || which.lockfile
1484                } else {
1485                    // things *not* filtered by the name filter are retained
1486                    // iff the directory exists and is new enough
1487                    which.dir && !which.old_enough_to_vanish()
1488                }
1489            })
1490            .map(|which| Slug::new(which.to_string()).unwrap())
1491            .collect();
1492
1493        itertools::assert_equal(&found, &expected);
1494
1495        // 5. Check that the junk was ignored
1496
1497        for (p, is_dir) in junk {
1498            let md = fs::metadata(&p).unwrap();
1499            assert_eq!(md.is_dir(), is_dir, "{}", p.display_lossy());
1500        }
1501    }
1502
1503    #[test]
1504    #[traced_test]
1505    fn test_reset_expiry() {
1506        // Tests that things that should update the instance mtime do so,
1507        // and that things that shouldn't, don't.
1508        //
1509        // For each test case, we:
1510        //   1. create a new subdirectory of our temp dir, making a new StateDirectory.
1511        //   2. (optionally) set up one instance within it, containing one pre-prepared
1512        //      existing storage file and one pre-prepared (empty) raw subdir
1513        //   3. perform test-case specific actions on the instance
1514        //   4. run a stunt `purge_instances` call that merely checks
1515        //      that the right value was passed to age_filter
1516
1517        let temp_dir = test_temp_dir!();
1518
1519        const KIND: &str = "kind";
1520
1521        // keys for various sub-objects
1522        const S_EXISTS: &str = "state-existing";
1523        const S_ABSENT: &str = "state-initially-absent";
1524        const R_EXISTS: &str = "raw-subdir-existing";
1525        const R_ABSENT: &str = "raw-subdir-initially-absent";
1526
1527        struct FixedId;
1528        impl InstanceIdentity for FixedId {
1529            fn kind() -> &'static str {
1530                KIND
1531            }
1532            fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1533                write!(f, "id")
1534            }
1535        }
1536
1537        /// Did we expect this test case's actions to change the mtime?
1538        #[derive(PartialEq, Debug)]
1539        enum Expect {
1540            /// mtime should be updated
1541            New,
1542            /// mtime should be unchanged
1543            Old,
1544        }
1545        use Expect as Ex;
1546
1547        /// Callbacks for stunt purge
1548        ///
1549        /// `self == None` means we've called `age_filter` already.
1550        #[allow(non_local_definitions)] // rust-lang/rust#125068
1551        impl InstancePurgeHandler for Option<&'_ Expect> {
1552            fn kind(&self) -> &'static str {
1553                KIND
1554            }
1555            fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
1556                Ok(Liveness::PossiblyUnused)
1557            }
1558            fn age_filter(&mut self, _identity: &SlugRef, age: Duration) -> Result<Liveness> {
1559                let did_reset = if age < days(1) { Ex::New } else { Ex::Old };
1560                assert_eq!(&did_reset, self.unwrap());
1561                *self = None;
1562                // Stop processing the instance
1563                Ok(Liveness::Live)
1564            }
1565            fn dispose(
1566                &mut self,
1567                _info: &InstancePurgeInfo<'_>,
1568                _handle: InstanceStateHandle,
1569            ) -> Result<()> {
1570                panic!("disposed live")
1571            }
1572        }
1573
1574        /// Helper for test that purge iteration doesn't itself update the mtime
1575        ///
1576        /// Says `PossiblyUnused` so that `dispose` gets called,
1577        /// but then just drops the handle and doesn't delete.
1578        struct ExamineAll;
1579        impl InstancePurgeHandler for ExamineAll {
1580            fn kind(&self) -> &'static str {
1581                KIND
1582            }
1583            fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
1584                Ok(Liveness::PossiblyUnused)
1585            }
1586            fn age_filter(&mut self, _identity: &SlugRef, _age: Duration) -> Result<Liveness> {
1587                Ok(Liveness::PossiblyUnused)
1588            }
1589            fn dispose(
1590                &mut self,
1591                _info: &InstancePurgeInfo<'_>,
1592                _handle: InstanceStateHandle,
1593            ) -> Result<()> {
1594                Ok(())
1595            }
1596        }
1597
1598        // Run a check (raw - doesn't creating an initial instance state)
1599        let chk_without_create = |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory)| {
1600            temp_dir.subdir_used_by(which, |dir| {
1601                let state_dir = mk_state_dir(&dir);
1602                acts(&state_dir);
1603
1604                let mut exp = Some(&exp);
1605                state_dir.purge_instances(now(), &mut exp).unwrap();
1606                assert!(exp.is_none(), "age_filter not called, instance missing?");
1607            });
1608        };
1609
1610        // Run a check with a prepared instance state
1611        //
1612        // The prepared instance:
1613        //  - has an existing storage at key S_EXISTS
1614        //  - has an existing empty raw subdir at key R_EXISTS
1615        //  - has been acquired, so `acts` gets an handle
1616        //  - but all of this (looks like it) happened 2 days ago
1617        let chk =
1618            |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory, InstanceStateHandle)| {
1619                chk_without_create(exp, which, &|state_dir| {
1620                    let inst = state_dir.acquire_instance(&FixedId).unwrap();
1621
1622                    inst.storage_handle(S_EXISTS)
1623                        .unwrap()
1624                        .store(&StoredData { some_value: 1 })
1625                        .unwrap();
1626                    inst.raw_subdir(R_EXISTS).unwrap();
1627
1628                    let mtime = now() - days(2);
1629                    filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
1630
1631                    acts(state_dir, inst);
1632                });
1633            };
1634
1635        // Test things that shouldn't count for keeping an instance alive
1636
1637        chk(Ex::Old, "just-releasing-acquired", &|_, inst| {
1638            drop(inst);
1639        });
1640        chk(Ex::Old, "loading", &|_, inst| {
1641            let load = |key| {
1642                inst.storage_handle::<StoredData>(key)
1643                    .unwrap()
1644                    .load()
1645                    .unwrap()
1646            };
1647            assert!(load(S_EXISTS).is_some());
1648            assert!(load(S_ABSENT).is_none());
1649        });
1650        chk(Ex::Old, "messing-in-subdir", &|_, inst| {
1651            // we don't have a raw subdir path here, but we know what it is
1652            let in_raw = inst.dir.as_path().join(R_EXISTS).join("new");
1653            let _: File = File::create(in_raw).unwrap();
1654        });
1655        chk(Ex::Old, "purge-iter-no-delete", &|state_dir, inst| {
1656            drop(inst);
1657            // ExamineAll looks at everything but never calls InstanceStateHandle::purge.
1658            // It it causes every instance to be locked, but not mtime-updated.
1659            state_dir.purge_instances(now(), &mut ExamineAll).unwrap();
1660        });
1661
1662        // Test things that *should* count for keeping an instance alive
1663
1664        chk_without_create(Ex::New, "acquire-new-instance", &|state_dir| {
1665            state_dir.acquire_instance(&FixedId).unwrap();
1666        });
1667        chk(Ex::New, "acquire-existing-instance", &|state_dir, inst| {
1668            drop(inst);
1669            state_dir.acquire_instance(&FixedId).unwrap();
1670        });
1671        for storage_key in [S_EXISTS, S_ABSENT] {
1672            chk(Ex::New, &format!("store-{}", storage_key), &|_, inst| {
1673                inst.storage_handle(storage_key)
1674                    .unwrap()
1675                    .store(&StoredData { some_value: 2 })
1676                    .unwrap();
1677            });
1678        }
1679        for raw_dir in [R_EXISTS, R_ABSENT] {
1680            chk(Ex::New, &format!("raw_subdir-{}", raw_dir), &|_, inst| {
1681                let _: InstanceRawSubdir = inst.raw_subdir(raw_dir).unwrap();
1682            });
1683        }
1684    }
1685}