#![allow(unsafe_code)]
use crate::error::{ComposeError, Result};
use super::Engine;
#[must_use = "the project lock is released as soon as this guard is dropped"]
pub struct ProjectLock {
#[allow(dead_code)]
file: std::fs::File,
}
impl Engine {
pub fn lock_project(&self) -> Result<ProjectLock> {
if !super::staging::is_safe_project_name(&self.project) {
return Err(ComposeError::Unsupported(format!(
"unsafe project name '{}' — refusing to create a lock file",
self.project
)));
}
let base = super::staging::staging_base()?;
let path = base.join(format!("{}.lock", self.project));
let file = open_lock_file(&path)?;
acquire(&file)?;
Ok(ProjectLock { file })
}
}
#[cfg(unix)]
fn open_lock_file(path: &std::path::Path) -> Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt;
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(path)
.map_err(ComposeError::Io)
}
#[cfg(not(unix))]
fn open_lock_file(path: &std::path::Path) -> Result<std::fs::File> {
std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(false)
.open(path)
.map_err(ComposeError::Io)
}
#[cfg(unix)]
fn acquire(file: &std::fs::File) -> Result<()> {
use std::os::unix::io::AsRawFd;
let fd = file.as_raw_fd();
let rc = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if rc == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
if err.raw_os_error() == Some(libc::EWOULDBLOCK) {
tracing::info!("waiting for project lock held by another podup process...");
let rc = unsafe { libc::flock(fd, libc::LOCK_EX) };
if rc != 0 {
return Err(ComposeError::Io(std::io::Error::last_os_error()));
}
return Ok(());
}
Err(ComposeError::Io(err))
}
#[cfg(not(unix))]
fn acquire(_file: &std::fs::File) -> Result<()> {
Ok(())
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::libpod::Client;
fn engine(project: &str) -> Engine {
Engine::with_base_dir(
Client::new("/nonexistent.sock"),
project.into(),
std::env::temp_dir(),
)
}
#[test]
fn lock_acquire_release_reacquire() {
let e = engine("podup-locktest");
let first = e.lock_project().expect("first acquire");
drop(first);
let _second = e.lock_project().expect("re-acquire after release");
}
#[test]
fn lock_rejects_unsafe_project_name() {
assert!(engine("../evil").lock_project().is_err());
assert!(engine(".hidden").lock_project().is_err());
}
#[test]
fn second_holder_blocks_until_first_releases() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Barrier};
use std::thread;
let project = "podup-lock-contention";
let held = engine(project).lock_project().expect("first acquire");
let released = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&released);
let barrier = Arc::new(Barrier::new(2));
let waiter_barrier = Arc::clone(&barrier);
let waiter = thread::spawn(move || {
waiter_barrier.wait();
let _guard = engine(project).lock_project().expect("second acquire");
assert!(
flag.load(Ordering::SeqCst),
"second holder acquired the lock before the first released it"
);
});
barrier.wait();
released.store(true, Ordering::SeqCst);
drop(held);
waiter.join().expect("waiter thread panicked");
}
}