#![forbid(unsafe_code)]
use std::collections::HashSet;
use std::fmt::{self, Display};
use std::fs;
use std::io;
use std::marker::PhantomData;
use std::path::Path;
use std::sync::Arc;
use web_time_compat::{Duration, SystemTime, SystemTimeExt};
use derive_deftly::{Deftly, define_derive_deftly};
use derive_more::{AsRef, Deref};
use itertools::chain;
use serde::{Serialize, de::DeserializeOwned};
use fs_mistrust::{CheckedDir, Mistrust};
use tor_error::ErrorReport as _;
use tor_error::bad_api_usage;
use tracing::trace;
pub use crate::Error;
use crate::err::{Action, ErrorSource, Resource};
use crate::load_store;
use crate::slug::{BadSlug, Slug, SlugRef, TryIntoSlug};
#[allow(unused_imports)] use crate::slug;
define_derive_deftly! {
ContainsInstanceStateGuard:
impl<$tgens> ContainsInstanceStateGuard for $ttype where $twheres {
fn raw_lock_guard(&self) -> Arc<LockFileGuard> {
self.flock_guard.clone()
}
}
}
pub use fslock_guard::LockFileGuard;
use std::result::Result as StdResult;
use std::path::MAIN_SEPARATOR as PATH_SEPARATOR;
pub type Result<T> = StdResult<T, Error>;
const LOCK_EXTN: &str = "lock";
const DOT_LOCK: &str = ".lock";
#[derive(Debug, Clone)]
pub struct StateDirectory {
dir: CheckedDir,
}
pub trait InstanceIdentity {
fn kind() -> &'static str;
fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result;
}
pub trait InstancePurgeHandler {
fn kind(&self) -> &'static str;
fn name_filter(&mut self, identity: &SlugRef) -> Result<Liveness>;
fn age_filter(&mut self, identity: &SlugRef, age: Duration) -> Result<Liveness>;
fn dispose(&mut self, info: &InstancePurgeInfo, handle: InstanceStateHandle) -> Result<()>;
}
#[derive(Debug, Clone, amplify::Getters, AsRef)]
pub struct InstancePurgeInfo<'i> {
#[as_ref]
identity: &'i SlugRef,
last_modified: SystemTime,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[allow(clippy::exhaustive_enums)] pub enum Liveness {
PossiblyUnused,
Live,
}
pub trait ContainsInstanceStateGuard {
fn raw_lock_guard(&self) -> Arc<LockFileGuard>;
}
type InstanceIdWriter<'i> = &'i dyn Fn(&mut fmt::Formatter) -> fmt::Result;
impl StateDirectory {
pub fn new(state_dir: impl AsRef<Path>, mistrust: &Mistrust) -> Result<Self> {
fn inner(path: &Path, mistrust: &Mistrust) -> Result<StateDirectory> {
let resource = || Resource::Directory {
dir: path.to_owned(),
};
let handle_err = |source| Error::new(source, Action::Initializing, resource());
let dir = mistrust
.verifier()
.make_secure_dir(path)
.map_err(handle_err)?;
Ok(StateDirectory { dir })
}
inner(state_dir.as_ref(), mistrust)
}
pub fn acquire_instance<I: InstanceIdentity>(
&self,
identity: &I,
) -> Result<InstanceStateHandle> {
fn inner(
sd: &StateDirectory,
kind_str: &'static str,
id_writer: InstanceIdWriter,
) -> Result<InstanceStateHandle> {
sd.with_instance_path_pieces(kind_str, id_writer, |kind, id, resource| {
let handle_err =
|action, source: ErrorSource| Error::new(source, action, resource());
let make_secure_directory = |parent: &CheckedDir, subdir| {
let resource = || Resource::Directory {
dir: parent.as_path().join(subdir),
};
parent
.make_secure_directory(subdir)
.map_err(|source| Error::new(source, Action::Initializing, resource()))
};
let kind_dir = make_secure_directory(&sd.dir, kind)?;
let lock_path = kind_dir
.join(format!("{id}.{LOCK_EXTN}"))
.map_err(|source| handle_err(Action::Initializing, source.into()))?;
let flock_guard = match LockFileGuard::try_lock(&lock_path) {
Ok(Some(y)) => {
trace!("locked {lock_path:?}");
y.into()
}
Err(source) => {
trace!("locking {lock_path:?}, error {}", source.report());
return Err(handle_err(Action::Locking, source.into()));
}
Ok(None) => {
trace!("locking {lock_path:?}, in use",);
return Err(handle_err(Action::Locking, ErrorSource::AlreadyLocked));
}
};
let dir = make_secure_directory(&kind_dir, id)?;
touch_instance_dir(&dir)?;
Ok(InstanceStateHandle { dir, flock_guard })
})
}
inner(self, I::kind(), &|f| identity.write_identity(f))
}
fn with_instance_path_pieces<T>(
self: &StateDirectory,
kind_str: &'static str,
id_writer: InstanceIdWriter,
call: impl FnOnce(&SlugRef, &SlugRef, &dyn Fn() -> Resource) -> Result<T>,
) -> Result<T> {
struct InstanceIdDisplay<'i>(InstanceIdWriter<'i>);
impl Display for InstanceIdDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(self.0)(f)
}
}
let id_string = InstanceIdDisplay(id_writer).to_string();
let resource = || Resource::InstanceState {
state_dir: self.dir.as_path().to_owned(),
kind: kind_str.to_string(),
identity: id_string.clone(),
};
let handle_bad_slug = |source| Error::new(source, Action::Initializing, resource());
if kind_str.is_empty() {
return Err(handle_bad_slug(BadSlug::EmptySlugNotAllowed));
}
let kind = SlugRef::new(kind_str).map_err(handle_bad_slug)?;
let id = SlugRef::new(&id_string).map_err(handle_bad_slug)?;
call(kind, id, &resource)
}
pub fn list_instances<I: InstanceIdentity>(
&self,
) -> impl Iterator<Item = Result<Slug>> + use<I> {
self.list_instances_inner(I::kind())
}
#[allow(clippy::blocks_in_conditions)] #[allow(clippy::redundant_closure_call)] fn list_instances_inner(
&self,
kind: &'static str,
) -> impl Iterator<Item = Result<Slug>> + use<> {
let mut out = HashSet::new();
let mut errs = Vec::new();
let resource = || Resource::InstanceState {
state_dir: self.dir.as_path().into(),
kind: kind.into(),
identity: "*".into(),
};
macro_rules! handle_err { { } => {
|source| Error::new(source, Action::Enumerating, resource())
} }
match (|| {
let kind = SlugRef::new(kind).map_err(handle_err!())?;
self.dir.read_directory(kind).map_err(handle_err!())
})() {
Err(e) => errs.push(e),
Ok(ents) => {
for ent in ents {
match ent {
Err(e) => errs.push(handle_err!()(e)),
Ok(ent) => {
let Some(id) = (|| {
let id = ent.file_name();
let id = id.to_str()?; let id = id.strip_suffix(DOT_LOCK).unwrap_or(id);
let id = SlugRef::new(id).ok()?; Some(id.to_owned())
})() else {
continue;
};
out.insert(id);
}
}
}
}
}
chain!(errs.into_iter().map(Err), out.into_iter().map(Ok),)
}
pub fn purge_instances(
&self,
now: SystemTime,
filter: &mut (dyn InstancePurgeHandler + '_),
) -> Result<()> {
let kind = filter.kind();
for id in self.list_instances_inner(kind) {
let id = id?;
self.with_instance_path_pieces(kind, &|f| write!(f, "{id}"), |kind, id, resource| {
self.maybe_purge_instance(now, kind, id, resource, filter)
})?;
}
Ok(())
}
#[allow(clippy::cognitive_complexity)] fn maybe_purge_instance(
&self,
now: SystemTime,
kind: &SlugRef,
id: &SlugRef,
resource: &dyn Fn() -> Resource,
filter: &mut (dyn InstancePurgeHandler + '_),
) -> Result<()> {
macro_rules! check_liveness { { $l:expr } => {
match $l {
Liveness::Live => return Ok(()),
Liveness::PossiblyUnused => {},
}
} }
check_liveness!(filter.name_filter(id)?);
let dir_path = self.dir.as_path().join(kind).join(id);
let mut age_check = || -> Result<(Liveness, Option<SystemTime>)> {
let handle_io_error = |source| Error::new(source, Action::Enumerating, resource());
let md = match fs::metadata(&dir_path) {
Err(e) if e.kind() == io::ErrorKind::NotFound => {
return Ok((Liveness::PossiblyUnused, None));
}
other => other.map_err(handle_io_error)?,
};
let mtime = md.modified().map_err(handle_io_error)?;
let age = now.duration_since(mtime).unwrap_or(Duration::ZERO);
let liveness = filter.age_filter(id, age)?;
Ok((liveness, Some(mtime)))
};
check_liveness!(age_check()?.0);
let lock_path = dir_path.with_extension(LOCK_EXTN);
let flock_guard = match LockFileGuard::try_lock(&lock_path) {
Ok(Some(y)) => {
trace!("locked {lock_path:?} (for purge)");
y
}
Err(source) if source.kind() == io::ErrorKind::NotFound => {
trace!("locking {lock_path:?} (for purge), not found");
return Ok(());
}
Ok(None) => {
trace!("locking {lock_path:?} (for purge), in use");
return Ok(());
}
Err(source) => {
trace!(
"locking {lock_path:?} (for purge), error {}",
source.report()
);
return Err(Error::new(source, Action::Locking, resource()));
}
};
let (age, mtime) = age_check()?;
check_liveness!(age);
match mtime {
None => {
let lockfile_rsrc = || Resource::File {
container: lock_path.parent().expect("no /!").into(),
file: lock_path.file_name().expect("no /!").into(),
};
flock_guard
.delete_lock_file(&lock_path)
.map_err(|source| Error::new(source, Action::Deleting, lockfile_rsrc()))?;
}
Some(last_modified) => {
let dir = self
.dir
.make_secure_directory(format!("{kind}/{id}"))
.map_err(|source| Error::new(source, Action::Enumerating, resource()))?;
let flock_guard = Arc::new(flock_guard);
filter.dispose(
&InstancePurgeInfo {
identity: id,
last_modified,
},
InstanceStateHandle { dir, flock_guard },
)?;
}
}
Ok(())
}
pub fn instance_peek_storage<I: InstanceIdentity, T: DeserializeOwned>(
&self,
identity: &I,
key: &(impl TryIntoSlug + ?Sized),
) -> Result<Option<T>> {
self.with_instance_path_pieces(
I::kind(),
&|f| identity.write_identity(f),
|kind_slug: &SlugRef, id_slug: &SlugRef, _resource| {
let key_slug = key.try_into_slug()?;
let rel_fname = format!(
"{}{PATH_SEPARATOR}{}{PATH_SEPARATOR}{}.json",
kind_slug, id_slug, key_slug,
);
let target = load_store::Target {
dir: &self.dir,
rel_fname: rel_fname.as_ref(),
};
target
.load()
.map_err(|source| {
Error::new(
source,
Action::Loading,
Resource::File {
container: self.dir.as_path().to_owned(),
file: rel_fname.into(),
},
)
})
},
)
}
}
#[derive(Debug, Clone, Deftly)]
#[derive_deftly(ContainsInstanceStateGuard)]
pub struct InstanceStateHandle {
dir: CheckedDir,
flock_guard: Arc<LockFileGuard>,
}
impl InstanceStateHandle {
pub fn storage_handle<T>(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<StorageHandle<T>> {
fn inner(
ih: &InstanceStateHandle,
key: StdResult<Slug, BadSlug>,
) -> Result<(CheckedDir, String, Arc<LockFileGuard>)> {
let key = key?;
let instance_dir = ih.dir.clone();
let leafname = format!("{key}.json");
let flock_guard = ih.flock_guard.clone();
Ok((instance_dir, leafname, flock_guard))
}
let (instance_dir, leafname, flock_guard) = inner(self, key.try_into_slug())?;
Ok(StorageHandle {
instance_dir,
leafname,
marker: PhantomData,
flock_guard,
})
}
pub fn raw_subdir(&self, key: &(impl TryIntoSlug + ?Sized)) -> Result<InstanceRawSubdir> {
fn inner(
ih: &InstanceStateHandle,
key: StdResult<Slug, BadSlug>,
) -> Result<InstanceRawSubdir> {
let key = key?;
let irs = (|| {
trace!("ensuring/using {:?}/{:?}", ih.dir.as_path(), key.as_str());
let dir = ih.dir.make_secure_directory(&key)?;
let flock_guard = ih.flock_guard.clone();
Ok::<_, ErrorSource>(InstanceRawSubdir { dir, flock_guard })
})()
.map_err(|source| {
Error::new(
source,
Action::Initializing,
Resource::Directory {
dir: ih.dir.as_path().join(key),
},
)
})?;
touch_instance_dir(&ih.dir)?;
Ok(irs)
}
inner(self, key.try_into_slug())
}
pub fn purge(self) -> Result<()> {
let dir = self.dir.as_path();
(|| {
let flock_guard = Arc::into_inner(self.flock_guard).ok_or_else(|| {
bad_api_usage!(
"InstanceStateHandle::purge called for {:?}, but other clones of the handle exist",
self.dir.as_path(),
)
})?;
trace!("purging {:?} (and {})", dir, DOT_LOCK);
fs::remove_dir_all(dir)?;
flock_guard.delete_lock_file(
dir.with_extension(LOCK_EXTN),
)?;
Ok::<_, ErrorSource>(())
})()
.map_err(|source| {
Error::new(
source,
Action::Deleting,
Resource::Directory { dir: dir.into() },
)
})
}
}
fn touch_instance_dir(dir: &CheckedDir) -> Result<()> {
let dir = dir.as_path();
let resource = || Resource::Directory { dir: dir.into() };
let mtime = filetime::FileTime::from_system_time(SystemTime::get());
filetime::set_file_mtime(dir, mtime)
.map_err(|source| Error::new(source, Action::Initializing, resource()))
}
#[derive(Deftly, Debug)] #[derive_deftly(ContainsInstanceStateGuard)]
pub struct StorageHandle<T> {
instance_dir: CheckedDir,
leafname: String,
marker: PhantomData<fn(T) -> T>,
flock_guard: Arc<LockFileGuard>,
}
impl<T: Serialize + DeserializeOwned> StorageHandle<T> {
pub fn load(&self) -> Result<Option<T>> {
self.with_load_store_target(Action::Loading, |t| t.load())
}
pub fn store(&mut self, v: &T) -> Result<()> {
self.with_load_store_target(Action::Storing, |t| t.store(v))
}
pub fn delete(&mut self) -> Result<()> {
self.with_load_store_target(Action::Deleting, |t| t.delete())
}
fn with_load_store_target<R, F>(&self, action: Action, f: F) -> Result<R>
where
F: FnOnce(load_store::Target<'_>) -> std::result::Result<R, ErrorSource>,
{
f(load_store::Target {
dir: &self.instance_dir,
rel_fname: self.leafname.as_ref(),
})
.map_err(self.map_err(action))
}
fn map_err(&self, action: Action) -> impl FnOnce(ErrorSource) -> Error + use<T> {
let resource = self.err_resource();
move |source| crate::Error::new(source, action, resource)
}
fn err_resource(&self) -> Resource {
Resource::File {
container: self.instance_dir.as_path().to_owned(),
file: self.leafname.clone().into(),
}
}
}
#[derive(Deref, Clone, Debug, Deftly)]
#[derive_deftly(ContainsInstanceStateGuard)]
pub struct InstanceRawSubdir {
#[deref]
dir: CheckedDir,
flock_guard: Arc<LockFileGuard>,
}
#[cfg(all(test, not(miri) /* filesystem access */))]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_time_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use derive_deftly::{Deftly, derive_deftly_adhoc};
use itertools::{Itertools, iproduct};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::fmt::Display;
use std::fs::File;
use std::io;
use std::str::FromStr;
use test_temp_dir::test_temp_dir;
use tor_basic_utils::PathExt as _;
use tor_error::HasKind as _;
use tracing_test::traced_test;
use web_time_compat::SystemTimeExt;
use tor_error::ErrorKind as TEK;
type AgeDays = i8;
fn days(days: AgeDays) -> Duration {
Duration::from_secs(86400 * u64::try_from(days).unwrap())
}
fn now() -> SystemTime {
SystemTime::get()
}
struct Garlic(Slug);
impl InstanceIdentity for Garlic {
fn kind() -> &'static str {
"garlic"
}
fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
struct StoredData {
some_value: i32,
}
fn mk_state_dir(dir: &Path) -> StateDirectory {
StateDirectory::new(
dir,
&fs_mistrust::Mistrust::new_dangerously_trust_everyone(),
)
.unwrap()
}
#[test]
#[traced_test]
fn test_api() {
test_temp_dir!().used_by(|dir| {
let sd = mk_state_dir(dir);
let garlic = Garlic("wild".try_into_slug().unwrap());
let acquire_instance = || sd.acquire_instance(&garlic);
let ih = acquire_instance().unwrap();
let inst_path = dir.join("garlic/wild");
assert!(fs::metadata(&inst_path).unwrap().is_dir());
assert_eq!(
acquire_instance().unwrap_err().kind(),
TEK::LocalResourceAlreadyInUse,
);
let irsd = ih.raw_subdir("raw").unwrap();
assert!(fs::metadata(irsd.as_path()).unwrap().is_dir());
assert_eq!(irsd.as_path(), dir.join("garlic").join("wild").join("raw"));
let mut sh = ih.storage_handle::<StoredData>("stored_data").unwrap();
let storage_path = dir.join("garlic/wild/stored_data.json");
let peek = || sd.instance_peek_storage(&garlic, "stored_data");
let expect_load = |sh: &StorageHandle<_>, expect| {
let check_loaded = |what, loaded: Result<Option<StoredData>>| {
assert_eq!(loaded.unwrap().as_ref(), expect, "{what}");
};
check_loaded("load", sh.load());
check_loaded("peek", peek());
};
expect_load(&sh, None);
let to_store = StoredData { some_value: 42 };
sh.store(&to_store).unwrap();
assert!(fs::metadata(storage_path).unwrap().is_file());
expect_load(&sh, Some(&to_store));
sh.delete().unwrap();
expect_load(&sh, None);
drop(sh);
drop(irsd);
ih.purge().unwrap();
assert_eq!(peek().unwrap(), None);
assert_eq!(
fs::metadata(&inst_path).unwrap_err().kind(),
io::ErrorKind::NotFound
);
});
}
#[test]
#[traced_test]
#[allow(clippy::comparison_chain)]
#[allow(clippy::expect_fun_call)]
fn test_iter() {
let temp_dir = test_temp_dir!();
let state_dir = temp_dir.used_by(mk_state_dir);
#[derive(Deftly, Eq, PartialEq, Debug)]
#[derive_deftly_adhoc]
struct Which {
namefilter_live: bool,
#[deftly(test = "0, 2")]
max_age: AgeDays,
#[deftly(test = "-1, 1, 3")]
age: AgeDays,
dir: bool,
lockfile: bool,
}
impl Display for Which {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
derive_deftly_adhoc! {
Which:
$(
write!(
f, "{}{}_",
stringify!($fname).chars().next().unwrap(),
self.$fname,
)?;
)
}
Ok(())
}
}
impl FromStr for Which {
type Err = Error;
fn from_str(s: &str) -> Result<Self> {
let mut fields = s.split('_');
derive_deftly_adhoc! {
Which:
Ok(Which { $(
$fname: fields.next().unwrap()
.split_at(1).1
.parse().unwrap(),
)})
}
}
}
impl InstanceIdentity for Which {
fn kind() -> &'static str {
"which"
}
fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
Display::fmt(self, f)
}
}
let whiches = {
derive_deftly_adhoc!(
Which:
iproduct!(
$(
${if fmeta(test) { [ ${fmeta(test) as token_stream} ] }
else { [false, true] }},
)
[()]
)
)
.map(derive_deftly_adhoc!(
Which:
|($( $fname, ) ())| Which { $( $fname, ) }
))
.collect_vec()
};
for which in &whiches {
let s = which.to_string();
println!("{s}");
assert_eq!(&s.parse::<Which>().unwrap(), which);
let inst = state_dir.acquire_instance(which).unwrap();
if !which.dir {
fs::remove_dir_all(inst.dir.as_path()).unwrap();
} else {
let now = now();
let set_mtime = |mtime: SystemTime| {
filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
};
if which.age > 0 {
set_mtime(now - days(which.age));
} else if which.age < 0 {
set_mtime(now + days(-which.age));
};
}
if !which.lockfile {
let lock_path = inst.dir.as_path().with_extension(LOCK_EXTN);
let flock_guard = Arc::into_inner(inst.flock_guard).unwrap();
flock_guard
.delete_lock_file(&lock_path)
.expect(&lock_path.display_lossy().to_string());
}
}
let junk = {
let mut junk = Vec::new();
let base = state_dir.dir.as_path();
for rhs in ["+bad", &format!("+bad{DOT_LOCK}"), ".tmp"] {
let mut mk = |lhs, is_dir| {
let p = base.join(format!("{lhs}{rhs}"));
junk.push((p.clone(), is_dir));
p
};
File::create(mk("file", false)).unwrap();
fs::create_dir(mk("dir", true)).unwrap();
}
junk
};
let list_instances = || {
state_dir
.list_instances::<Which>()
.map(Result::unwrap)
.collect::<BTreeSet<_>>()
};
let found = list_instances();
let expected: BTreeSet<_> = whiches
.iter()
.filter(|which| which.dir || which.lockfile)
.map(|which| Slug::new(which.to_string()).unwrap())
.collect();
itertools::assert_equal(&found, &expected);
struct PurgeHandler<'r> {
expected: &'r BTreeSet<Slug>,
}
impl Which {
fn old_enough_to_vanish(&self) -> bool {
self.age > self.max_age
}
}
impl InstancePurgeHandler for PurgeHandler<'_> {
fn kind(&self) -> &'static str {
"which"
}
fn name_filter(&mut self, id: &SlugRef) -> Result<Liveness> {
eprintln!("{id} - name_filter");
assert!(self.expected.contains(id));
let which: Which = id.as_str().parse().unwrap();
Ok(if which.namefilter_live {
Liveness::Live
} else {
Liveness::PossiblyUnused
})
}
fn age_filter(&mut self, id: &SlugRef, age: Duration) -> Result<Liveness> {
eprintln!("{id} - age_filter({age:?})");
let which: Which = id.as_str().parse().unwrap();
assert!(!which.namefilter_live);
Ok(if age <= days(which.max_age) {
Liveness::Live
} else {
Liveness::PossiblyUnused
})
}
fn dispose(
&mut self,
info: &InstancePurgeInfo,
handle: InstanceStateHandle,
) -> Result<()> {
let id = info.identity();
eprintln!("{id} - dispose");
let which: Which = id.as_str().parse().unwrap();
assert!(!which.namefilter_live);
assert!(which.old_enough_to_vanish());
assert!(which.dir);
handle.purge()
}
}
state_dir
.purge_instances(
now(),
&mut PurgeHandler {
expected: &expected,
},
)
.unwrap();
let found = list_instances();
let expected: BTreeSet<_> = whiches
.iter()
.filter(|which| {
if which.namefilter_live {
which.dir || which.lockfile
} else {
which.dir && !which.old_enough_to_vanish()
}
})
.map(|which| Slug::new(which.to_string()).unwrap())
.collect();
itertools::assert_equal(&found, &expected);
for (p, is_dir) in junk {
let md = fs::metadata(&p).unwrap();
assert_eq!(md.is_dir(), is_dir, "{}", p.display_lossy());
}
}
#[test]
#[traced_test]
fn test_reset_expiry() {
let temp_dir = test_temp_dir!();
const KIND: &str = "kind";
const S_EXISTS: &str = "state-existing";
const S_ABSENT: &str = "state-initially-absent";
const R_EXISTS: &str = "raw-subdir-existing";
const R_ABSENT: &str = "raw-subdir-initially-absent";
struct FixedId;
impl InstanceIdentity for FixedId {
fn kind() -> &'static str {
KIND
}
fn write_identity(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "id")
}
}
#[derive(PartialEq, Debug)]
enum Expect {
New,
Old,
}
use Expect as Ex;
#[allow(non_local_definitions)] impl InstancePurgeHandler for Option<&'_ Expect> {
fn kind(&self) -> &'static str {
KIND
}
fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
Ok(Liveness::PossiblyUnused)
}
fn age_filter(&mut self, _identity: &SlugRef, age: Duration) -> Result<Liveness> {
let did_reset = if age < days(1) { Ex::New } else { Ex::Old };
assert_eq!(&did_reset, self.unwrap());
*self = None;
Ok(Liveness::Live)
}
fn dispose(
&mut self,
_info: &InstancePurgeInfo<'_>,
_handle: InstanceStateHandle,
) -> Result<()> {
panic!("disposed live")
}
}
struct ExamineAll;
impl InstancePurgeHandler for ExamineAll {
fn kind(&self) -> &'static str {
KIND
}
fn name_filter(&mut self, _identity: &SlugRef) -> Result<Liveness> {
Ok(Liveness::PossiblyUnused)
}
fn age_filter(&mut self, _identity: &SlugRef, _age: Duration) -> Result<Liveness> {
Ok(Liveness::PossiblyUnused)
}
fn dispose(
&mut self,
_info: &InstancePurgeInfo<'_>,
_handle: InstanceStateHandle,
) -> Result<()> {
Ok(())
}
}
let chk_without_create = |exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory)| {
temp_dir.subdir_used_by(which, |dir| {
let state_dir = mk_state_dir(&dir);
acts(&state_dir);
let mut exp = Some(&exp);
state_dir.purge_instances(now(), &mut exp).unwrap();
assert!(exp.is_none(), "age_filter not called, instance missing?");
});
};
let chk =
|exp: Expect, which: &str, acts: &dyn Fn(&StateDirectory, InstanceStateHandle)| {
chk_without_create(exp, which, &|state_dir| {
let inst = state_dir.acquire_instance(&FixedId).unwrap();
inst.storage_handle(S_EXISTS)
.unwrap()
.store(&StoredData { some_value: 1 })
.unwrap();
inst.raw_subdir(R_EXISTS).unwrap();
let mtime = now() - days(2);
filetime::set_file_mtime(inst.dir.as_path(), mtime.into()).unwrap();
acts(state_dir, inst);
});
};
chk(Ex::Old, "just-releasing-acquired", &|_, inst| {
drop(inst);
});
chk(Ex::Old, "loading", &|_, inst| {
let load = |key| {
inst.storage_handle::<StoredData>(key)
.unwrap()
.load()
.unwrap()
};
assert!(load(S_EXISTS).is_some());
assert!(load(S_ABSENT).is_none());
});
chk(Ex::Old, "messing-in-subdir", &|_, inst| {
let in_raw = inst.dir.as_path().join(R_EXISTS).join("new");
let _: File = File::create(in_raw).unwrap();
});
chk(Ex::Old, "purge-iter-no-delete", &|state_dir, inst| {
drop(inst);
state_dir.purge_instances(now(), &mut ExamineAll).unwrap();
});
chk_without_create(Ex::New, "acquire-new-instance", &|state_dir| {
state_dir.acquire_instance(&FixedId).unwrap();
});
chk(Ex::New, "acquire-existing-instance", &|state_dir, inst| {
drop(inst);
state_dir.acquire_instance(&FixedId).unwrap();
});
for storage_key in [S_EXISTS, S_ABSENT] {
chk(Ex::New, &format!("store-{}", storage_key), &|_, inst| {
inst.storage_handle(storage_key)
.unwrap()
.store(&StoredData { some_value: 2 })
.unwrap();
});
}
for raw_dir in [R_EXISTS, R_ABSENT] {
chk(Ex::New, &format!("raw_subdir-{}", raw_dir), &|_, inst| {
let _: InstanceRawSubdir = inst.raw_subdir(raw_dir).unwrap();
});
}
}
}