summaryrefslogtreecommitdiffstats
path: root/crates/common/flockfile/src/unix.rs
blob: 72fe1260940245c4e731bcd739d1b320c4a191b0 (plain)
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
use nix::fcntl::{flock, FlockArg};
use std::{
    fs::{self, File, OpenOptions},
    io,
    os::unix::io::AsRawFd,
    path::{Path, PathBuf},
};
use tracing::{debug, error, info, warn};

const LOCK_CHILD_DIRECTORY: &str = "lock/";

#[derive(thiserror::Error, Debug)]
pub enum FlockfileError {
    #[error("Couldn't acquire file lock.")]
    FromIo {
        path: PathBuf,
        #[source]
        source: std::io::Error,
    },

    #[error("Couldn't acquire file lock.")]
    FromNix {
        path: PathBuf,
        #[source]
        source: nix::Error,
    },
}

impl FlockfileError {
    fn path(&self) -> &Path {
        match self {
            FlockfileError::FromIo { path, .. } => path,
            FlockfileError::FromNix { path, .. } => path,
        }
    }
}

/// flockfile creates a lockfile in the filesystem under `/run/lock` and then creates a filelock using system fcntl with flock.
/// flockfile will automatically remove lockfile on application exit and the OS should cleanup the filelock afterwards.
/// If application exits unexpectedly the filelock will be dropped, but the lockfile will not be removed unless handled in signal handler.
#[derive(Debug)]
pub struct Flockfile {
    handle: Option<File>,
    pub path: PathBuf,
}

impl Flockfile {
    /// Create new lockfile in `/run/lock` with specific name:
    ///
    /// #Example
    ///
    /// let _lockfile = match flockfile::Flockfile::new_lock("app")).unwrap();
    ///
    pub fn new_lock(path: impl AsRef<Path>) -> Result<Flockfile, FlockfileError> {
        let path = PathBuf::new().join(path);
        let file = OpenOptions::new()
            .create(true)
            .read(true)
            .write(true)
            .open(&path)
            .map_err(|err| FlockfileError::FromIo {
                path: path.clone(),
                source: err,
            })?;

        flock(file.as_raw_fd(), FlockArg::LockExclusiveNonblock).map_err(|err| {
            FlockfileError::FromNix {
                path: path.clone(),
                source: err,
            }
        })?;

        info!(r#"Lockfile created {:?}"#, &path);
        Ok(Flockfile {
            handle: Some(file),
            path,
        })
    }

    /// Manually remove filelock and lockfile from the filesystem, this method doesn't have to be called explicitly,
    /// however if access to the locked file is required this must be called.
    pub fn unlock(mut self) -> Result<(), io::Error> {
        self.handle.take().expect("handle dropped");
        fs::remove_file(&self.path)?;
        Ok(())
    }
}

impl Drop for Flockfile {
    /// The Drop trait will be called always when the lock goes out of scope, however,
    /// if the program exits unexpectedly and drop is not called the lock will be removed by the system.
    fn drop(&mut self) {
        if let Some(handle) = self.handle.take() {
            drop(handle);

            // Even if the file is not removed this is not an issue, as OS will take care of the flock.
            // Additionally if the file is created before an attempt to create the lock that won't be an issue as we rely on filesystem lock.
            match fs::remove_file(&self.path) {
                Ok(()) => debug!(r#"Lockfile deleted "{:?}""#, self.path),
                Err(err) => warn!(
                    r#"Error while handling lockfile at "{:?}": {:?}"#,
                    self.path, err
                ),
            }
        }
    }
}

impl AsRef<Path> for Flockfile {
    fn as_ref(&self) -> &Path {
        self.path.as_ref()
    }
}

/// Check `run_dir`/lock/ for a lock file of a given `app_name`
pub fn check_another_instance_is_not_running(
    app_name: &str,
    run_dir: &Path,
) -> Result<Flockfile, FlockfileError> {
    let lock_path = run_dir.join(format!("{}{}.lock", LOCK_CHILD_DIRECTORY, app_name));

    Flockfile::new_lock(lock_path.as_path()).map_err(|err| {
        error!("Another instance of {} is running.", app_name);
        error!("Lock file path: {}", err.path().to_str().unwrap());
        err
    })
}

#[cfg(test)]
mod tests {

    use super::*;
    use assert_matches::*;
    use std::{fs, io};
    use tempfile::NamedTempFile;

    #[test]
    fn lock_access_remove() {
        let path = NamedTempFile::new().unwrap().into_temp_path().to_owned();
        let lockfile = Flockfile::new_lock(&path).unwrap();

        assert_eq!(lockfile.path, path);

        lockfile.unlock().unwrap();

        assert_eq!(
            fs::metadata(path).unwrap_err().kind(),
            io::ErrorKind::NotFound
        );
    }

    #[test]
    fn lock_out_of_scope() {
        let path = NamedTempFile::new().unwrap().into_temp_path().to_owned();
        {
            let _lockfile = Flockfile::new_lock(&path).unwrap();
            // assert!(path.exists());
            assert!(fs::metadata(&path).is_ok());
        }

        assert_eq!(
            fs::metadata(path).unwrap_err().kind(),
            io::ErrorKind::NotFound
        );
    }

    #[test]
    fn lock_twice() {
        let path = NamedTempFile::new().unwrap().into_temp_path().to_owned();
        let _lockfile = Flockfile::new_lock(&path).unwrap();

        assert_matches!(
            Flockfile::new_lock(&path).unwrap_err(),
            FlockfileError::FromNix { .. }
        );
    }
}