Skip to main content

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//! use web_time_compat::SystemTimeExt;
74//! #
75//! # // fake up some things; we do this rather than using real ones
76//! # // since this example will move, with the module, to a lower level crate.
77//! # struct OnionService { }
78//! # #[derive(derive_more::Display)] struct HsNickname(String);
79//! # type Error = anyhow::Error;
80//! # mod ipt_mgr { pub mod persist {
81//! #     #[derive(serde::Serialize, serde::Deserialize)] pub struct StateRecord {}
82//! # } }
83//!
84//! impl InstanceIdentity for HsNickname {
85//!     fn kind() -> &'static str { "hss" }
86//!     fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
87//!         write!(f, "{self}")
88//!     }
89//! }
90//!
91//! impl OnionService {
92//!     fn new(
93//!         nick: HsNickname,
94//!         state_dir: &StateDirectory,
95//!     ) -> Result<Self, Error> {
96//!         let instance_state = state_dir.acquire_instance(&nick)?;
97//!         let replay_log_dir = instance_state.raw_subdir("ipt_replay")?;
98//!         let ipts_storage: StorageHandle<ipt_mgr::persist::StateRecord> =
99//!             instance_state.storage_handle("ipts")?;
100//!         // ..
101//! #       Ok(OnionService { })
102//!     }
103//! }
104//!
105//! struct PurgeHandler<'h>(&'h HashSet<&'h str>, Duration);
106//! impl InstancePurgeHandler for PurgeHandler<'_> {
107//!     fn kind(&self) -> &'static str {
108//!         <HsNickname as InstanceIdentity>::kind()
109//!     }
110//!     fn name_filter(&mut self, id: &SlugRef) -> state_dir::Result<state_dir::Liveness> {
111//!         Ok(if self.0.contains(id.as_str()) {
112//!             state_dir::Liveness::Live
113//!         } else {
114//!             state_dir::Liveness::PossiblyUnused
115//!         })
116//!     }
117//!     fn age_filter(&mut self, id: &SlugRef, age: Duration)
118//!              -> state_dir::Result<state_dir::Liveness>
119//!     {
120//!         Ok(if age > self.1 {
121//!             state_dir::Liveness::PossiblyUnused
122//!         } else {
123//!             state_dir::Liveness::Live
124//!         })
125//!     }
126//!     fn dispose(&mut self, _info: &InstancePurgeInfo, handle: InstanceStateHandle)
127//!                -> state_dir::Result<()> {
128//!         // here might be a good place to delete keys too
129//!         handle.purge()
130//!     }
131//! }
132//! pub fn expire_hidden_services(
133//!     state_dir: &StateDirectory,
134//!     currently_configured_nicks: &HashSet<&str>,
135//!     retain_for: Duration,
136//! ) -> Result<(), Error> {
137//!     state_dir.purge_instances(
138//!         SystemTime::get(),
139//!         &mut PurgeHandler(currently_configured_nicks, retain_for),
140//!     )?;
141//!     Ok(())
142//! }
143//! ```
144//!
145//! ### Platforms without a filesystem
146//!
147//! The implementation and (in places) the documentation
148//! is in terms of filesystems.
149//! But, everything except `InstanceStateHandle::raw_subdir`
150//! is abstract enough to implement some other way.
151//!
152//! If we wish to support such platforms, the approach is:
153//!
154//!  * Decide on an approach for `StorageHandle`
155//!    and for each caller of `raw_subdir`.
156//!
157//!  * Figure out how the startup code will look.
158//!    (Currently everything is in terms of `fs_mistrust` and filesystems.)
159//!
160//!  * Provide a version of this module with a compatible API
161//!    in terms of whatever underlying facilities are available.
162//!    Use `#[cfg]` to select it.
163//!    Don't implement `raw_subdir`.
164//!
165//!  * Call sites using `raw_subdir` will no longer compile.
166//!    Use `#[cfg]` at call sites to replace the `raw_subdir`
167//!    with whatever is appropriate for the platform.
168
169#![forbid(unsafe_code)] // if you remove this, enable (or write) miri tests (git grep miri)
170
171use std::collections::HashSet;
172use std::fmt::{self, Display};
173use std::fs;
174use std::io;
175use std::marker::PhantomData;
176use std::path::Path;
177use std::sync::Arc;
178use web_time_compat::{Duration, SystemTime, SystemTimeExt};
179
180use derive_deftly::{Deftly, define_derive_deftly};
181use derive_more::{AsRef, Deref};
182use itertools::chain;
183use serde::{Serialize, de::DeserializeOwned};
184
185use fs_mistrust::{CheckedDir, Mistrust};
186use tor_error::ErrorReport as _;
187use tor_error::bad_api_usage;
188use tracing::trace;
189
190pub use crate::Error;
191use crate::err::{Action, ErrorSource, Resource};
192use crate::load_store;
193use crate::slug::{BadSlug, Slug, SlugRef, TryIntoSlug};
194
195#[allow(unused_imports)] // Simplifies a lot of references in our docs
196use crate::slug;
197
198define_derive_deftly! {
199    ContainsInstanceStateGuard:
200
201    impl<$tgens> ContainsInstanceStateGuard for $ttype where $twheres {
202        fn raw_lock_guard(&self) -> Arc<LockFileGuard> {
203            self.flock_guard.clone()
204        }
205    }
206}
207
208/// Re-export of the lock guard type, as obtained via [`ContainsInstanceStateGuard`]
209pub use fslock_guard::LockFileGuard;
210
211use std::result::Result as StdResult;
212
213use std::path::MAIN_SEPARATOR as PATH_SEPARATOR;
214
215/// [`Result`](StdResult) throwing a [`state_dir::Error`](Error)
216pub type Result<T> = StdResult<T, Error>;
217
218/// Extension for lockfiles
219const LOCK_EXTN: &str = "lock";
220/// Suffix for lockfiles, precisely `"." + LOCK_EXTN`
221// There's no way to concatenate constant strings with names!
222// We could use the const_format crate maybe?
223const DOT_LOCK: &str = ".lock";
224
225/// The whole program's state directory
226///
227/// Representation of `[storage] state_dir` and `permissions`
228/// from the Arti configuration.
229///
230/// This type does not embody any subpaths relating to
231/// any particular facility within Arti.
232///
233/// Constructing a `StateDirectory` may involve filesystem permissions checks,
234/// so ideally it would be created once per process for performance reasons.
235///
236/// Existence of a `StateDirectory` also does not imply exclusive access.
237///
238/// This type is passed to each facility's constructor;
239/// the facility implements [`InstanceIdentity`]
240/// and calls [`acquire_instance`](StateDirectory::acquire_instance).
241///
242/// ### Use for caches
243///
244/// In principle this type and the methods and subtypes available
245/// would be suitable for cache data as well as state data.
246///
247/// However the locking scheme does not tolerate random removal of files.
248/// And cache directories are sometimes configured to point to locations
249/// with OS-supplied automatic file cleaning.
250/// That would not be correct,
251/// since the automatic file cleaner might remove an in-use lockfile,
252/// effectively unlocking the instance state
253/// even while a process exists that thinks it still has the lock.
254#[derive(Debug, Clone)]
255pub struct StateDirectory {
256    /// The actual directory, including mistrust config
257    dir: CheckedDir,
258}
259
260/// An instance of a facility that wants to save persistent state (caller-provided impl)
261///
262/// Each value of a type implementing `InstanceIdentity`
263/// designates a specific instance of a specific facility.
264///
265/// For example, `HsNickname` implements `state_dir::InstanceIdentity`.
266///
267/// The kind and identity are [`slug`]s.
268pub trait InstanceIdentity {
269    /// Return the kind.  For example `hss` for a Tor Hidden Service.
270    ///
271    /// This must return a fixed string,
272    /// since usually all instances represented the same Rust type
273    /// are also the same kind.
274    ///
275    /// The returned value must be valid as a [`slug`].
276    //
277    // This precludes dynamically chosen instance kind identifiers.
278    // If we ever want that, we'd need an InstanceKind trait that is implemented
279    // not for actual instances, but for values representing a kind.
280    fn kind() -> &'static str;
281
282    /// Obtain identity
283    ///
284    /// The instance identity distinguishes different instances of the same kind.
285    ///
286    /// For example, for a Tor Hidden Service the identity is the nickname.
287    ///
288    /// The generated string must be valid as a [`slug`].
289    /// If it is not, the functions in this module will throw `Bug` errors.
290    /// (Returning `fmt::Error` will cause a panic, as is usual with the fmt API.)
291    fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result;
292}
293
294/// For a facility to be expired using [`purge_instances`](StateDirectory::purge_instances) (caller-provided impl)
295///
296/// A filter which decides which instances to delete,
297/// and deletes them if appropriate.
298///
299/// See [`purge_instances`](StateDirectory::purge_instances) for full documentation.
300pub trait InstancePurgeHandler {
301    /// What kind to iterate over
302    fn kind(&self) -> &'static str;
303
304    /// Can we tell by its name that this instance is still live ?
305    fn name_filter(&mut self, identity: &SlugRef) -> Result<Liveness>;
306
307    /// Can we tell by recent modification that this instance is still live ?
308    ///
309    /// Many implementations won't need to use the `identity` parameter.
310    ///
311    /// ### Concurrency
312    ///
313    /// The `age` passed to this callback might
314    /// sometimes not be the most recent modification time of the instance.
315    /// But. before calling `dispose`, `purge_instances` will call this
316    /// function at least once with a fully up-to-date modification time.
317    fn age_filter(&mut self, identity: &SlugRef, age: Duration) -> Result<Liveness>;
318
319    /// Decide whether to keep this instance
320    ///
321    /// When it has made its decision, `dispose` should
322    /// either call [`delete`](InstanceStateHandle::purge),
323    /// or simply drop `handle`.
324    ///
325    /// Called only after `name_filter` and `age_filter`
326    /// both returned [`Liveness::PossiblyUnused`].
327    ///
328    /// `info` includes the instance name and other useful information
329    /// such as the last modification time.
330    ///
331    /// Note that although the existence of `handle` implies
332    /// there can be no other `InstanceStateHandle`s for this instance,
333    /// the last modification time of this instance has *not* been updated,
334    /// as it would be by [`acquire_instance`](StateDirectory::acquire_instance).
335    fn dispose(&mut self, info: &InstancePurgeInfo, handle: InstanceStateHandle) -> Result<()>;
336}
337
338/// Information about an instance, passed to [`InstancePurgeHandler::dispose`]
339#[derive(Debug, Clone, amplify::Getters, AsRef)]
340pub struct InstancePurgeInfo<'i> {
341    /// The instance's identity string
342    #[as_ref]
343    identity: &'i SlugRef,
344
345    /// When the instance state was last updated, according to the filesystem timestamps
346    ///
347    /// See `[InstanceStateHandle::purge_instances]`
348    /// for details of what kinds of events count as modifications.
349    last_modified: SystemTime,
350}
351
352/// Is an instance still relevant?
353///
354/// Returned by [`InstancePurgeHandler::name_filter`].
355///
356/// See [`StateDirectory::purge_instances`] for details of the semantics.
357#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
358#[allow(clippy::exhaustive_enums)] // this is a boolean
359pub enum Liveness {
360    /// This instance is not known to be interesting
361    ///
362    /// It could be perhaps expired, if it's been long enough
363    PossiblyUnused,
364    /// This instance is still wanted
365    Live,
366}
367
368/// Objects that co-own a lock on an instance
369///
370/// Each type implementing this trait mutually excludes independently-acquired
371/// [`InstanceStateHandle`]s, and anything derived from them
372/// (including, therefore, `ContainsInstanceStateGuard` implementors
373/// with independent provenance.)
374pub trait ContainsInstanceStateGuard {
375    /// Obtain a raw clone of the underlying filesystem lock
376    ///
377    /// This lock (and clones of it) will mutually exclude
378    /// re-acquisition of the same instance.
379    fn raw_lock_guard(&self) -> Arc<LockFileGuard>;
380}
381
382/// Instance identity string formatter, type-erased
383type InstanceIdWriter<'i> = &'i dyn Fn(&mut fmt::Formatter) -> fmt::Result;
384
385impl StateDirectory {
386    /// Create a new `StateDirectory` from a directory and mistrust configuration
387    pub fn new(state_dir: impl AsRef<Path>, mistrust: &Mistrust) -> Result<Self> {
388        /// Implementation, taking non-generic path
389        fn inner(path: &Path, mistrust: &Mistrust) -> Result<StateDirectory> {
390            let resource = || Resource::Directory {
391                dir: path.to_owned(),
392            };
393            let handle_err = |source| Error::new(source, Action::Initializing, resource());
394
395            let dir = mistrust
396                .verifier()
397                .make_secure_dir(path)
398                .map_err(handle_err)?;
399
400            Ok(StateDirectory { dir })
401        }
402        inner(state_dir.as_ref(), mistrust)
403    }
404
405    /// Acquires (creates and locks) a storage for an instance
406    ///
407    /// Ensures the existence and suitability of a subdirectory named `kind/identity`,
408    /// and locks it for exclusive access.
409    pub fn acquire_instance<I: InstanceIdentity>(
410        &self,
411        identity: &I,
412    ) -> Result<InstanceStateHandle> {
413        /// Implementation, taking non-generic values for identity
414        fn inner(
415            sd: &StateDirectory,
416            kind_str: &'static str,
417            id_writer: InstanceIdWriter,
418        ) -> Result<InstanceStateHandle> {
419            sd.with_instance_path_pieces(kind_str, id_writer, |kind, id, resource| {
420                let handle_err =
421                    |action, source: ErrorSource| Error::new(source, action, resource());
422
423                // Obtain (creating if necessary) a subdir for a Checked
424                let make_secure_directory = |parent: &CheckedDir, subdir| {
425                    let resource = || Resource::Directory {
426                        dir: parent.as_path().join(subdir),
427                    };
428                    parent
429                        .make_secure_directory(subdir)
430                        .map_err(|source| Error::new(source, Action::Initializing, resource()))
431                };
432
433                // ---- obtain the lock ----
434
435                let kind_dir = make_secure_directory(&sd.dir, kind)?;
436
437                let lock_path = kind_dir
438                    .join(format!("{id}.{LOCK_EXTN}"))
439                    .map_err(|source| handle_err(Action::Initializing, source.into()))?;
440
441                let flock_guard = match LockFileGuard::try_lock(&lock_path) {
442                    Ok(Some(y)) => {
443                        trace!("locked {lock_path:?}");
444                        y.into()
445                    }
446                    Err(source) => {
447                        trace!("locking {lock_path:?}, error {}", source.report());
448                        return Err(handle_err(Action::Locking, source.into()));
449                    }
450                    Ok(None) => {
451                        trace!("locking {lock_path:?}, in use",);
452                        return Err(handle_err(Action::Locking, ErrorSource::AlreadyLocked));
453                    }
454                };
455
456                // ---- we have the lock, calculate the directory (creating it if need be) ----
457
458                let dir = make_secure_directory(&kind_dir, id)?;
459
460                touch_instance_dir(&dir)?;
461
462                Ok(InstanceStateHandle { dir, flock_guard })
463            })
464        }
465
466        inner(self, I::kind(), &|f| identity.write_identity(f))
467    }
468
469    /// Given a kind and id, obtain pieces of its path and call a "doing work" callback
470    ///
471    /// This function factors out common functionality needed by
472    /// [`StateDirectory::acquire_instance`] and [`StateDirectory::instance_peek_storage`],
473    /// particularly relating to instance kind and id, and errors.
474    ///
475    /// `kind` and `id` are from an `InstanceIdentity`.
476    fn with_instance_path_pieces<T>(
477        self: &StateDirectory,
478        kind_str: &'static str,
479        id_writer: InstanceIdWriter,
480        // fn call(kind: &SlugRef, id: &SlugRef, resource_for_error: &impl Fn) -> _
481        call: impl FnOnce(&SlugRef, &SlugRef, &dyn Fn() -> Resource) -> Result<T>,
482    ) -> Result<T> {
483        /// Struct that impls `Display` for formatting an instance id
484        //
485        // This exists because we want implementors of InstanceIdentity to be able to
486        // use write! to format their identity string.
487        struct InstanceIdDisplay<'i>(InstanceIdWriter<'i>);
488
489        impl Display for InstanceIdDisplay<'_> {
490            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
491                (self.0)(f)
492            }
493        }
494        let id_string = InstanceIdDisplay(id_writer).to_string();
495
496        // Both we and caller use this for our error reporting
497        let resource = || Resource::InstanceState {
498            state_dir: self.dir.as_path().to_owned(),
499            kind: kind_str.to_string(),
500            identity: id_string.clone(),
501        };
502
503        let handle_bad_slug = |source| Error::new(source, Action::Initializing, resource());
504
505        if kind_str.is_empty() {
506            return Err(handle_bad_slug(BadSlug::EmptySlugNotAllowed));
507        }
508        let kind = SlugRef::new(kind_str).map_err(handle_bad_slug)?;
509        let id = SlugRef::new(&id_string).map_err(handle_bad_slug)?;
510
511        call(kind, id, &resource)
512    }
513
514    /// List the instances of a particular kind
515    ///
516    /// Returns the instance identities.
517    ///
518    /// (The implementation lists subdirectories named `kind_*`.)
519    ///
520    /// Concurrency:
521    /// An instance which is not being removed or created will be
522    /// listed (or not) according to whether it's present.
523    /// But, in the presence of concurrent calls to `acquire_instance` and `delete`
524    /// on different instances,
525    /// is not guaranteed to provide a snapshot:
526    /// serialisation is not guaranteed across different instances.
527    ///
528    /// It *is* guaranteed to list each instance only once.
529    pub fn list_instances<I: InstanceIdentity>(
530        &self,
531    ) -> impl Iterator<Item = Result<Slug>> + use<I> {
532        self.list_instances_inner(I::kind())
533    }
534
535    /// List the instances of a kind, where the kind is supplied as a value
536    ///
537    /// Used by `list_instances` and `purge_instances`.
538    ///
539    /// *Includes* instances that exists only as a stale lockfile.
540    #[allow(clippy::blocks_in_conditions)] // TODO #1176 this wants to be global
541    #[allow(clippy::redundant_closure_call)] // false positive, re handle_err
542    fn list_instances_inner(
543        &self,
544        kind: &'static str,
545    ) -> impl Iterator<Item = Result<Slug>> + use<> {
546        // We collect the output into these
547        let mut out = HashSet::new();
548        let mut errs = Vec::new();
549
550        // Error handling
551
552        let resource = || Resource::InstanceState {
553            state_dir: self.dir.as_path().into(),
554            kind: kind.into(),
555            identity: "*".into(),
556        };
557
558        /// `fn handle_err!()(source: impl Into<ErrorSource>) -> Error`
559        //
560        // (Generic, so can't be a closure.  Uses local bindings, so can't be a fn.)
561        macro_rules! handle_err { { } => {
562            |source| Error::new(source, Action::Enumerating, resource())
563        } }
564
565        // Obtain an iterator of Result<DirEntry>
566        match (|| {
567            let kind = SlugRef::new(kind).map_err(handle_err!())?;
568            self.dir.read_directory(kind).map_err(handle_err!())
569        })() {
570            Err(e) => errs.push(e),
571            Ok(ents) => {
572                for ent in ents {
573                    match ent {
574                        Err(e) => errs.push(handle_err!()(e)),
575                        Ok(ent) => {
576                            // Actually handle a directory entry!
577
578                            let Some(id) = (|| {
579                                // look for either ID or ID.lock
580                                let id = ent.file_name();
581                                let id = id.to_str()?; // ignore non-UTF-8
582                                let id = id.strip_suffix(DOT_LOCK).unwrap_or(id);
583                                let id = SlugRef::new(id).ok()?; // ignore other things
584                                Some(id.to_owned())
585                            })() else {
586                                continue;
587                            };
588
589                            out.insert(id);
590                        }
591                    }
592                }
593            }
594        }
595
596        chain!(errs.into_iter().map(Err), out.into_iter().map(Ok),)
597    }
598
599    /// Delete instances according to selections made by the caller
600    ///
601    /// Each instance is considered in three stages.
602    ///
603    /// Firstly, it is passed to [`name_filter`](InstancePurgeHandler::name_filter).
604    /// If `name_filter` returns `Live`,
605    /// further consideration is skipped and the instance is retained.
606    ///
607    /// Secondly, the last time the instance was written to is determined,
608    // This must be done with the lock held, for correctness
609    // but the lock must be acquired in a way that doesn't itself update the modification time.
610    // On Unix this is straightforward because opening for write doesn't update the mtime.
611    // If this is hard on another platform, we'll need a separate stamp file updated
612    // by an explicit Acquire operation.
613    // This is tested by `test_reset_expiry`.
614    /// and passed to
615    /// [`age_filter`](InstancePurgeHandler::age_filter).
616    /// Again, this might mean ensure the instance is retained.
617    ///
618    /// Thirdly, the resulting `InstanceStateHandle` is passed to
619    /// [`dispose`](InstancePurgeHandler::dispose).
620    /// `dispose` may choose to call `handle.delete()`,
621    /// or simply drop the handle.
622    ///
623    /// Concurrency:
624    /// In the presence of multiple concurrent calls to `acquire_instance` and `delete`:
625    /// `filter` may be called for an instance which is being created or deleted
626    /// by another task.
627    /// `dispose` will be properly serialised with other activities on the same instance,
628    /// as implied by it receiving an `InstanceStateHandle`.
629    ///
630    /// The expiry time is reset by calls to `acquire_instance`,
631    /// `StorageHandle::store` and `InstanceStateHandle::raw_subdir`;
632    /// it *may* be reset by calls to `StorageHandle::delete`.
633    ///
634    /// Instances that are currently locked by another task will not be purged,
635    /// but the expiry time is *not* reset by *unlocking* an instance
636    /// (dropping the last clone of an `InstanceStateHandle`).
637    ///
638    /// ### Sequencing of `InstancePurgeHandler` callbacks
639    ///
640    /// Each instance will be processed
641    /// (and callbacks made for it) at most once;
642    /// and calls for different instances will not be interleaved.
643    ///
644    /// During the processing of a particular instance
645    /// The callbacks will be made in order,
646    /// progressing monotonically through the methods in the order listed.
647    /// But `name_filter` and `age_filter` might each be called
648    /// more than once for the same instance.
649    // We don't actually call name_filter more than once.
650    ///
651    /// Between each stage,
652    /// the purge implementation may discover that the instance
653    /// ought not to be processed further.
654    /// So returning `Liveness::PossiblyUnused` from a filter does not
655    /// guarantee that the next callback will be made.
656    pub fn purge_instances(
657        &self,
658        now: SystemTime,
659        filter: &mut (dyn InstancePurgeHandler + '_),
660    ) -> Result<()> {
661        let kind = filter.kind();
662
663        for id in self.list_instances_inner(kind) {
664            let id = id?;
665            self.with_instance_path_pieces(kind, &|f| write!(f, "{id}"), |kind, id, resource| {
666                self.maybe_purge_instance(now, kind, id, resource, filter)
667            })?;
668        }
669
670        Ok(())
671    }
672
673    /// Consider whether to purge an instance
674    ///
675    /// Performs all the necessary steps, including liveness checks,
676    /// passing an InstanceStateHandle to filter.dispose,
677    /// and deleting stale lockfiles without associated state.
678    #[allow(clippy::cognitive_complexity)] // splitting this would be more, not less, confusing
679    fn maybe_purge_instance(
680        &self,
681        now: SystemTime,
682        kind: &SlugRef,
683        id: &SlugRef,
684        resource: &dyn Fn() -> Resource,
685        filter: &mut (dyn InstancePurgeHandler + '_),
686    ) -> Result<()> {
687        /// If `$l` is `Liveness::Live`, returns early with `Ok(())`.
688        macro_rules! check_liveness { { $l:expr } => {
689            match $l {
690                Liveness::Live => return Ok(()),
691                Liveness::PossiblyUnused => {},
692            }
693        } }
694
695        check_liveness!(filter.name_filter(id)?);
696
697        let dir_path = self.dir.as_path().join(kind).join(id);
698
699        // Checks whether it should be kept due to being recently modified.
700        // None::<SystemTime> means the instance directory is ENOENT
701        // (which must mean that the instance exists only as a stale lockfile).
702        let mut age_check = || -> Result<(Liveness, Option<SystemTime>)> {
703            let handle_io_error = |source| Error::new(source, Action::Enumerating, resource());
704
705            // 1. stat the instance dir
706            let md = match fs::metadata(&dir_path) {
707                // If instance dir is ENOENT, treat as old (maybe there was just a lockfile)
708                Err(e) if e.kind() == io::ErrorKind::NotFound => {
709                    return Ok((Liveness::PossiblyUnused, None));
710                }
711                other => other.map_err(handle_io_error)?,
712            };
713            let mtime = md.modified().map_err(handle_io_error)?;
714
715            // 2. calculate the age
716            let age = now.duration_since(mtime).unwrap_or(Duration::ZERO);
717
718            // 3. do the age check
719            let liveness = filter.age_filter(id, age)?;
720
721            Ok((liveness, Some(mtime)))
722        };
723
724        // preliminary check, without locking yet
725        check_liveness!(age_check()?.0);
726
727        // ok we're probably doing to pass it to dispose (for possible deletion)
728
729        let lock_path = dir_path.with_extension(LOCK_EXTN);
730        let flock_guard = match LockFileGuard::try_lock(&lock_path) {
731            Ok(Some(y)) => {
732                trace!("locked {lock_path:?} (for purge)");
733                y
734            }
735            Err(source) if source.kind() == io::ErrorKind::NotFound => {
736                // We couldn't open the lockfile due to ENOENT
737                // (Presumably) a containing directory is gone, so we don't need to do anything.
738                trace!("locking {lock_path:?} (for purge), not found");
739                return Ok(());
740            }
741            Ok(None) => {
742                // Someone else has it locked.  Skip purging it.
743                trace!("locking {lock_path:?} (for purge), in use");
744                return Ok(());
745            }
746            Err(source) => {
747                trace!(
748                    "locking {lock_path:?} (for purge), error {}",
749                    source.report()
750                );
751                return Err(Error::new(source, Action::Locking, resource()));
752            }
753        };
754
755        // recheck to see if anyone has updated it
756        let (age, mtime) = age_check()?;
757        check_liveness!(age);
758
759        // We have locked it and the filters say to maybe purge it.
760
761        match mtime {
762            None => {
763                // And it doesn't even exist!  All we have is a leftover lockfile.  Delete it.
764                let lockfile_rsrc = || Resource::File {
765                    container: lock_path.parent().expect("no /!").into(),
766                    file: lock_path.file_name().expect("no /!").into(),
767                };
768                flock_guard
769                    .delete_lock_file(&lock_path)
770                    .map_err(|source| Error::new(source, Action::Deleting, lockfile_rsrc()))?;
771            }
772            Some(last_modified) => {
773                // Construct a state handle.
774                let dir = self
775                    .dir
776                    .make_secure_directory(format!("{kind}/{id}"))
777                    .map_err(|source| Error::new(source, Action::Enumerating, resource()))?;
778                let flock_guard = Arc::new(flock_guard);
779
780                filter.dispose(
781                    &InstancePurgeInfo {
782                        identity: id,
783                        last_modified,
784                    },
785                    InstanceStateHandle { dir, flock_guard },
786                )?;
787            }
788        }
789
790        Ok(())
791    }
792
793    /// Tries to peek at something written by [`StorageHandle::store`]
794    ///
795    /// It is guaranteed that this will return either the `T` that was stored,
796    /// or `None` if `store` was never called,
797    /// or `StorageHandle::delete` was called
798    ///
799    /// So the operation is atomic, but there is no further synchronisation.
800    //
801    // Not sure if we need this, but it's logically permissible
802    pub fn instance_peek_storage<I: InstanceIdentity, T: DeserializeOwned>(
803        &self,
804        identity: &I,
805        key: &(impl TryIntoSlug + ?Sized),
806    ) -> Result<Option<T>> {
807        self.with_instance_path_pieces(
808            I::kind(),
809            &|f| identity.write_identity(f),
810            // This closure is generic over T, so with_instance_path_pieces will be too;
811            // this isn't desirable (code bloat) but avoiding it would involves some contortions.
812            |kind_slug: &SlugRef, id_slug: &SlugRef, _resource| {
813                // Throwing this error here will give a slightly wrong Error for this Bug
814                // (because with_instance_path_pieces has its own notion of Action & Resource)
815                // but that seems OK.
816                let key_slug = key.try_into_slug()?;
817
818                let rel_fname = format!(
819                    "{}{PATH_SEPARATOR}{}{PATH_SEPARATOR}{}.json",
820                    kind_slug, id_slug, key_slug,
821                );
822
823                let target = load_store::Target {
824                    dir: &self.dir,
825                    rel_fname: rel_fname.as_ref(),
826                };
827
828                target
829                    .load()
830                    // This Resource::File isn't consistent with those from StorageHandle:
831                    // StorageHandle's `container` is the instance directory;
832                    // here `container` is the top-level `state_dir`,
833                    // and `file` is `KIND+INSTANCE/STORAGE.json".
834                    .map_err(|source| {
835                        Error::new(
836                            source,
837                            Action::Loading,
838                            Resource::File {
839                                container: self.dir.as_path().to_owned(),
840                                file: rel_fname.into(),
841                            },
842                        )
843                    })
844            },
845        )
846    }
847}
848
849/// State or cache directory for an instance of a facility
850///
851/// Implies exclusive access:
852/// there is only one `InstanceStateHandle` at a time,
853/// across any number of processes, tasks, and threads,
854/// for the same instance.
855///
856/// # Key uniqueness and syntactic restrictions
857///
858/// Methods on `InstanceStateHandle` typically take a [`TryIntoSlug`].
859///
860/// **It is important that keys are distinct within an instance.**
861///
862/// Specifically:
863/// each key provided to a method on the same [`InstanceStateHandle`]
864/// (or a clone of it)
865/// must be different.
866/// Violating this rule does not result in memory-unsafety,
867/// but might result in incorrect operation due to concurrent filesystem access,
868/// including possible data loss and corruption.
869/// (Typically, the key is fixed, and the [`StorageHandle`]s are usually
870/// obtained during instance construction, so ensuring this is straightforward.)
871///
872/// There are also syntactic restrictions on keys.  See [slug].
873// We could implement a runtime check for this by retaining a table of in-use keys,
874// possibly only with `cfg(debug_assertions)`.  However I think this isn't worth the code:
875// it would involve an Arc<Mutex<SlugsInUseTable>> in InstanceStateHnndle and StorageHandle,
876// and Drop impls to remove unused entries (and `raw_subdir` would have imprecise checking
877// unless it returned a Drop newtype around CheckedDir).
878#[derive(Debug, Clone, Deftly)]
879#[derive_deftly(ContainsInstanceStateGuard)]
880pub struct InstanceStateHandle {
881    /// The directory
882    dir: CheckedDir,
883    /// Lock guard
884    flock_guard: Arc<LockFileGuard>,
885}
886
887impl InstanceStateHandle {
888    /// Obtain a [`StorageHandle`], usable for storing/retrieving a `T`
889    ///
890    /// [`key` has syntactic and uniqueness restrictions.](InstanceStateHandle#key-uniqueness-and-syntactic-restrictions)
891    pub fn storage_handle<T>(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<StorageHandle<T>> {
892        /// Implementation, not generic over `slug` and `T`
893        fn inner(
894            ih: &InstanceStateHandle,
895            key: StdResult<Slug, BadSlug>,
896        ) -> Result<(CheckedDir, String, Arc<LockFileGuard>)> {
897            let key = key?;
898            let instance_dir = ih.dir.clone();
899            let leafname = format!("{key}.json");
900            let flock_guard = ih.flock_guard.clone();
901            Ok((instance_dir, leafname, flock_guard))
902        }
903
904        let (instance_dir, leafname, flock_guard) = inner(self, key.try_into_slug())?;
905        Ok(StorageHandle {
906            instance_dir,
907            leafname,
908            marker: PhantomData,
909            flock_guard,
910        })
911    }
912
913    /// Obtain a raw filesystem subdirectory, within the directory for this instance
914    ///
915    /// This API is unsuitable platforms without a filesystem accessible via `std::fs`.
916    /// May therefore only be used within Arti for features
917    /// where we're happy to not to support such platforms (eg WASM without WASI)
918    /// without substantial further work.
919    ///
920    /// [`key` has syntactic and uniqueness restrictions.](InstanceStateHandle#key-uniqueness-and-syntactic-restrictions)
921    pub fn raw_subdir(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<InstanceRawSubdir> {
922        /// Implementation, not generic over `slug`
923        fn inner(
924            ih: &InstanceStateHandle,
925            key: StdResult<Slug, BadSlug>,
926        ) -> Result<InstanceRawSubdir> {
927            let key = key?;
928            let irs = (|| {
929                trace!("ensuring/using {:?}/{:?}", ih.dir.as_path(), key.as_str());
930                let dir = ih.dir.make_secure_directory(&key)?;
931                let flock_guard = ih.flock_guard.clone();
932                Ok::<_, ErrorSource>(InstanceRawSubdir { dir, flock_guard })
933            })()
934            .map_err(|source| {
935                Error::new(
936                    source,
937                    Action::Initializing,
938                    Resource::Directory {
939                        dir: ih.dir.as_path().join(key),
940                    },
941                )
942            })?;
943            touch_instance_dir(&ih.dir)?;
944            Ok(irs)
945        }
946        inner(self, key.try_into_slug())
947    }
948
949    /// Unconditionally delete this instance directory
950    ///
951    /// For expiry, use `StateDirectory::purge_instances`,
952    /// and then call this in the `dispose` method.
953    ///
954    /// Will return a `BadAPIUsage` if other clones of this `InstanceStateHandle` exist.
955    ///
956    /// ### Deletion is *not* atomic
957    ///
958    /// If a deletion operation doesn't complete for any reason
959    /// (maybe it was interrupted, or there was a filesystem access problem),
960    /// *part* of the instance contents may remain.
961    ///
962    /// After such an interrupted deletion,
963    /// storage items ([`StorageHandle`]) are might each independently
964    /// be deleted ([`load`](StorageHandle::load) returns `None`)
965    /// or retained (`Some`).
966    ///
967    /// Deletion of the contents of raw subdirectories
968    /// ([`InstanceStateHandle::raw_subdir`])
969    /// is done with `std::fs::remove_dir_all`.
970    /// If deletion is interrupted, the raw subdirectory may contain partial contents.
971    //
972    // In principle we could provide atomic deletion, but it would lead to instances
973    // that were in "limbo": they exist, but wouldn't appear in list_instances,
974    // and the deletion would need to be completed next time they were acquired
975    // (or during a purge_instances run).
976    //
977    // In practice we expect that callers will not try to use a partially-deleted instance,
978    // and that if they do they will fail with a "state corrupted" error, which would be fine.
979    pub fn purge(self) -> Result<()> {
980        let dir = self.dir.as_path();
981
982        (|| {
983            // use Arc::into_inner on the lock object,
984            // to make sure we're actually the only surviving InstanceStateHandle
985            let flock_guard = Arc::into_inner(self.flock_guard).ok_or_else(|| {
986                bad_api_usage!(
987 "InstanceStateHandle::purge called for {:?}, but other clones of the handle exist",
988                    self.dir.as_path(),
989                )
990            })?;
991
992            trace!("purging {:?} (and {})", dir, DOT_LOCK);
993            fs::remove_dir_all(dir)?;
994            flock_guard.delete_lock_file(
995                // dir.with_extension is right because the last component of dir
996                // is KIND+ID which doesn't contain `.` so no extension will be stripped
997                dir.with_extension(LOCK_EXTN),
998            )?;
999
1000            Ok::<_, ErrorSource>(())
1001        })()
1002        .map_err(|source| {
1003            Error::new(
1004                source,
1005                Action::Deleting,
1006                Resource::Directory { dir: dir.into() },
1007            )
1008        })
1009    }
1010}
1011
1012/// Touch an instance the state directory, `dir`, for expiry purposes
1013fn touch_instance_dir(dir: &CheckedDir) -> Result<()> {
1014    let dir = dir.as_path();
1015    let resource = || Resource::Directory { dir: dir.into() };
1016
1017    let mtime = filetime::FileTime::from_system_time(SystemTime::get());
1018    filetime::set_file_mtime(dir, mtime)
1019        .map_err(|source| Error::new(source, Action::Initializing, resource()))
1020}
1021
1022/// A place in the state or cache directory, where we can load/store a serialisable type
1023///
1024/// Implies exclusive access.
1025///
1026/// Rust mutability-xor-sharing rules enforce proper synchronisation,
1027/// unless multiple `StorageHandle`s are created
1028/// using the same [`InstanceStateHandle`] and key.
1029#[derive(Deftly, Debug)] // not Clone, to enforce mutability rules (see above)
1030#[derive_deftly(ContainsInstanceStateGuard)]
1031pub struct StorageHandle<T> {
1032    /// The directory and leafname
1033    instance_dir: CheckedDir,
1034    /// `KEY.json`
1035    leafname: String,
1036    /// We can load and store a `T`.
1037    ///
1038    /// Invariant in `T`.  But we're `Sync` and `Send` regardless of `T`.
1039    /// (From the Table of PhantomData patterns in the Nomicon.)
1040    marker: PhantomData<fn(T) -> T>,
1041    /// Clone of the InstanceStateHandle's lock
1042    flock_guard: Arc<LockFileGuard>,
1043}
1044
1045// Like tor_persist, but writing needs `&mut`
1046impl<T: Serialize + DeserializeOwned> StorageHandle<T> {
1047    /// Load this persistent state
1048    ///
1049    /// `None` means the state was most recently [`delete`](StorageHandle::delete)ed
1050    pub fn load(&self) -> Result<Option<T>> {
1051        self.with_load_store_target(Action::Loading, |t| t.load())
1052    }
1053    /// Store this persistent state
1054    pub fn store(&mut self, v: &T) -> Result<()> {
1055        // The renames will cause a directory mtime update
1056        self.with_load_store_target(Action::Storing, |t| t.store(v))
1057    }
1058    /// Delete this persistent state
1059    pub fn delete(&mut self) -> Result<()> {
1060        // Only counts as a recent modification if this state *did* exist
1061        self.with_load_store_target(Action::Deleting, |t| t.delete())
1062    }
1063
1064    /// Operate using a `load_store::Target`
1065    fn with_load_store_target<R, F>(&self, action: Action, f: F) -> Result<R>
1066    where
1067        F: FnOnce(load_store::Target<'_>) -> std::result::Result<R, ErrorSource>,
1068    {
1069        f(load_store::Target {
1070            dir: &self.instance_dir,
1071            rel_fname: self.leafname.as_ref(),
1072        })
1073        .map_err(self.map_err(action))
1074    }
1075
1076    /// Helper to convert an `ErrorSource` to an `Error`, if we were performing `action`
1077    fn map_err(&self, action: Action) -> impl FnOnce(ErrorSource) -> Error + use<T> {
1078        let resource = self.err_resource();
1079        move |source| crate::Error::new(source, action, resource)
1080    }
1081
1082    /// Return the proper `Resource` for reporting errors
1083    fn err_resource(&self) -> Resource {
1084        Resource::File {
1085            // TODO ideally we would remember what proportion of instance_dir
1086            // came from the original state_dir, so we can put state_dir in the container
1087            container: self.instance_dir.as_path().to_owned(),
1088            file: self.leafname.clone().into(),
1089        }
1090    }
1091}
1092
1093/// Subdirectory within an instance's state, for raw filesystem operations
1094///
1095/// Dereferences to `fs_mistrust::CheckedDir` and can be used mostly like one.
1096/// Obtained from [`InstanceStateHandle::raw_subdir`].
1097///
1098/// Existence of this value implies exclusive access to the instance.
1099///
1100/// If you need to manage the lock, and the directory path, separately,
1101/// [`raw_lock_guard`](ContainsInstanceStateGuard::raw_lock_guard)
1102///  will help.
1103#[derive(Deref, Clone, Debug, Deftly)]
1104#[derive_deftly(ContainsInstanceStateGuard)]
1105pub struct InstanceRawSubdir {
1106    /// The actual directory, as a [`fs_mistrust::CheckedDir`]
1107    #[deref]
1108    dir: CheckedDir,
1109    /// Clone of the InstanceStateHandle's lock
1110    flock_guard: Arc<LockFileGuard>,
1111}
1112
1113#[cfg(all(test, not(miri) /* filesystem access */))]
1114mod test {
1115    // @@ begin test lint list maintained by maint/add_warning @@
1116    #![allow(clippy::bool_assert_comparison)]
1117    #![allow(clippy::clone_on_copy)]
1118    #![allow(clippy::dbg_macro)]
1119    #![allow(clippy::mixed_attributes_style)]
1120    #![allow(clippy::print_stderr)]
1121    #![allow(clippy::print_stdout)]
1122    #![allow(clippy::single_char_pattern)]
1123    #![allow(clippy::unwrap_used)]
1124    #![allow(clippy::unchecked_time_subtraction)]
1125    #![allow(clippy::useless_vec)]
1126    #![allow(clippy::needless_pass_by_value)]
1127    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
1128
1129    use super::*;
1130    use derive_deftly::{Deftly, derive_deftly_adhoc};
1131    use itertools::{Itertools, iproduct};
1132    use serde::{Deserialize, Serialize};
1133    use std::collections::BTreeSet;
1134    use std::fmt::Display;
1135    use std::fs::File;
1136    use std::io;
1137    use std::str::FromStr;
1138    use test_temp_dir::test_temp_dir;
1139    use tor_basic_utils::PathExt as _;
1140    use tor_error::HasKind as _;
1141    use tracing_test::traced_test;
1142    use web_time_compat::SystemTimeExt;
1143
1144    use tor_error::ErrorKind as TEK;
1145
1146    type AgeDays = i8;
1147
1148    fn days(days: AgeDays) -> Duration {
1149        Duration::from_secs(86400 * u64::try_from(days).unwrap())
1150    }
1151
1152    fn now() -> SystemTime {
1153        SystemTime::get()
1154    }
1155
1156    struct Garlic(Slug);
1157
1158    impl InstanceIdentity for Garlic {
1159        fn kind() -> &'static str {
1160            "garlic"
1161        }
1162        fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1163            Display::fmt(&self.0, f)
1164        }
1165    }
1166
1167    #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
1168    struct StoredData {
1169        some_value: i32,
1170    }
1171
1172    fn mk_state_dir(dir: &Path) -> StateDirectory {
1173        StateDirectory::new(
1174            dir,
1175            &fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
1176        )
1177        .unwrap()
1178    }
1179
1180    #[test]
1181    #[traced_test]
1182    fn test_api() {
1183        test_temp_dir!().used_by(|dir| {
1184            let sd = mk_state_dir(dir);
1185
1186            let garlic = Garlic("wild".try_into_slug().unwrap());
1187
1188            let acquire_instance = || sd.acquire_instance(&garlic);
1189
1190            let ih = acquire_instance().unwrap();
1191            let inst_path = dir.join("garlic/wild");
1192            assert!(fs::metadata(&inst_path).unwrap().is_dir());
1193
1194            assert_eq!(
1195                acquire_instance().unwrap_err().kind(),
1196                TEK::LocalResourceAlreadyInUse,
1197            );
1198
1199            let irsd = ih.raw_subdir("raw").unwrap();
1200            assert!(fs::metadata(irsd.as_path()).unwrap().is_dir());
1201            assert_eq!(irsd.as_path(), dir.join("garlic").join("wild").join("raw"));
1202
1203            let mut sh = ih.storage_handle::<StoredData>("stored_data").unwrap();
1204            let storage_path = dir.join("garlic/wild/stored_data.json");
1205
1206            let peek = || sd.instance_peek_storage(&garlic, "stored_data");
1207
1208            let expect_load = |sh: &StorageHandle<_>, expect| {
1209                let check_loaded = |what, loaded: Result<Option<StoredData>>| {
1210                    assert_eq!(loaded.unwrap().as_ref(), expect, "{what}");
1211                };
1212                check_loaded("load", sh.load());
1213                check_loaded("peek", peek());
1214            };
1215
1216            expect_load(&sh, None);
1217
1218            let to_store = StoredData { some_value: 42 };
1219            sh.store(&to_store).unwrap();
1220            assert!(fs::metadata(storage_path).unwrap().is_file());
1221
1222            expect_load(&sh, Some(&to_store));
1223
1224            sh.delete().unwrap();
1225
1226            expect_load(&sh, None);
1227
1228            drop(sh);
1229            drop(irsd);
1230            ih.purge().unwrap();
1231
1232            assert_eq!(peek().unwrap(), None);
1233            assert_eq!(
1234                fs::metadata(&inst_path).unwrap_err().kind(),
1235                io::ErrorKind::NotFound
1236            );
1237        });
1238    }
1239
1240    #[test]
1241    #[traced_test]
1242    #[allow(clippy::comparison_chain)]
1243    #[allow(clippy::expect_fun_call)]
1244    fn test_iter() {
1245        // Tests list_instances and purge_instances.
1246        //
1247        //  1. Set up a single state directory containing a number of instances,
1248        //    enumerating all the possible situations that purge_instance might find.
1249        //    The instance is identified by a `Which` which specifies its properties,
1250        //    and which is representable as the instance id slug.
1251        //  1b. Put some junk in the state directory too, that we expect to be ignored.
1252        //
1253        //  2. Call list_instances and check that we see what we expect.
1254        //
1255        //  3. Call purge_instances and check that all the callbacks happen as we expect.
1256        //
1257        //  4. Call list_instances again and check that we see what we now expect.
1258        //
1259        //  5. Check that the junk is still present.
1260
1261        let temp_dir = test_temp_dir!();
1262        let state_dir = temp_dir.used_by(mk_state_dir);
1263
1264        /// Reified test case spec for expiry
1265        //
1266        // For non-`bool` fields, `#[deftly(test = )]` gives the set of values to test.
1267        #[derive(Deftly, Eq, PartialEq, Debug)]
1268        #[derive_deftly_adhoc]
1269        struct Which {
1270            /// Does `name_filter` return `Live`?
1271            namefilter_live: bool,
1272            /// What is the oldest does `age_filter` will return `Live` for?
1273            #[deftly(test = "0, 2")]
1274            max_age: AgeDays,
1275            /// How long ago was the instance dir actually modified?
1276            #[deftly(test = "-1, 1, 3")]
1277            age: AgeDays,
1278            /// Does the instance dir exist?
1279            dir: bool,
1280            /// Does the instance !lockfile exist?
1281            lockfile: bool,
1282        }
1283
1284        /// Ad-hoc (de)serialisation scheme of `Which` as an instance id (a `Slug`)
1285        ///
1286        /// The serialisation is `n<namefilter_live>_m<max_age>_...`,
1287        /// ie, for each field, the initial letter of its name, followed by the value.
1288        /// (We don't bother suppressing the trailiong `_`).
1289        impl Display for Which {
1290            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1291                derive_deftly_adhoc! {
1292                    Which:
1293                    $(
1294                        write!(
1295                            f, "{}{}_",
1296                            stringify!($fname).chars().next().unwrap(),
1297                            self.$fname,
1298                        )?;
1299                    )
1300                }
1301                Ok(())
1302            }
1303        }
1304        impl FromStr for Which {
1305            type Err = Error;
1306            fn from_str(s: &str) -> Result<Self> {
1307                let mut fields = s.split('_');
1308                derive_deftly_adhoc! {
1309                    Which:
1310                    Ok(Which { $(
1311                        $fname: fields.next().unwrap()
1312                            .split_at(1).1
1313                            .parse().unwrap(),
1314                    )})
1315                }
1316            }
1317        }
1318
1319        impl InstanceIdentity for Which {
1320            fn kind() -> &'static str {
1321                "which"
1322            }
1323            fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1324                Display::fmt(self, f)
1325            }
1326        }
1327
1328        // 0. Calculate all possible whiches
1329
1330        let whiches = {
1331            derive_deftly_adhoc!(
1332                Which:
1333                iproduct!(
1334                    $(
1335                        ${if fmeta(test) { [ ${fmeta(test) as token_stream} ] }
1336                          else { [false, true] }},
1337                    )
1338                    // iproduct hates a trailing comma, so add a dummy element
1339                    // https://github.com/rust-itertools/itertools/issues/868
1340                    [()]
1341                )
1342            )
1343            .map(derive_deftly_adhoc!(
1344                Which:
1345                //
1346                |($( $fname, ) ())| Which { $( $fname, ) }
1347            ))
1348            // if you want to debug one test case, you can do this:
1349            // .filter(|wh| wh.to_string() == "nfalse_r2_a3_lfalse_dtrue_")
1350            .collect_vec()
1351        };
1352
1353        // 1. Create all the test instances, according to the specifications
1354
1355        for which in &whiches {
1356            let s = which.to_string();
1357            println!("{s}");
1358            assert_eq!(&s.parse::<Which>().unwrap(), which);
1359
1360            let inst = state_dir.acquire_instance(which).unwrap();
1361
1362            if !which.dir {
1363                fs::remove_dir_all(inst.dir.as_path()).unwrap();
1364            } else {
1365                let now = now();
1366                let set_mtime = |mtime: SystemTime| {
1367                    filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
1368                };
1369                if which.age > 0 {
1370                    set_mtime(now - days(which.age));
1371                } else if which.age < 0 {
1372                    set_mtime(now + days(-which.age));
1373                };
1374            }
1375
1376            if !which.lockfile {
1377                let lock_path = inst.dir.as_path().with_extension(LOCK_EXTN);
1378                let flock_guard = Arc::into_inner(inst.flock_guard).unwrap();
1379                flock_guard
1380                    .delete_lock_file(&lock_path)
1381                    .expect(&lock_path.display_lossy().to_string());
1382            }
1383        }
1384
1385        // 1b. Create some junk that should be ignored
1386
1387        let junk = {
1388            let mut junk = Vec::new();
1389            let base = state_dir.dir.as_path();
1390            for rhs in ["+bad", &format!("+bad{DOT_LOCK}"), ".tmp"] {
1391                let mut mk = |lhs, is_dir| {
1392                    let p = base.join(format!("{lhs}{rhs}"));
1393                    junk.push((p.clone(), is_dir));
1394                    p
1395                };
1396                File::create(mk("file", false)).unwrap();
1397                fs::create_dir(mk("dir", true)).unwrap();
1398            }
1399            junk
1400        };
1401
1402        // 2. Check that we see the ones we expect
1403
1404        let list_instances = || {
1405            state_dir
1406                .list_instances::<Which>()
1407                .map(Result::unwrap)
1408                .collect::<BTreeSet<_>>()
1409        };
1410
1411        let found = list_instances();
1412
1413        let expected: BTreeSet<_> = whiches
1414            .iter()
1415            .filter(|which| which.dir || which.lockfile)
1416            .map(|which| Slug::new(which.to_string()).unwrap())
1417            .collect();
1418
1419        itertools::assert_equal(&found, &expected);
1420
1421        // 3. Run a purge and check that we see the expected callbacks
1422
1423        struct PurgeHandler<'r> {
1424            expected: &'r BTreeSet<Slug>,
1425        }
1426
1427        impl Which {
1428            fn old_enough_to_vanish(&self) -> bool {
1429                self.age > self.max_age
1430            }
1431        }
1432
1433        impl InstancePurgeHandler for PurgeHandler<'_> {
1434            fn kind(&self) -> &'static str {
1435                "which"
1436            }
1437            fn name_filter(&mut self, id: &SlugRef) -> Result<Liveness> {
1438                eprintln!("{id} - name_filter");
1439                assert!(self.expected.contains(id));
1440                let which: Which = id.as_str().parse().unwrap();
1441                Ok(if which.namefilter_live {
1442                    Liveness::Live
1443                } else {
1444                    Liveness::PossiblyUnused
1445                })
1446            }
1447            fn age_filter(&mut self, id: &SlugRef, age: Duration) -> Result<Liveness> {
1448                eprintln!("{id} - age_filter({age:?})");
1449                let which: Which = id.as_str().parse().unwrap();
1450                assert!(!which.namefilter_live);
1451                Ok(if age <= days(which.max_age) {
1452                    Liveness::Live
1453                } else {
1454                    Liveness::PossiblyUnused
1455                })
1456            }
1457            fn dispose(
1458                &mut self,
1459                info: &InstancePurgeInfo,
1460                handle: InstanceStateHandle,
1461            ) -> Result<()> {
1462                let id = info.identity();
1463                eprintln!("{id} - dispose");
1464                let which: Which = id.as_str().parse().unwrap();
1465                assert!(!which.namefilter_live);
1466                assert!(which.old_enough_to_vanish());
1467                assert!(which.dir);
1468                handle.purge()
1469            }
1470        }
1471
1472        state_dir
1473            .purge_instances(
1474                now(),
1475                &mut PurgeHandler {
1476                    expected: &expected,
1477                },
1478            )
1479            .unwrap();
1480
1481        // 4. List the instances again and check the results
1482
1483        let found = list_instances();
1484
1485        let expected: BTreeSet<_> = whiches
1486            .iter()
1487            .filter(|which| {
1488                if which.namefilter_live {
1489                    // things filtered by the name filter are left alone;
1490                    // we see them if any bits of them existed, even a stale lockfile
1491                    which.dir || which.lockfile
1492                } else {
1493                    // things *not* filtered by the name filter are retained
1494                    // iff the directory exists and is new enough
1495                    which.dir && !which.old_enough_to_vanish()
1496                }
1497            })
1498            .map(|which| Slug::new(which.to_string()).unwrap())
1499            .collect();
1500
1501        itertools::assert_equal(&found, &expected);
1502
1503        // 5. Check that the junk was ignored
1504
1505        for (p, is_dir) in junk {
1506            let md = fs::metadata(&p).unwrap();
1507            assert_eq!(md.is_dir(), is_dir, "{}", p.display_lossy());
1508        }
1509    }
1510
1511    #[test]
1512    #[traced_test]
1513    fn test_reset_expiry() {
1514        // Tests that things that should update the instance mtime do so,
1515        // and that things that shouldn't, don't.
1516        //
1517        // For each test case, we:
1518        //   1. create a new subdirectory of our temp dir, making a new StateDirectory.
1519        //   2. (optionally) set up one instance within it, containing one pre-prepared
1520        //      existing storage file and one pre-prepared (empty) raw subdir
1521        //   3. perform test-case specific actions on the instance
1522        //   4. run a stunt `purge_instances` call that merely checks
1523        //      that the right value was passed to age_filter
1524
1525        let temp_dir = test_temp_dir!();
1526
1527        const KIND: &str = "kind";
1528
1529        // keys for various sub-objects
1530        const S_EXISTS: &str = "state-existing";
1531        const S_ABSENT: &str = "state-initially-absent";
1532        const R_EXISTS: &str = "raw-subdir-existing";
1533        const R_ABSENT: &str = "raw-subdir-initially-absent";
1534
1535        struct FixedId;
1536        impl InstanceIdentity for FixedId {
1537            fn kind() -> &'static str {
1538                KIND
1539            }
1540            fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
1541                write!(f, "id")
1542            }
1543        }
1544
1545        /// Did we expect this test case's actions to change the mtime?
1546        #[derive(PartialEq, Debug)]
1547        enum Expect {
1548            /// mtime should be updated
1549            New,
1550            /// mtime should be unchanged
1551            Old,
1552        }
1553        use Expect as Ex;
1554
1555        /// Callbacks for stunt purge
1556        ///
1557        /// `self == None` means we've called `age_filter` already.
1558        #[allow(non_local_definitions)] // rust-lang/rust#125068
1559        impl InstancePurgeHandler for Option<&'_ Expect> {
1560            fn kind(&self) -> &'static str {
1561                KIND
1562            }
1563            fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
1564                Ok(Liveness::PossiblyUnused)
1565            }
1566            fn age_filter(&mut self, _identity: &SlugRef, age: Duration) -> Result<Liveness> {
1567                let did_reset = if age < days(1) { Ex::New } else { Ex::Old };
1568                assert_eq!(&did_reset, self.unwrap());
1569                *self = None;
1570                // Stop processing the instance
1571                Ok(Liveness::Live)
1572            }
1573            fn dispose(
1574                &mut self,
1575                _info: &InstancePurgeInfo<'_>,
1576                _handle: InstanceStateHandle,
1577            ) -> Result<()> {
1578                panic!("disposed live")
1579            }
1580        }
1581
1582        /// Helper for test that purge iteration doesn't itself update the mtime
1583        ///
1584        /// Says `PossiblyUnused` so that `dispose` gets called,
1585        /// but then just drops the handle and doesn't delete.
1586        struct ExamineAll;
1587        impl InstancePurgeHandler for ExamineAll {
1588            fn kind(&self) -> &'static str {
1589                KIND
1590            }
1591            fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
1592                Ok(Liveness::PossiblyUnused)
1593            }
1594            fn age_filter(&mut self, _identity: &SlugRef, _age: Duration) -> Result<Liveness> {
1595                Ok(Liveness::PossiblyUnused)
1596            }
1597            fn dispose(
1598                &mut self,
1599                _info: &InstancePurgeInfo<'_>,
1600                _handle: InstanceStateHandle,
1601            ) -> Result<()> {
1602                Ok(())
1603            }
1604        }
1605
1606        // Run a check (raw - doesn't creating an initial instance state)
1607        let chk_without_create = |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory)| {
1608            temp_dir.subdir_used_by(which, |dir| {
1609                let state_dir = mk_state_dir(&dir);
1610                acts(&state_dir);
1611
1612                let mut exp = Some(&exp);
1613                state_dir.purge_instances(now(), &mut exp).unwrap();
1614                assert!(exp.is_none(), "age_filter not called, instance missing?");
1615            });
1616        };
1617
1618        // Run a check with a prepared instance state
1619        //
1620        // The prepared instance:
1621        //  - has an existing storage at key S_EXISTS
1622        //  - has an existing empty raw subdir at key R_EXISTS
1623        //  - has been acquired, so `acts` gets an handle
1624        //  - but all of this (looks like it) happened 2 days ago
1625        let chk =
1626            |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory, InstanceStateHandle)| {
1627                chk_without_create(exp, which, &|state_dir| {
1628                    let inst = state_dir.acquire_instance(&FixedId).unwrap();
1629
1630                    inst.storage_handle(S_EXISTS)
1631                        .unwrap()
1632                        .store(&StoredData { some_value: 1 })
1633                        .unwrap();
1634                    inst.raw_subdir(R_EXISTS).unwrap();
1635
1636                    let mtime = now() - days(2);
1637                    filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
1638
1639                    acts(state_dir, inst);
1640                });
1641            };
1642
1643        // Test things that shouldn't count for keeping an instance alive
1644
1645        chk(Ex::Old, "just-releasing-acquired", &|_, inst| {
1646            drop(inst);
1647        });
1648        chk(Ex::Old, "loading", &|_, inst| {
1649            let load = |key| {
1650                inst.storage_handle::<StoredData>(key)
1651                    .unwrap()
1652                    .load()
1653                    .unwrap()
1654            };
1655            assert!(load(S_EXISTS).is_some());
1656            assert!(load(S_ABSENT).is_none());
1657        });
1658        chk(Ex::Old, "messing-in-subdir", &|_, inst| {
1659            // we don't have a raw subdir path here, but we know what it is
1660            let in_raw = inst.dir.as_path().join(R_EXISTS).join("new");
1661            let _: File = File::create(in_raw).unwrap();
1662        });
1663        chk(Ex::Old, "purge-iter-no-delete", &|state_dir, inst| {
1664            drop(inst);
1665            // ExamineAll looks at everything but never calls InstanceStateHandle::purge.
1666            // It it causes every instance to be locked, but not mtime-updated.
1667            state_dir.purge_instances(now(), &mut ExamineAll).unwrap();
1668        });
1669
1670        // Test things that *should* count for keeping an instance alive
1671
1672        chk_without_create(Ex::New, "acquire-new-instance", &|state_dir| {
1673            state_dir.acquire_instance(&FixedId).unwrap();
1674        });
1675        chk(Ex::New, "acquire-existing-instance", &|state_dir, inst| {
1676            drop(inst);
1677            state_dir.acquire_instance(&FixedId).unwrap();
1678        });
1679        for storage_key in [S_EXISTS, S_ABSENT] {
1680            chk(Ex::New, &format!("store-{}", storage_key), &|_, inst| {
1681                inst.storage_handle(storage_key)
1682                    .unwrap()
1683                    .store(&StoredData { some_value: 2 })
1684                    .unwrap();
1685            });
1686        }
1687        for raw_dir in [R_EXISTS, R_ABSENT] {
1688            chk(Ex::New, &format!("raw_subdir-{}", raw_dir), &|_, inst| {
1689                let _: InstanceRawSubdir = inst.raw_subdir(raw_dir).unwrap();
1690            });
1691        }
1692    }
1693}