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);
        }
    }
}