mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-10-05 16:37:44 +03:00
add PID lockfile crate
This commit is contained in:
parent
786d63e601
commit
6968e99120
78
Cargo.lock
generated
78
Cargo.lock
generated
@ -1993,6 +1993,16 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gitbutler-pidlock"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"polonius-the-crab",
|
||||
"sysinfo",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glib"
|
||||
version = "0.15.12"
|
||||
@ -2250,6 +2260,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "higher-kinded-types"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "561985554c8b8d4808605c90a5f1979cc6c31a5d20b78465cd59501233c6678e"
|
||||
dependencies = [
|
||||
"never-say-never",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hmac"
|
||||
version = "0.12.1"
|
||||
@ -3020,6 +3039,12 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "never-say-never"
|
||||
version = "6.6.666"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf5a574dadd7941adeaa71823ecba5e28331b8313fb2e1c6a5c7e5981ea53ad6"
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.4"
|
||||
@ -3100,6 +3125,15 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
@ -3780,6 +3814,16 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polonius-the-crab"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d9b0ba45e2c294fcd0795bc51e45c12746758e9954bb1190f97603ec9a3e91"
|
||||
dependencies = [
|
||||
"higher-kinded-types",
|
||||
"never-say-never",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.6.0"
|
||||
@ -5225,6 +5269,21 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
@ -6475,6 +6534,16 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-bindgen"
|
||||
version = "0.39.0"
|
||||
@ -6485,6 +6554,15 @@ dependencies = [
|
||||
"windows-tokens",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.39.0"
|
||||
|
@ -2,6 +2,7 @@
|
||||
members = [
|
||||
"gitbutler-app",
|
||||
"gitbutler-changeset",
|
||||
"gitbutler-pidlock",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
@ -45,10 +45,10 @@ refinery = { version = "0.8", features = [ "rusqlite" ] }
|
||||
regex = "1.10"
|
||||
reqwest = "0.11.24"
|
||||
resolve-path = "0.1.0"
|
||||
rusqlite.workspace = true
|
||||
rusqlite = { workspace = true }
|
||||
sentry = { version = "0.32", optional = true, features = ["backtrace", "contexts", "panic", "transport", "anyhow", "debug-images", "reqwest", "native-tls" ] }
|
||||
sentry-tracing = "0.32.0"
|
||||
serde.workspace = true
|
||||
serde = { workspace = true }
|
||||
serde_json = { version = "1.0", features = [ "std", "arbitrary_precision" ] }
|
||||
sha1 = "0.10.6"
|
||||
sha2 = "0.10.8"
|
||||
@ -71,7 +71,7 @@ tracing-appender = "0.2.3"
|
||||
tracing-subscriber = "0.3.17"
|
||||
url = "2.5"
|
||||
urlencoding = "2.1.3"
|
||||
uuid.workspace = true
|
||||
uuid = { workspace = true }
|
||||
walkdir = "2.3.2"
|
||||
zip = "0.6.5"
|
||||
|
||||
|
12
gitbutler-pidlock/Cargo.toml
Normal file
12
gitbutler-pidlock/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "gitbutler-pidlock"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
polonius-the-crab = "0.4.1"
|
||||
sysinfo = "0.30.7"
|
||||
thiserror.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.10.1"
|
284
gitbutler-pidlock/src/lib.rs
Normal file
284
gitbutler-pidlock/src/lib.rs
Normal file
@ -0,0 +1,284 @@
|
||||
//! # gitbutler-pidlock
|
||||
//! Provides a PID-based lockfile for use where a task must ensure
|
||||
//! it's the only operation working on a resource, typically a filesystem
|
||||
//! resource.
|
||||
//!
|
||||
//! The PID file first checks if the PID file exists, pulls the PID out,
|
||||
//! and checks if the process is still running. If all of those checks pass,
|
||||
//! a lock blocks creation until any of those things is no longer true.
|
||||
//!
|
||||
//! It then (over)writes the PID file with its own PID, returns a lock,
|
||||
//! and when the lock is dropped, it removes the PID file.
|
||||
//!
|
||||
//! Note that the above checks are not atomic; there *is* a small chance for
|
||||
//! a race condition. Please keep this in mind.
|
||||
#![deny(missing_docs)]
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Mutex,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use ::polonius_the_crab::prelude::*;
|
||||
use sysinfo::{Pid, ProcessRefreshKind, ProcessStatus};
|
||||
|
||||
/// The error type returned by the lockfile types.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The lockfile failed to get the current process's PID.
|
||||
#[error("failed to get current process ID: {0}")]
|
||||
GetPid(String),
|
||||
/// The lockfile is being accessed by something else within
|
||||
/// this process.
|
||||
#[error("concurrent access to lockfile (same process)")]
|
||||
ConcurrentAccess,
|
||||
/// I/O error (generic, from [`std::io::Error`]).
|
||||
#[error("I/O error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
/// Another process has locked the resource.
|
||||
#[error("resource is locked by another process: {0}")]
|
||||
ProcessLocked(u32),
|
||||
}
|
||||
|
||||
/// Implements a PID lockfile to be used as a filesystem-based lock for a
|
||||
/// resource, typically a filesystem resource.
|
||||
///
|
||||
/// See this crate's main documentation for information about the implementation.
|
||||
pub struct PidLock {
|
||||
filepath: PathBuf,
|
||||
our_pid: Pid,
|
||||
poll_rate: Duration,
|
||||
access_mutex: Mutex<()>,
|
||||
}
|
||||
|
||||
impl PidLock {
|
||||
/// Creates a new `PidLock` instance given a filepath at which the PID file
|
||||
/// should be stored.
|
||||
#[cold]
|
||||
pub fn new<P: AsRef<Path>>(filepath: P, poll_rate: Duration) -> Result<Self, Error> {
|
||||
let our_pid = sysinfo::get_current_pid().map_err(|s| Error::GetPid(s.to_string()))?;
|
||||
|
||||
Ok(PidLock {
|
||||
filepath: filepath.as_ref().to_owned(),
|
||||
our_pid,
|
||||
access_mutex: Mutex::new(()),
|
||||
poll_rate,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the path at which this PID lockfile is stored.
|
||||
#[inline]
|
||||
#[cold]
|
||||
pub fn filepath(&self) -> &Path {
|
||||
&self.filepath
|
||||
}
|
||||
|
||||
/// Attempts to acquire the lock, blocking until it can be acquired.
|
||||
pub fn try_lock(&mut self) -> Result<PidLockGuard, Error> {
|
||||
{
|
||||
let _access_lock = self
|
||||
.access_mutex
|
||||
.try_lock()
|
||||
.map_err(|_| Error::ConcurrentAccess)?;
|
||||
|
||||
// Attempt to read the PID file.
|
||||
let locked_pid = match fs::read_to_string(&self.filepath) {
|
||||
Ok(s) => s.trim().parse::<Pid>().ok(),
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
None
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// If the PID file exists, check if the process is still running.
|
||||
if let Some(locked_pid) = locked_pid {
|
||||
let mut sys = sysinfo::System::new();
|
||||
// We refresh with `::new()` here because we don't care about getting
|
||||
// any of the information about the process other than its status.
|
||||
sys.refresh_pids_specifics(&[locked_pid], ProcessRefreshKind::new());
|
||||
if sys
|
||||
.process(locked_pid)
|
||||
.map(|p| p.status() != ProcessStatus::Zombie)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(Error::ProcessLocked(locked_pid.as_u32()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, write our PID and return a lock.
|
||||
fs::write(&self.filepath, self.our_pid.to_string())?;
|
||||
Ok(PidLockGuard { lock: self })
|
||||
}
|
||||
|
||||
/// Acquires the lock, blocking until it can be acquired.
|
||||
pub fn lock<'a>(&'a mut self) -> Result<PidLockGuard<'a>, Error> {
|
||||
let poll_rate = self.poll_rate;
|
||||
let mut this = self;
|
||||
loop {
|
||||
polonius!(|this| -> Result<PidLockGuard<'polonius>, Error> {
|
||||
match this.try_lock() {
|
||||
Ok(guard) => polonius_return!(Ok(guard)),
|
||||
Err(Error::ProcessLocked(_) | Error::ConcurrentAccess) => {
|
||||
std::thread::sleep(poll_rate)
|
||||
}
|
||||
Err(e) => polonius_return!(Err(e)),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A guard returned by [`PidLock::try_lock`] and [`PidLock::lock`], which removes the PID file
|
||||
/// when dropped.
|
||||
///
|
||||
/// **NOTE:** The removal is not guaranteed to work. If for some reason the removal fails, it will
|
||||
/// be silently ignored.
|
||||
pub struct PidLockGuard<'a> {
|
||||
lock: &'a mut PidLock,
|
||||
}
|
||||
|
||||
impl Drop for PidLockGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_file(&self.lock.filepath);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_ok_lock_drop() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let mut lock = PidLock::new(
|
||||
td.path().join("test-ok-lock-drop.pid"),
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.unwrap();
|
||||
{
|
||||
let _guard = lock.try_lock().unwrap();
|
||||
}
|
||||
{
|
||||
let _guard = lock.try_lock().unwrap();
|
||||
}
|
||||
{
|
||||
let _guard = lock.try_lock().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_err_process_lock() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let path = td.path().join("test-err-process-lock.pid");
|
||||
let mut lock = PidLock::new(&path, Duration::from_millis(100)).unwrap();
|
||||
let _guard = lock.try_lock().unwrap();
|
||||
let mut lock2 = PidLock::new(&path, Duration::from_millis(100)).unwrap();
|
||||
let guard2 = lock2.try_lock();
|
||||
match guard2 {
|
||||
Ok(_) => panic!("expected process locked error, got lock"),
|
||||
Err(Error::ProcessLocked(pid)) => {
|
||||
assert_eq!(pid, sysinfo::get_current_pid().unwrap().as_u32())
|
||||
}
|
||||
Err(e) => panic!("expected process locked error, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_err_process_locked_different_pid() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let path = td.path().join("test-err-lock-process-locked.pid");
|
||||
let mut lock = PidLock::new(&path, Duration::from_millis(50)).unwrap();
|
||||
let _guard = lock.try_lock().unwrap();
|
||||
|
||||
// Write a fake PID to the file. In our case, we create a zombie process
|
||||
// that is suspended upon boot (either forcefully, via a loop, or by the kernel).
|
||||
let mut command = {
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
// On Unix, we do a sleep loop before exec is invoked.
|
||||
use std::{os::unix::process::CommandExt, process::Command};
|
||||
Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(":")
|
||||
.pre_exec(|| {
|
||||
std::thread::sleep(Duration::from_millis(1000));
|
||||
Ok(())
|
||||
})
|
||||
.spawn()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// On Windows, we start the program suspended and never resume it.
|
||||
use std::{os::windows::process::CommandExt, process::Command};
|
||||
Command::new("echo")
|
||||
.creation_flags(0x00000004) // CREATE_SUSPENDED
|
||||
.spawn()
|
||||
.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let fake_pid = command.id();
|
||||
::std::fs::write(&path, fake_pid.to_string()).unwrap();
|
||||
|
||||
let mut lock2 = PidLock::new(&path, Duration::from_millis(1000)).unwrap();
|
||||
let guard2 = lock2.try_lock();
|
||||
|
||||
command.kill().unwrap();
|
||||
|
||||
match guard2 {
|
||||
Ok(_) => panic!("expected process locked error, got lock"),
|
||||
Err(Error::ProcessLocked(pid)) => assert_eq!(pid, fake_pid),
|
||||
Err(e) => panic!("expected process locked error, got {:?}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_err_process_lock_wait() {
|
||||
let joined = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
|
||||
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let path = td.path().join("test-err-process-lock-wait.pid");
|
||||
let mut lock = PidLock::new(&path, Duration::from_millis(100)).unwrap();
|
||||
let guard = lock.try_lock().unwrap();
|
||||
|
||||
let other_thread = std::thread::spawn({
|
||||
let joined = joined.clone();
|
||||
move || {
|
||||
let mut lock2 = PidLock::new(&path, Duration::from_millis(10)).unwrap();
|
||||
let guard2 = lock2.lock().unwrap();
|
||||
joined.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
drop(guard2);
|
||||
}
|
||||
});
|
||||
|
||||
// Sleep for a while and then drop the guard.
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
assert!(!joined.load(std::sync::atomic::Ordering::SeqCst));
|
||||
drop(guard);
|
||||
|
||||
other_thread.join().unwrap();
|
||||
assert!(joined.load(std::sync::atomic::Ordering::SeqCst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ensure_remove_lock_file_on_guard_drop() {
|
||||
let td = tempfile::tempdir().unwrap();
|
||||
let path = td
|
||||
.path()
|
||||
.join("test-ensure-remove-lock-file-on-guard-drop.pid");
|
||||
let mut lock = PidLock::new(&path, Duration::from_millis(100)).unwrap();
|
||||
let guard = lock.try_lock().unwrap();
|
||||
assert!(path.exists());
|
||||
drop(guard);
|
||||
assert!(!path.exists());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user