1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
/* Copyright (C) 2023 Purism SPC
* Copyright (C) 2024 DorotaC
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
/*! Locking files */
use std::borrow::Borrow;
use std::ffi::c_int;
use std::fs::File;
use std::io;
use std::ops::Deref;
use std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd};
use std::fs::Metadata;
use std::os::unix::fs::MetadataExt;
use std::sync::Mutex;
pub trait FileLike {
fn get_metadata(&self) -> io::Result<Metadata>;
fn as_raw_fd(&self) -> RawFd;
}
impl FileLike for File {
fn get_metadata(&self) -> io::Result<Metadata> {
self.metadata()
}
fn as_raw_fd(&self) -> RawFd {
<Self as AsRawFd>::as_raw_fd(&self)
}
}
impl FileLike for v4l::Device {
fn get_metadata(&self) -> io::Result<Metadata> {
let fd = self.as_raw_fd();
// An extra reference to the same file gets picked up
let file = unsafe { File::from_raw_fd(fd) };
let metadata = file.metadata();
let _ = file.into_raw_fd(); // drop the reference without closing the file
metadata
}
fn as_raw_fd(&self) -> RawFd {
self.handle().fd()
}
}
mod c {
use super::c_int;
use nix::libc::off_t;
pub enum LockfCmd {
Ulock = 0,
Lock = 1,
Tlock = 3,
}
extern "C" {
pub fn lockf(fd: c_int, cmd: c_int, len: off_t) -> c_int;
}
}
type Inode = u64;
/// According to <https://gavv.net/articles/file-locks/#lockf-function>, lockf is stored in the kernel as [i-node, pid] pairs, allowing the process to access the device freely.
/// Thus, the kernel alone will not prevent another piece of code in the same process from acquiring the same resource. For instance, another library, or another linked copy of libcamera.
/// But we need each instance to be separate, so let's keep a list of open instances here, and reject those used already.
static LOCKF_LOCKS: Mutex<Vec<Inode>> = Mutex::new(Vec::new());
/// Locks a file securely (requires `lockf`)
/// Not reentrant on Linux. That is, even the same thread cannot acquire the same *file* twice.
/// **BUT** if there are two copies of this software in the same process, those copies can both lock the same file. This can happen if two different versions of the library are used, for example.
pub struct Lock<'a, T: FileLike>(&'a T);
/// Should acquiring the lock block the caller?
enum LockBlock {
/// Wait until the lock is acquired
Wait = c::LockfCmd::Lock as _,
/// Return immediately either way
Test = c::LockfCmd::Tlock as _,
}
pub struct NotAvailable;
impl<'a, T: FileLike> Lock<'a, T> {
fn new_(file: &'a T, mode: LockBlock) -> Result<Self, NotAvailable> {
let inode = file.get_metadata().unwrap().ino();
let mut locked = LOCKF_LOCKS.lock().unwrap();
if locked.iter().find(|ino| **ino == inode).is_some() {
Err(NotAvailable)
} else {
let res = unsafe {
c::lockf(file.as_raw_fd(), mode as c_int, 0)
};
if res == 0 {
locked.push(inode);
Ok(Lock(file))
} else {
Err(NotAvailable)
}
}
}
pub fn new(file: &'a T) -> Result<Self, NotAvailable> {
Self::new_(file, LockBlock::Test)
}
/// Waits only if another process is holding the lock.
// maybe TODO: wait for other threads too.
pub fn new_wait(file: &'a T) -> Result<Self, NotAvailable> {
Self::new_(file, LockBlock::Wait)
}
}
impl<'a, T: FileLike> crate::pipelines::Lock for Lock<'a, T> {}
impl<'a, T: FileLike> Drop for Lock<'a, T> {
fn drop(&mut self) {
let inode = self.0.get_metadata().unwrap().ino();
let mut locked = LOCKF_LOCKS.lock().unwrap();
let res = unsafe {
c::lockf(self.0.as_raw_fd(), c::LockfCmd::Ulock as c_int, 0)
};
if res == 0 {
locked.iter().position(|ino| *ino == inode)
.map(|i| locked.remove(i));
}
}
}
use std::mem::ManuallyDrop;
/// Locks a file securely (requires `lockf`)
///
/// This version takes ownership of the protected resource.
///
/// Not reentrant on Linux. That is, even the same thread cannot acquire the same *file* twice.
/// **BUT** if there are two copies of this software in the same process, those copies can both lock the same file. This can happen if two different versions of the library are used, for example.
pub struct Locked<T: FileLike>(ManuallyDrop<T>);
impl<T: FileLike> Deref for Locked<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.0.borrow()
}
}
impl<T: FileLike> Locked<T> {
pub fn new(file: T) -> Result<Self, T> {
let inode = file.get_metadata().unwrap().ino();
let mut locked = LOCKF_LOCKS.lock().unwrap();
if locked.iter().find(|ino| **ino == inode).is_some() {
Err(file)
} else {
let res = unsafe {
c::lockf(file.as_raw_fd(), c::LockfCmd::Tlock as c_int, 0)
};
if res == 0 {
locked.push(inode);
Ok(Locked(ManuallyDrop::new(file)))
} else {
Err(file)
}
}
}
// unlocking this way allows other threads to lock while this object exists
unsafe fn _unlock(&self) {
let inode = self.0.get_metadata().unwrap().ino();
let mut locked = LOCKF_LOCKS.lock().unwrap();
let res = unsafe {
c::lockf(self.0.as_raw_fd(), c::LockfCmd::Ulock as c_int, 0)
};
if res == 0 {
locked.iter().position(|ino| *ino == inode)
.map(|i| locked.remove(i));
}
}
/// Unlocks the file
pub fn unlock(mut self) -> T {
unsafe {
self._unlock();
ManuallyDrop::take(&mut self.0)
}
}
}
impl<T: FileLike> Drop for Locked<T> {
fn drop(&mut self) {
unsafe {
self._unlock();
ManuallyDrop::drop(&mut self.0);
}
}
}