use std::env;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::SessionId;
use crate::pal::error::{PalError, PalErrorKind};
use crate::pal::session_store::SessionStore;
use crate::pal::session_store::fs_store::windows::{
RecordFile, move_file_no_replace, move_file_replace,
};
use crate::pal::session_store::stored::StoredSession;
use crate::session_record::{ProcessIdentity, SessionRecord};
#[derive(Clone, Debug)]
pub(crate) struct FsSessionStore {
root: PathBuf,
}
fn parse_stored(bytes: &[u8]) -> Result<StoredSession, PalError> {
serde_json::from_slice(bytes).map_err(|error| {
PalError::with_source(
PalErrorKind::Other,
io::Error::new(io::ErrorKind::InvalidData, error),
)
})
}
fn parse_record(bytes: &[u8], expected: SessionId) -> Result<Option<SessionRecord>, PalError> {
let StoredSession::Published(record) = parse_stored(bytes)? else {
return Ok(None);
};
if record.id != expected {
return Ok(None);
}
Ok(Some(record))
}
fn replace_file(tmp: &Path, dest: &Path) -> io::Result<()> {
move_file_replace(tmp, dest)
}
#[cfg_attr(coverage_nightly, coverage(off))]
fn serialization_error(error: serde_json::Error) -> PalError {
PalError::with_source(
PalErrorKind::Other,
io::Error::new(io::ErrorKind::InvalidData, error),
)
}
fn next_session_id(id: SessionId) -> Result<SessionId, PalError> {
id.get()
.checked_add(1)
.and_then(SessionId::from_u32)
.ok_or_else(|| PalError::new(PalErrorKind::Other))
}
pub(crate) fn remove_failed_staging(staging: &Path, installed: &Result<SessionId, PalError>) {
if installed.is_err() {
_ = fs::remove_file(staging);
}
}
impl FsSessionStore {
pub(crate) fn new(root: PathBuf) -> Self {
Self { root }
}
fn record_path(&self, id: SessionId) -> PathBuf {
self.root.join(format!("{}.json", id.get()))
}
fn staging_path(&self, owner: &ProcessIdentity) -> PathBuf {
static NEXT: AtomicU64 = AtomicU64::new(0);
let attempt = NEXT.fetch_add(1, Ordering::Relaxed);
self.root.join(format!(
"{}-{}-{attempt}.claim",
owner.pid, owner.creation_time
))
}
#[cfg_attr(test, mutants::skip)]
pub(crate) fn install_claim(&self, staging: &Path) -> Result<SessionId, PalError> {
let mut id = SessionId::MIN;
loop {
match move_file_no_replace(staging, &self.record_path(id)) {
Ok(()) => return Ok(id),
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
id = next_session_id(id)?;
}
Err(error) => return Err(PalError::from_io(error)),
}
}
}
fn stored(&self) -> Result<Vec<(SessionId, StoredSession)>, PalError> {
let entries = match fs::read_dir(&self.root) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(PalError::from_io(error)),
};
let mut stored = Vec::new();
for entry in entries {
let entry = entry.map_err(PalError::from_io)?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let Some(stem) = name.strip_suffix(".json") else {
continue;
};
let Ok(raw) = stem.parse::<u32>() else {
continue;
};
let Some(id) = SessionId::from_u32(raw) else {
continue;
};
let bytes = match read_if_present(&entry.path()) {
Ok(Some(bytes)) => bytes,
Ok(None) => continue,
Err(error) => return Err(PalError::from_io(error)),
};
let Ok(parsed) = parse_stored(&bytes) else {
continue;
};
stored.push((id, parsed));
}
stored.sort_by_key(|(id, _stored)| id.get());
Ok(stored)
}
}
impl SessionStore for FsSessionStore {
#[cfg(test)]
fn root(&self) -> PathBuf {
self.root.clone()
}
fn allocate_id(&self, owner: &ProcessIdentity) -> Result<SessionId, PalError> {
fs::create_dir_all(&self.root).map_err(PalError::from_io)?;
let claim = serde_json::to_vec(&StoredSession::Reserved { owner: *owner })
.map_err(serialization_error)?;
let staging = self.staging_path(owner);
write_new_file(&staging, &claim).map_err(PalError::from_io)?;
let installed = self.install_claim(&staging);
remove_failed_staging(&staging, &installed);
installed
}
fn publish(&self, record: &SessionRecord) -> Result<(), PalError> {
fs::create_dir_all(&self.root).map_err(PalError::from_io)?;
let id = record.id;
let path = self.record_path(id);
let tmp = self.root.join(format!("{}.json.tmp", id.get()));
let json = serde_json::to_vec_pretty(&StoredSession::Published(record.clone()))
.map_err(serialization_error)?;
let mut file = File::create(&tmp).map_err(PalError::from_io)?;
file.write_all(&json).map_err(PalError::from_io)?;
file.sync_all().map_err(PalError::from_io)?;
drop(file);
replace_file(&tmp, &path).map_err(PalError::from_io)
}
fn read(&self, id: SessionId) -> Result<Option<SessionRecord>, PalError> {
let path = self.record_path(id);
match fs::read(&path) {
Ok(bytes) if bytes.is_empty() => Ok(None),
Ok(bytes) => parse_record(&bytes, id),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(PalError::from_io(error)),
}
}
fn list(&self) -> Result<Vec<SessionRecord>, PalError> {
Ok(self
.stored()?
.into_iter()
.filter_map(|(id, stored)| match stored {
StoredSession::Published(record) if record.id == id => Some(record),
_ => None,
})
.collect())
}
fn list_reservations(&self) -> Result<Vec<(SessionId, ProcessIdentity)>, PalError> {
Ok(self
.stored()?
.into_iter()
.filter_map(|(id, stored)| match stored {
StoredSession::Reserved { owner } => Some((id, owner)),
StoredSession::Published(_record) => None,
})
.collect())
}
fn delete_owned_by(&self, id: SessionId, owner: &ProcessIdentity) -> Result<(), PalError> {
let file = match RecordFile::open(&self.record_path(id)) {
Ok(file) => file,
Err(error) if is_absent(&error) => return Ok(()),
Err(error) => return Err(PalError::from_io(error)),
};
let bytes = file.read().map_err(PalError::from_io)?;
let owned = match parse_stored(&bytes) {
Ok(StoredSession::Reserved { owner: current }) => current == *owner,
Ok(StoredSession::Published(record)) => record.supervisor == *owner,
Err(_error) => false,
};
if !owned {
return Ok(());
}
file.delete().map_err(PalError::from_io)
}
fn canonicalize(&self, path: &Path) -> Result<PathBuf, PalError> {
fs::canonicalize(path).map_err(PalError::from_io)
}
fn current_dir(&self) -> Result<PathBuf, PalError> {
env::current_dir().map_err(PalError::from_io)
}
}
fn is_absent(error: &io::Error) -> bool {
matches!(error.kind(), io::ErrorKind::NotFound)
}
pub(crate) fn read_if_present(path: &Path) -> io::Result<Option<Vec<u8>>> {
match fs::read(path) {
Ok(bytes) => Ok(Some(bytes)),
Err(error) if is_absent(&error) => Ok(None),
Err(error) => Err(error),
}
}
fn write_new_file(path: &Path, content: &[u8]) -> io::Result<()> {
let mut file = OpenOptions::new().write(true).create_new(true).open(path)?;
file.write_all(content)?;
file.sync_all()
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
#[test]
fn advancing_the_last_session_id_reports_exhaustion() {
let last = SessionId::from_u32(u32::MAX).unwrap();
next_session_id(last).unwrap_err();
}
#[test]
fn a_staging_name_carries_the_complete_process_identity() {
let store = FsSessionStore::new(PathBuf::from("store"));
let owner = ProcessIdentity {
pid: 73_u32.try_into().unwrap(),
creation_time: 987_654_321,
};
let name = store
.staging_path(&owner)
.file_name()
.unwrap()
.to_string_lossy()
.into_owned();
assert!(name.starts_with(&format!("{}-{}-", owner.pid, owner.creation_time)));
}
}