simplelock 0.4.1

Simple abstractions for inter-process synchronization.
Documentation
/// Library for interacting with the following syscalls in a safe way:
///  https://linux.die.net/man/7/sem_overview
///  https://linux.die.net/man/3/sem_close
///  https://linux.die.net/man/3/sem_open
///  https://linux.die.net/man/3/sem_push
///  https://linux.die.net/man/3/sem_trywait
///  https://linux.die.net/man/3/sem_wait
///
use crate::*;

// TODO: Fix deps.
//  - Either get nix updated with libc calls below.
//  - Or find a way to nix nix.
use nix::{fcntl::OFlag, sys::stat::Mode};

/// A safe publicly usable object surrounding the
/// wrapper we built since 'nix' doesn't have it yet.
pub struct SemaphoreLock {
    /// The POSIX Named Semaphore.
    sem: safe_wrapper::NamedSem,

    /// Local consistency check, will be true if we have successfully
    /// completed the lock operation, will be false otherwise or on
    /// successful completion of the unlock operation.
    have_lock: bool,
}

const INITIAL_VALUE: i32 = 1;
const FLAGS: OFlag = OFlag::O_CREAT;

/// The naming scheme for semaphore names, per POSIX sem_overview:
///
///   A named semaphore is identified by a name of the form /somename;
///   that is, a null-terminated string of up to NAME_MAX-4 (i.e., 251)
///   characters consisting of an initial slash, followed by one or more
///   characters, none of which are slashes.
///
/// We expect either 'somename' or '/somename' and we'll append the slash.
fn verify_name(name: impl Into<String>) -> SimpleLockResult<String> {
    let mut test = name.into();
    if !test.starts_with("/") {
        test.insert_str(0, "/");
    }

    if test.len() < 2 || test.len() > 250 || test.split('/').count() > 2 {
        Err(SimpleLockError::InvalidName)
    } else {
        Ok(test)
    }
}

impl SemaphoreLock {
    /// Create a new lock with the given semaphore name. Must meet POSIX requirements.
    pub fn new(name: impl Into<String>) -> SimpleLockResult<SemaphoreLock> {
        let name = verify_name(name)?;
        let mode = Mode::S_IRWXU | Mode::S_IRWXO;
        let sem = safe_wrapper::sem_open(&name, &FLAGS, mode, INITIAL_VALUE)?;
        Ok(SemaphoreLock {
            sem,
            have_lock: false,
        })
    }

    /// Get a copy of the name used in creating this Semaphore.
    pub fn name(&self) -> String {
        self.sem.name()
    }
}

impl ConcreteLock for SemaphoreLock {
    fn status(&self) -> SimpleLockResult<LockStatus> {
        if self.have_lock {
            Ok(LockStatus::Mine)
        } else if safe_wrapper::sem_getvalue(&self.sem)? != INITIAL_VALUE {
            Ok(LockStatus::Taken)
        } else {
            Ok(LockStatus::Open)
        }
    }

    fn try_lock(&mut self) -> SimpleLockResult<()> {
        if self.have_lock {
            return Ok(());
        }
        safe_wrapper::sem_trywait(&self.sem)
            .map_err(|e| e.into())
            .and_then(|_| {
                self.have_lock = true;
                Ok(())
            })
    }

    fn hang_lock(&mut self) -> SimpleLockResult<()> {
        if self.have_lock {
            return Ok(());
        }
        safe_wrapper::sem_wait(&self.sem)
            .map_err(|e| e.into())
            .and_then(|_| {
                self.have_lock = true;
                Ok(())
            })
    }

    fn try_unlock(&mut self) -> SimpleLockResult<()> {
        if !self.have_lock {
            return Ok(());
        }
        safe_wrapper::sem_post(&self.sem)
            .map_err(|e| e.into())
            .and_then(|_| {
                self.have_lock = false;
                Ok(())
            })
    }
}

// FIXME: Once all the semaphore stuff is in `nix' then this can be removed.
mod safe_wrapper {
    use crate::SimpleLockError;
    use nix::{errno::Errno, fcntl::OFlag, sys::stat::Mode, Error, NixPath};

    impl From<Error> for SimpleLockError {
        fn from(e: Error) -> Self {
            match e {
                // We can make the assumption that its an invalid name issue since the
                // only time we use strings/paths/utf8 here is with the name creation.
                Error::InvalidPath => SimpleLockError::InvalidName,
                Error::InvalidUtf8 => SimpleLockError::InvalidName,

                // This is brute-force, and drops a lot of potentially helpful context.
                Error::Sys(errno) => errno.into(),

                // This should really not ever happen since we implement everything here
                // and we don't do the proper error handling for "other Operating Systems".
                Error::UnsupportedOperation => {
                    SimpleLockError::UnknownError("nix::UnsupportedOperation".into())
                }
            }
        }
    }

    impl From<Errno> for SimpleLockError {
        fn from(e: Errno) -> Self {
            match e {
                // sem_open - The semaphore exists, but the caller does not have permission to open it.
                Errno::EACCES => SimpleLockError::PermissionDenied,

                // sem_open - The per-process limit on the number of open file descriptors has been reached.
                Errno::EMFILE => SimpleLockError::PermissionDenied,
                // sem_open - The system-wide limit on the total number of open files has been reached.
                Errno::ENFILE => SimpleLockError::PermissionDenied,
                // sem_open - Insufficient memory.
                Errno::ENOMEM => SimpleLockError::PermissionDenied,

                // sem_open - name consists of just "/", followed by no other characters.
                // sem_post - sem is not a valid semaphore.
                // sem_getvalue - sem is not a valid semaphore.
                Errno::EINVAL => SimpleLockError::InvalidName,

                // sem_open - name was too long.
                Errno::ENAMETOOLONG => SimpleLockError::InvalidName,

                // sem_wait - The call was interrupted by a signal handler; see signal(7).
                Errno::EINTR => SimpleLockError::UnknownError("Signal interrupt.".into()),

                // Bad fail-through...
                _ => SimpleLockError::UnknownError(format!("{:?}: {}", e, e.desc())),
            }
        }
    }

    /// Fake object to wrap what libc has. Would hope that in the future nix has this.
    pub struct NamedSem {
        name: String,
        sem: *mut libc::sem_t,
    }
    impl Drop for NamedSem {
        fn drop(&mut self) {
            // Close this process's use of the semaphore.
            let _ = sem_post(self);
            let _ = sem_close(self);
        }
    }
    impl NamedSem {
        pub fn name(&self) -> String {
            self.name.clone()
        }
    }

    pub fn sem_getvalue(sem: &NamedSem) -> nix::Result<i32> {
        let mut res: i32 = 0;
        if unsafe { libc::sem_getvalue(sem.sem, &mut res) == libc::EXIT_SUCCESS } {
            Ok(res)
        } else {
            Err(nix::Error::Sys(Errno::last()).into())
        }
    }

    pub fn sem_open(name: &String, oflag: &OFlag, mode: Mode, value: i32) -> nix::Result<NamedSem> {
        name.as_bytes().with_nix_path(|cstr| unsafe {
            let sem: *mut libc::sem_t = libc::sem_open(
                cstr.as_ptr(),
                oflag.bits(),
                mode.bits() as libc::mode_t,
                value,
            );
            if sem == libc::SEM_FAILED {
                Err(nix::Error::Sys(Errno::last()))
            } else {
                Ok(NamedSem {
                    name: name.clone(),
                    sem,
                })
            }
        })?
    }

    pub fn sem_wait(sem: &NamedSem) -> nix::Result<()> {
        if unsafe { libc::sem_wait(sem.sem) == libc::EXIT_SUCCESS } {
            Ok(())
        } else {
            Err(nix::Error::Sys(Errno::last()).into())
        }
    }

    pub fn sem_trywait(sem: &NamedSem) -> nix::Result<()> {
        if unsafe { libc::sem_trywait(sem.sem) == libc::EXIT_SUCCESS } {
            Ok(())
        } else {
            Err(nix::Error::Sys(Errno::last()).into())
        }
    }

    pub fn sem_post(sem: &NamedSem) -> nix::Result<()> {
        if unsafe { libc::sem_post(sem.sem) == libc::EXIT_SUCCESS } {
            Ok(())
        } else {
            Err(nix::Error::Sys(Errno::last()).into())
        }
    }

    pub fn sem_close(sem: &mut NamedSem) -> nix::Result<()> {
        if unsafe { libc::sem_close(sem.sem) == libc::EXIT_SUCCESS } {
            Ok(())
        } else {
            Err(nix::Error::Sys(Errno::last()).into())
        }
    }
}

#[cfg(test)]
mod _test {
    use super::*;

    #[test]
    fn good_names() {
        // Pretty much any character except slashes:
        assert_eq!(true, verify_name("abc").is_ok(), "lowercase");
        assert_eq!(true, verify_name("123").is_ok(), "numbers");
        assert_eq!(true, verify_name("ABC").is_ok(), "capitals");
        assert_eq!(true, verify_name("a-c").is_ok(), "dash");
        assert_eq!(true, verify_name("a_c").is_ok(), "underscore");

        // Can begin with a slash though.
        assert_eq!(true, verify_name("/ab").is_ok(), "prefix-slash");
    }

    #[test]
    fn bad_names() {
        assert_eq!(false, verify_name("").is_ok(), "too short");
        assert_eq!(false, verify_name("a".repeat(251)).is_ok(), "too long");
        assert_eq!(false, verify_name("a/").is_ok(), "slash within name");
    }
}