# -*- coding: utf-8 -*- # locks.py # Copyright (C) 2013 LEAP # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Utilities for handling multi-platform file locking mechanisms """ import errno import os import platform from leap.bitmask.logs.utils import get_logger from leap.bitmask.platform_init import IS_WIN, IS_UNIX from leap.common.events import emit, catalog if IS_UNIX: from fcntl import flock, LOCK_EX, LOCK_NB else: # WINDOWS import datetime import glob import shutil import time from tempfile import gettempdir from leap.bitmask.util import get_modification_ts, update_modification_ts logger = get_logger() if IS_UNIX: class UnixLock(object): """ Uses flock to get an exclusive lock over a file. See man 2 flock """ _LOCK_FILE = '/tmp/bitmask.lock' def __init__(self): """ Initialize the UnixLock. """ self._fd = None def get_lock(self): """ Tries to get a lock, and writes the running pid there if successful """ gotit, pid = self._get_lock_and_pid() return gotit def get_pid(self): """ Returns the pid of the locking process """ gotit, pid = self._get_lock_and_pid() return pid def _get_lock(self): """ Tries to get a lock, returning True if successful :rtype: bool """ self._fd = os.open(self._LOCK_FILE, os.O_CREAT | os.O_RDWR) try: flock(self._fd, LOCK_EX | LOCK_NB) except IOError as exc: # could not get the lock if exc.args[0] in (errno.EDEADLK, errno.EAGAIN): # errno 11 or 35 # Resource temporarily unavailable return False else: raise return True @property def locked_by_us(self): """ Returns True if the pid in the pidfile is ours. :rtype: bool """ gotit, pid = self._get_lock_and_pid() return pid == os.getpid() @classmethod def release_lock(self): """ Release the lock. :return: True if the lock was released, False otherwise :rtype: bool """ try: os.remove(self._LOCK_FILE) return True except Exception as e: logger.debug("Problem removing lock, {0!r}".format(e)) return False def _get_lock_and_pid(self): """ Tries to get a lock over the file. Returns (locked, pid) tuple. :rtype: tuple """ if self._get_lock(): self._write_to_pidfile() return True, None return False, self._read_from_pidfile() def _read_from_pidfile(self): """ Tries to read pid from the pidfile, returns False if no content found. """ pidfile = os.read(self._fd, 16) if not pidfile: return False try: return int(pidfile.strip()) except Exception as exc: exc.args += (pidfile, self.lock_file) raise def _write_to_pidfile(self): """ Writes the pid of the running process to the pidfile """ fd = self._fd os.ftruncate(fd, 0) os.write(fd, '%d\n' % os.getpid()) os.fsync(fd) if IS_WIN: # Time to wait (in secs) before assuming a raise window signal has not been # ack-ed. RAISE_WINDOW_TIMEOUT = 2 # How many steps to do while checking lockfile ts update. RAISE_WINDOW_WAIT_STEPS = 10 def _release_lock(name): """ Tries to remove a folder path. :param name: folder lock to remove :type name: str """ try: shutil.rmtree(name) return True except WindowsError as exc: if exc.errno in (errno.EPIPE, errno.ENOENT, errno.ESRCH, errno.EACCES): logger.warning( 'exception while trying to remove the lockfile dir') logger.warning('errno %s: %s' % (exc.errno, exc.args[1])) # path does not exist return False else: logger.debug('errno = %s' % (exc.errno,)) # we did not foresee this error, better add it explicitely raise class WindowsLock(object): """ Creates a lock based on the atomic nature of mkdir on Windows system calls. """ LOCKBASE = os.path.join(gettempdir(), "bitmask-lock") def __init__(self): """ Initializes the lock. Sets the lock name to basename plus the process pid. """ self._fd = None pid = os.getpid() self.name = "%s-%s" % (self.LOCKBASE, pid) self.pid = pid def get_lock(self): """ Tries to get a lock, and writes the running pid there if successful """ gotit = self._get_lock() return gotit def _get_lock(self): """ Tries to write to a file with the current pid as part of the name """ try: self._fd = os.makedirs(self.name) except OSError as exc: # could not create the dir if exc.args[0] == 183: logger.debug('cannot create dir') # cannot create dir with existing name return False else: raise return self._is_one_pidfile()[0] def _is_one_pidfile(self): """ Returns True, pid if there is only one pidfile with the expected base path :rtype: tuple """ pidfiles = glob.glob(self.LOCKBASE + '-*') if len(pidfiles) == 1: pid = pidfiles[0].split('-')[-1] return True, int(pid) else: return False, None def get_pid(self): """ Returns the pid of the locking process. :rtype: int """ # XXX assert there is only one? _, pid = self._is_one_pidfile() return pid def get_locking_path(self): """ Returns the pid path of the locking process. :rtype: str """ pid = self.get_pid() if pid: return "%s-%s" % (self.LOCKBASE, pid) def release_lock(self, name=None): """ Releases the pidfile dir for this process, by removing it. """ if not name: name = self.name _release_lock(name) @classmethod def release_all_locks(self): """ Releases all locks. Used for clean shutdown. """ for lockdir in glob.glob("%s-%s" % (self.LOCKBASE, '*')): _release_lock(lockdir) @property def locked_by_us(self): """ Returns True if the pid in the pidfile is ours. :rtype: bool """ _, pid = self._is_one_pidfile() return pid == self.pid def update_ts(self): """ Updates the timestamp of the lock. """ if self.locked_by_us: update_modification_ts(self.name) def write_port(self, port): """ Writes the port for windows control to the pidfile folder Returns True if successful. :rtype: bool """ if not self.locked_by_us: logger.warning("Tried to write control port to a " "non-unique pidfile folder") return False port_file = os.path.join(self.name, "port") with open(port_file, 'w') as f: f.write("%s" % port) return True def get_control_port(self): """ Reads control port of the main instance from the port file in the pidfile dir :rtype: int """ pid = self.get_pid() port_file = os.path.join(self.LOCKBASE + "-%s" % pid, "port") port = None try: with open(port_file) as f: port_str = f.read() port = int(port_str.strip()) except IOError as exc: if exc.errno == errno.ENOENT: logger.error("Tried to read port from non-existent file") else: # we did not know explicitely about this error raise return port def raise_window_ack(): """ This function is called from the windows callback that is registered with the raise_window event. It just updates the modification time of the lock file so we can signal an ack to the instance that tried to raise the window. """ lock = WindowsLock() lock.update_ts() def we_are_the_one_and_only(): """ Returns True if we are the only instance running, False otherwise. If we came later, send a raise signal to the main instance of the application. Under windows we are not using flock magic, so we wait during RAISE_WINDOW_TIMEOUT time, if not ack is received, we assume it was a stalled lock, so we remove it and continue with initialization. :rtype: bool """ if IS_UNIX: locker = UnixLock() locker.get_lock() we_are_the_one = locker.locked_by_us if not we_are_the_one: emit(catalog.RAISE_WINDOW) return we_are_the_one elif IS_WIN: locker = WindowsLock() locker.get_lock() we_are_the_one = locker.locked_by_us if not we_are_the_one: locker.release_lock() lock_path = locker.get_locking_path() ts = get_modification_ts(lock_path) nowfun = datetime.datetime.now t0 = nowfun() pause = RAISE_WINDOW_TIMEOUT / float(RAISE_WINDOW_WAIT_STEPS) timeout_delta = datetime.timedelta(0, RAISE_WINDOW_TIMEOUT) check_interval = lambda: nowfun() - t0 < timeout_delta # let's assume it's a stalled lock we_are_the_one = True emit(catalog.RAISE_WINDOW) while check_interval(): if get_modification_ts(lock_path) > ts: # yay! someone claimed their control over the lock. # so the lock is alive logger.debug('Raise window ACK-ed') we_are_the_one = False break else: time.sleep(pause) if we_are_the_one: # ok, it really was a stalled lock. let's remove all # that is left, and put only ours there. WindowsLock.release_all_locks() WindowsLock().get_lock() return we_are_the_one else: logger.warning("Multi-instance checker " "not implemented for %s" % (platform.system())) # lies, lies, lies... return True def release_lock(): """ Release the acquired lock. """ if IS_WIN: WindowsLock.release_all_locks() elif IS_UNIX: UnixLock.release_lock()