feat(iam): implementasikan FileSystemSessionLockRepository (sebelumnya belum ada implementasi)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f2d97fb17d
commit
ff6a749c11
@@ -1,2 +1,3 @@
|
|||||||
pub mod oauth_repo;
|
pub mod oauth_repo;
|
||||||
|
pub mod session_lock_repo;
|
||||||
pub mod session_repo;
|
pub mod session_repo;
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
//! Filesystem-backed `SessionLockRepository` implementation.
|
||||||
|
//!
|
||||||
|
//! Ported from `zesdex_entities::seaorm::auth::session_lock::SessionLock`'s
|
||||||
|
//! inherent methods — same atomic-create-based locking, same stale-PID
|
||||||
|
//! recovery via `libc::kill(pid, 0)` plus a `/proc/<pid>/exe` identity
|
||||||
|
//! check to guard against PID reuse. This repository is stateless (no
|
||||||
|
//! `Drop`-based auto-release) — callers that need panic-safety should wrap
|
||||||
|
//! acquisition in their own RAII guard (see `zesdex-backend`'s
|
||||||
|
//! `main.rs::SessionLockGuard`, added in a later task of this plan).
|
||||||
|
use std::fs;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use crate::domain::repository::SessionLockRepository;
|
||||||
|
|
||||||
|
/// Concrete filesystem session-lock repository, using a PID file
|
||||||
|
/// (`<session_dir>/.lock`) with atomic `O_CREAT|O_EXCL` acquisition.
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct FileSystemSessionLockRepository;
|
||||||
|
|
||||||
|
impl FileSystemSessionLockRepository {
|
||||||
|
/// Create a new filesystem session-lock repository.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
FileSystemSessionLockRepository
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionLockRepository for FileSystemSessionLockRepository {
|
||||||
|
fn try_lock(&self, session_dir: &Path) -> anyhow::Result<bool> {
|
||||||
|
let path = session_dir.join(".lock");
|
||||||
|
let pid = std::process::id();
|
||||||
|
|
||||||
|
match fs::OpenOptions::new().create_new(true).write(true).open(&path) {
|
||||||
|
Ok(mut file) => {
|
||||||
|
write!(file, "{pid}")?;
|
||||||
|
file.sync_all()?;
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||||
|
Err(e) => return Err(e.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&path).unwrap_or_default();
|
||||||
|
if let Ok(existing_pid) = content.trim().parse::<u32>() {
|
||||||
|
if self.is_alive(existing_pid) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tmp = path.with_extension("lock.tmp");
|
||||||
|
{
|
||||||
|
let mut tmp_file = fs::OpenOptions::new().create(true).truncate(true).write(true).open(&tmp)?;
|
||||||
|
write!(tmp_file, "{pid}")?;
|
||||||
|
tmp_file.sync_all()?;
|
||||||
|
}
|
||||||
|
fs::rename(&tmp, &path)?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
let _ = fs::File::open(parent).and_then(|d| d.sync_all());
|
||||||
|
}
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unlock(&self, session_dir: &Path) -> anyhow::Result<()> {
|
||||||
|
let path = session_dir.join(".lock");
|
||||||
|
let _ = fs::remove_file(path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_alive(&self, pid: u32) -> bool {
|
||||||
|
// SAFETY: `libc::kill(pid, 0)` sends no signal; it only probes
|
||||||
|
// whether the process exists and is signalable by us.
|
||||||
|
if unsafe { libc::kill(pid as i32, 0) != 0 } {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
|
||||||
|
if let Ok(target) = std::fs::read_link(&proc_exe) {
|
||||||
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
|
if target != exe {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn tmp_dir() -> std::path::PathBuf {
|
||||||
|
let dir = std::env::temp_dir().join(format!("zesdex-iam-lock-test-{}", uuid::Uuid::new_v4()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_lock_succeeds_when_no_lock_file_exists() {
|
||||||
|
let dir = tmp_dir();
|
||||||
|
let repo = FileSystemSessionLockRepository::new();
|
||||||
|
assert!(repo.try_lock(&dir).unwrap());
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_lock_fails_when_held_by_a_live_process() {
|
||||||
|
let dir = tmp_dir();
|
||||||
|
let repo = FileSystemSessionLockRepository::new();
|
||||||
|
assert!(repo.try_lock(&dir).unwrap());
|
||||||
|
// A second acquisition attempt (simulating our own still-live PID)
|
||||||
|
// must fail since the lock file already holds a live PID.
|
||||||
|
assert!(!repo.try_lock(&dir).unwrap());
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn try_lock_recovers_a_stale_lock() {
|
||||||
|
let dir = tmp_dir();
|
||||||
|
let repo = FileSystemSessionLockRepository::new();
|
||||||
|
// Write a lock file with a PID that cannot possibly be alive.
|
||||||
|
std::fs::write(dir.join(".lock"), "999999999").unwrap();
|
||||||
|
assert!(repo.try_lock(&dir).unwrap(), "a stale lock (dead PID) must be recoverable");
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unlock_removes_the_lock_file() {
|
||||||
|
let dir = tmp_dir();
|
||||||
|
let repo = FileSystemSessionLockRepository::new();
|
||||||
|
assert!(repo.try_lock(&dir).unwrap());
|
||||||
|
repo.unlock(&dir).unwrap();
|
||||||
|
assert!(!dir.join(".lock").exists());
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_alive_returns_true_for_current_process() {
|
||||||
|
let repo = FileSystemSessionLockRepository::new();
|
||||||
|
assert!(repo.is_alive(std::process::id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn is_alive_returns_false_for_implausible_pid() {
|
||||||
|
let repo = FileSystemSessionLockRepository::new();
|
||||||
|
assert!(!repo.is_alive(999_999_999));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user