use anyhow::{bail, Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
const LOCK_FILE_NAME: &str = ".cctakt/lock";
pub struct LockFile {
path: PathBuf,
}
impl LockFile {
pub fn acquire() -> Result<Self> {
let lock_path = PathBuf::from(LOCK_FILE_NAME);
if let Some(parent) = lock_path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("ディレクトリの作成に失敗: {}", parent.display()))?;
}
if lock_path.exists() {
let existing_pid = Self::read_pid(&lock_path)?;
if Self::is_process_alive(existing_pid) {
bail!(
"既に別のcctaktインスタンスが実行中です (PID: {})\n\
同じディレクトリで複数のcctaktを起動することはできません。\n\
既存のインスタンスを終了してから再度お試しください。",
existing_pid
);
}
fs::remove_file(&lock_path)
.with_context(|| format!("古いロックファイルの削除に失敗: {}", lock_path.display()))?;
}
let current_pid = process::id();
fs::write(&lock_path, current_pid.to_string())
.with_context(|| format!("ロックファイルの作成に失敗: {}", lock_path.display()))?;
Ok(Self { path: lock_path })
}
fn read_pid(path: &Path) -> Result<u32> {
let content = fs::read_to_string(path)
.with_context(|| format!("ロックファイルの読み取りに失敗: {}", path.display()))?;
content
.trim()
.parse::<u32>()
.with_context(|| format!("ロックファイルのPIDが無効です: {}", content.trim()))
}
fn is_process_alive(pid: u32) -> bool {
#[cfg(target_os = "linux")]
{
Path::new(&format!("/proc/{}", pid)).exists()
}
#[cfg(target_os = "macos")]
{
use std::process::Command;
Command::new("kill")
.args(["-0", &pid.to_string()])
.output()
.map(|out| out.status.success())
.unwrap_or(false)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
true
}
}
pub fn release(self) {
drop(self);
}
}
impl Drop for LockFile {
fn drop(&mut self) {
if self.path.exists() {
if let Err(e) = fs::remove_file(&self.path) {
eprintln!("警告: ロックファイルの削除に失敗しました: {}", e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::env;
fn run_in_temp_dir<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let original_dir = env::current_dir().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
env::set_current_dir(&temp_dir).unwrap();
let result = f();
env::set_current_dir(&original_dir).unwrap();
result
}
#[test]
#[serial]
fn test_acquire_and_release() {
run_in_temp_dir(|| {
let lock = LockFile::acquire().expect("ロック取得に失敗");
assert!(PathBuf::from(LOCK_FILE_NAME).exists());
lock.release();
assert!(!PathBuf::from(LOCK_FILE_NAME).exists());
});
}
#[test]
#[serial]
fn test_stale_lock_cleanup() {
run_in_temp_dir(|| {
fs::create_dir_all(".cctakt").unwrap();
fs::write(LOCK_FILE_NAME, "999999999").unwrap();
let lock = LockFile::acquire().expect("古いロックがあっても取得できるはず");
let content = fs::read_to_string(LOCK_FILE_NAME).unwrap();
assert_eq!(content, process::id().to_string());
lock.release();
});
}
#[test]
fn test_is_process_alive_current() {
assert!(LockFile::is_process_alive(process::id()));
}
#[test]
fn test_is_process_alive_nonexistent() {
#[cfg(target_os = "linux")]
assert!(!LockFile::is_process_alive(999999999));
}
}