From: Frank Brehm Date: Fri, 9 Mar 2018 14:06:25 +0000 (+0100) Subject: Fatei lib/webhooks/lock_handler.py fuer LockObject und LockHandler dazu X-Git-Tag: 0.8.4^2~12 X-Git-Url: https://git.uhu-banane.de/?a=commitdiff_plain;h=8ee59a636520359e11c05367954a9257b5d0f9a5;p=pixelpark%2Fpuppetmaster-webhooks.git Fatei lib/webhooks/lock_handler.py fuer LockObject und LockHandler dazu --- diff --git a/lib/webhooks/lock_handler.py b/lib/webhooks/lock_handler.py new file mode 100644 index 0000000..04b27d7 --- /dev/null +++ b/lib/webhooks/lock_handler.py @@ -0,0 +1,1140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +@author: Frank Brehm +@contact: frank.brehm@pixelpark.com +@copyright: © 2018 by Frank Brehm, Publicies Pixelpark GmbH, Berlin +@summary: Module for a extended handler module, which has additional + methods for locking +""" +from __future__ import absolute_import + +# Standard modules +import sys +import os +import logging +import time +import errno +import traceback +import datetime + +from numbers import Number + +# Third party modules +from six import reraise + +# Own modules +from .common import to_utf8 + +from .errors import CouldntOccupyLockfileError + +from .obj import BaseObject + +from .handler import BaseHandlerError, BaseHandler + +__version__ = '0.1.1' + +log = logging.getLogger(__name__) + +# Module variables +DEFAULT_LOCKRETRY_DELAY_START = 0.1 +DEFAULT_LOCKRETRY_DELAY_INCREASE = 0.2 +DEFAULT_LOCKRETRY_MAX_DELAY = 10 +DEFAULT_MAX_LOCKFILE_AGE = 300 +DEFAULT_LOCKING_USE_PID = True + + +# ============================================================================= +class LockHandlerError(BaseHandlerError): + """ + Base exception class for all exceptions belonging to locking issues + in this module + """ + + pass + + +# ============================================================================= +class LockObjectError(LockHandlerError): + """ + Special exception class for exceptions raising inside methods of + the LockObject. + """ + + pass + + +# ============================================================================= +class LockdirNotExistsError(LockHandlerError): + """ + Exception class for the case, that the parent directory of the lockfile + (lockdir) doesn't exists. + """ + + # ------------------------------------------------------------------------- + def __init__(self, lockdir): + """ + Constructor. + + @param lockdir: the directory, wich doesn't exists. + @type lockdir: str + + """ + + self.lockdir = lockdir + + # ------------------------------------------------------------------------- + def __str__(self): + """Typecasting into a string for error output.""" + + return "Locking directory {!r} doesn't exists or is not a directory.".format(self.lockdir) + + +# ============================================================================= +class LockdirNotWriteableError(LockHandlerError): + """ + Exception class for the case, that the parent directory of the lockfile + (lockdir) isn't writeable for the current process. + """ + + # ------------------------------------------------------------------------- + def __init__(self, lockdir): + """ + Constructor. + + @param lockdir: the directory, wich isn't writeable + @type lockdir: str + + """ + + self.lockdir = lockdir + + # ------------------------------------------------------------------------- + def __str__(self): + """Typecasting into a string for error output.""" + + return "Locking directory {!r} isn't writeable.".format(self.lockdir) + + +# ============================================================================= +class LockObject(BaseObject): + """ + Capsulation class as a result of a successful lock action. It contains all + important informations about the lock. + + It can be used for holding these informations and, if desired, to remove + the lock automatically, if the current instance of LockObject is removed. + + """ + + # ------------------------------------------------------------------------- + def __init__( + self, lockfile, ctime=None, mtime=None, fcontent=None, simulate=False, + autoremove=False, appname=None, verbose=0, version=__version__, + base_dir=None, silent=False): + """ + Initialisation of the LockObject object. + + @raise LockObjectError: on a uncoverable error. + + @param lockfile: the file, which represents the lock, must exists + @type lockfile: str + @param ctime: the creation time of the lockfile + @type ctime: datetime + @param mtime: the modification time of the lockfile + @type mtime: datetime + @param fcontent: the content of the lockfile + @type fcontent: str + @param simulate: don't execute actions, only display them + @type simulate: bool + @param autoremove: removing the lockfile on deleting the current object + @type autoremove: bool + @param appname: name of the current running application + @type appname: str + @param verbose: verbose level + @type verbose: int + @param version: the version string of the current object or application + @type version: str + @param base_dir: the base directory of all operations + @type base_dir: str + @param silent: Remove silently the lockfile (except on verbose level >= 2) + @type silent: bool + + @return: None + """ + + super(LockObject, self).__init__( + appname=appname, verbose=verbose, version=version, + base_dir=base_dir, initialized=False, + ) + + if not lockfile: + raise LockObjectError("No lockfile given on init of a LockObject object.") + + if not os.path.exists(lockfile): + raise LockObjectError("Lockfile {!r} doesn't exists.".format(lockfile)) + + if not os.path.isfile(lockfile): + raise LockObjectError("Lockfile {!r} is not a regular file.".format(lockfile)) + + self._lockfile = os.path.realpath(lockfile) + + self._fcontent = None + if fcontent is not None: + self._fcontent = str(fcontent) + self._simulate = bool(simulate) + self._autoremove = bool(autoremove) + self._silent = bool(silent) + + self._ctime = ctime + self._mtime = mtime + + # Detecting self._ctime and self._mtime from filestat of the lockfile + if not self._ctime or not self._mtime: + fstat = os.stat(lockfile) + if not self._ctime: + self._ctime = datetime.datetime.utcfromtimestamp(fstat.st_ctime) + if not self._mtime: + self._mtime = datetime.datetime.utcfromtimestamp(fstat.st_mtime) + + self.initialized = True + + # ----------------------------------------------------------- + @property + def lockfile(self): + """The file, which represents the lock.""" + return self._lockfile + + # ----------------------------------------------------------- + @property + def ctime(self): + """The creation time of the lockfile.""" + return self._ctime + + # ----------------------------------------------------------- + @property + def mtime(self): + """The last modification time of the lockfile.""" + return self._mtime + + # ----------------------------------------------------------- + @property + def fcontent(self): + """The content of the lockfile.""" + return self._fcontent + + # ----------------------------------------------------------- + @property + def simulate(self): + """Don't execute actions, only display them.""" + return self._simulate + + @simulate.setter + def simulate(self, value): + self._simulate = bool(value) + + # ----------------------------------------------------------- + @property + def autoremove(self): + """Removing the lockfile on deleting the current object.""" + return self._autoremove + + @autoremove.setter + def autoremove(self, value): + self._autoremove = bool(value) + + # ----------------------------------------------------------- + @property + def silent(self): + """Remove silently the lockfile (except on verbose level >= 2).""" + return self._silent + + @silent.setter + def silent(self, value): + self._silent = bool(value) + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(LockObject, self).as_dict(short=short) + res['lockfile'] = self.lockfile + res['ctime'] = self.ctime + res['mtime'] = self.mtime + res['fcontent'] = self.fcontent + res['simulate'] = self.simulate + res['autoremove'] = self.autoremove + res['silent'] = self.silent + + return res + + # ------------------------------------------------------------------------- + def __repr__(self): + """Typecasting into a string for reproduction.""" + + out = super(LockObject, self).__repr__()[:-2] + + fields = [] + fields.append("lockfile={!r}".format(self.lockfile)) + if self.fcontent: + fields.append("fcontent={!r}".format(self.fcontent)) + fields.append("ctime={!r}".format(self.ctime)) + fields.append("mtime={!r}".format(self.mtime)) + fields.append("fcontent={!r}".format(self.fcontent)) + fields.append("simulate={!r}".format(self.simulate)) + fields.append("autoremove={!r}".format(self.autoremove)) + fields.append("silent={!r}".format(self.silent)) + + if fields: + out += ', ' + ", ".join(fields) + out += ")>" + return out + + # ------------------------------------------------------------------------- + def __del__(self): + """Destructor. + + Removes the lockfile, if self.autoremove is True + + """ + + if not getattr(self, '_initialized', False): + return + + if self.autoremove and self.exists: + + msg = "Automatic removing of {!r} ...".format(self.lockfile) + if self.silent: + if self.verbose >= 2: + log.debug(msg) + else: + log.info(msg) + + if not self.simulate: + os.remove(self.lockfile) + + # ------------------------------------------------------------------------- + def exists(self): + """Returns, whether the lockfile exists or not.""" + + if self.simulate: + return True + + return os.path.exists(self.lockfile) + + # ------------------------------------------------------------------------- + def refresh(self): + """ + Refreshes the atime and mtime of the lockfile to the current time. + """ + + msg = "Refreshing atime and mtime of {!r} to the current timestamp.".format(self.lockfile) + log.debug(msg) + + if not self.simulate: + os.utime(self.lockfile, None) + + self._mtime = datetime.datetime.utcnow() + + +# ============================================================================= +class LockHandler(BaseHandler): + """ + Handler class with additional properties and methods to create, + check and remove lock files. + """ + + # ------------------------------------------------------------------------- + def __init__( + self, lockdir=None, + lockretry_delay_start=DEFAULT_LOCKRETRY_DELAY_START, + lockretry_delay_increase=DEFAULT_LOCKRETRY_DELAY_INCREASE, + lockretry_max_delay=DEFAULT_LOCKRETRY_MAX_DELAY, + max_lockfile_age=DEFAULT_MAX_LOCKFILE_AGE, + locking_use_pid=DEFAULT_LOCKING_USE_PID, + appname=None, verbose=0, version=__version__, base_dir=None, + simulate=False, sudo=False, quiet=False, silent=False, *targs, **kwargs): + """ + Initialisation of the locking handler object. + + @raise LockdirNotExistsError: if the lockdir (or base_dir) doesn't exists + @raise LockHandlerError: on a uncoverable error. + + @param lockdir: a special directory for searching and creating the + lockfiles, if not given, self.base_dir will used + @type lockdir: str + @param lockretry_delay_start: the first delay in seconds after an + unsuccessful lockfile creation + @type lockretry_delay_start: Number + @param lockretry_delay_increase: seconds to increase the delay in every + wait cycle + @type lockretry_delay_increase: Number + @param lockretry_max_delay: the total maximum delay in seconds for + trying to create a lockfile + @type lockretry_max_delay: Number + @param max_lockfile_age: the maximum age of the lockfile (in seconds), + for the existing lockfile is valid (if + locking_use_pid is False). + @type max_lockfile_age: Number + @param locking_use_pid: write the PID of creating process into the + fresh created lockfile, if False, the lockfile + will be leaved empty, the PID in the lockfile + can be used to check the validity of the + lockfile + @type locking_use_pid: bool + @param appname: name of the current running application + @type appname: str + @param verbose: verbose level + @type verbose: int + @param version: the version string of the current object or application + @type version: str + @param base_dir: the base directory of all operations + @type base_dir: str + @param simulate: don't execute actions, only display them + @type simulate: bool + @param sudo: should the command executed by sudo by default + @type sudo: bool + @param quiet: don't display ouput of action after calling + @type quiet: bool + @param silent: Create and remove silently the lockfile (except on verbose level >= 2) + @type silent: bool + + @return: None + + """ + + super(LockHandler, self).__init__( + appname=appname, verbose=verbose, version=version, base_dir=base_dir, + initialized=False, simulate=simulate, sudo=sudo, quiet=quiet, + ) + + self._lockdir = None + if lockdir is not None: + self.lockdir = lockdir + + self._lockretry_delay_start = DEFAULT_LOCKRETRY_DELAY_START + self.lockretry_delay_start = lockretry_delay_start + + self._lockretry_delay_increase = DEFAULT_LOCKRETRY_DELAY_INCREASE + self.lockretry_delay_increase = lockretry_delay_increase + + self._lockretry_max_delay = DEFAULT_LOCKRETRY_MAX_DELAY + self.lockretry_max_delay = lockretry_max_delay + + self._max_lockfile_age = DEFAULT_MAX_LOCKFILE_AGE + self.max_lockfile_age = max_lockfile_age + + self._locking_use_pid = DEFAULT_LOCKING_USE_PID + self.locking_use_pid = locking_use_pid + + self._silent = bool(silent) + + # ----------------------------------------------------------- + @property + def lockdir(self): + """The directory for searching and creating the lockfiles.""" + if self._lockdir: + return self._lockdir + return self.base_dir + + @lockdir.setter + def lockdir(self, value): + if not value: + self._lockdir = None + return + + if os.path.isabs(value): + self._lockdir = os.path.normpath(value) + else: + self._lockdir = os.path.normpath(os.path.join(self.base_dir, value)) + + # ----------------------------------------------------------- + @property + def lockretry_delay_start(self): + """ + The first delay in seconds after an unsuccessful lockfile creation. + """ + return self._lockretry_delay_start + + @lockretry_delay_start.setter + def lockretry_delay_start(self, value): + if not isinstance(value, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=value, what='lockretry_delay_start') + raise LockHandlerError(msg) + + if value <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=value, what='lockretry_delay_start') + raise LockHandlerError(msg) + + self._lockretry_delay_start = value + + # ----------------------------------------------------------- + @property + def lockretry_delay_increase(self): + """ + The seconds to increase the delay in every wait cycle. + """ + return self._lockretry_delay_increase + + @lockretry_delay_increase.setter + def lockretry_delay_increase(self, value): + if not isinstance(value, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=value, what='lockretry_delay_increase') + raise LockHandlerError(msg) + + if value < 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=value, what='lockretry_delay_increase') + raise LockHandlerError(msg) + + self._lockretry_delay_increase = value + + # ----------------------------------------------------------- + @property + def lockretry_max_delay(self): + """ + The total maximum delay in seconds for trying to create a lockfile. + """ + return self._lockretry_max_delay + + @lockretry_max_delay.setter + def lockretry_max_delay(self, value): + if not isinstance(value, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=value, what='lockretry_max_delay') + raise LockHandlerError(msg) + + if value <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=value, what='lockretry_max_delay') + raise LockHandlerError(msg) + + self._lockretry_max_delay = value + + # ----------------------------------------------------------- + @property + def max_lockfile_age(self): + """ + The maximum age of the lockfile (in seconds), for the existing lockfile + is valid (if locking_use_pid is False). + """ + return self._max_lockfile_age + + @max_lockfile_age.setter + def max_lockfile_age(self, value): + if not isinstance(value, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=value, what='max_lockfile_age') + raise LockHandlerError(msg) + + if value <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=value, what='max_lockfile_age') + raise LockHandlerError(msg) + + self._max_lockfile_age = value + + # ----------------------------------------------------------- + @property + def locking_use_pid(self): + """ + Write the PID of creating process into the fresh created lockfile. + """ + return self._locking_use_pid + + @locking_use_pid.setter + def locking_use_pid(self, value): + self._locking_use_pid = bool(value) + + # ----------------------------------------------------------- + @property + def silent(self): + """Create and remove silently the lockfile (except on verbose level >= 2).""" + return self._silent + + @silent.setter + def silent(self, value): + self._silent = bool(value) + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(LockHandler, self).as_dict(short=short) + res['lockdir'] = self.lockdir + res['lockretry_delay_start'] = self.lockretry_delay_start + res['lockretry_delay_increase'] = self.lockretry_delay_increase + res['lockretry_max_delay'] = self.lockretry_max_delay + res['max_lockfile_age'] = self.max_lockfile_age + res['locking_use_pid'] = self.locking_use_pid + res['silent'] = self.silent + + return res + + # ------------------------------------------------------------------------- + def __repr__(self): + """Typecasting into a string for reproduction.""" + + out = super(LockHandler, self).__repr__()[:-2] + + fields = [] + if self._lockdir: + fields.append("lockdir=%r" % (self.lockdir)) + fields.append("lockretry_delay_start=%r" % (self.lockretry_delay_start)) + fields.append("lockretry_delay_increase=%r" % (self.lockretry_delay_increase)) + fields.append("lockretry_max_delay=%r" % (self.lockretry_max_delay)) + fields.append("max_lockfile_age=%r" % (self.max_lockfile_age)) + fields.append("locking_use_pid=%r" % (self.locking_use_pid)) + fields.append("silent=%r" % (self.silent)) + + if fields: + out += ', ' + ", ".join(fields) + out += ")>" + return out + + # ------------------------------------------------------------------------- + def create_lockfile( + self, lockfile, delay_start=None, delay_increase=None, max_delay=None, + use_pid=None, max_age=None, pid=None, raise_on_fail=True): + """ + Tries to create the given lockfile exclusive. + + If the lockfile exists and is valid, it waits a total maximum + of max_delay seconds an increasing amount of seconds to get exclusive + access to the lockfile. + + @raise CouldntOccupyLockfileError: if the lockfile couldn't occupied + and raise_on_fail is set to True + + @param lockfile: the lockfile to use as a semaphore, if not given + as an absolute path, it will be supposed to be + relative to self.lockdir. + @type lockfile: str + @param delay_start: the first delay in seconds after an unsuccessful + lockfile creation, if not given, + self.lockretry_delay_start will used. + @type delay_start: Number (or None) + @param delay_increase: seconds to increase the delay in every wait + cycle, if not given, self.lockretry_delay_increase + will used. + @type delay_increase: Number + @param max_delay: the total maximum delay in seconds for trying + to create a lockfile, if not given, + self.lockretry_max_delay will used. + @type max_delay: Number + @param use_pid: write the PID of creating process into the fresh + created lockfile, if not given, self.locking_use_pid + will used. + @type use_pid: bool + @param max_age: the maximum age of the lockfile (in seconds), for the + existing lockfile is valid (if locking_use_pid is False). + @type max_age: Number + @param pid: the pid to write into the lockfile, if use_pid is set + to True, if not given, the PID of the current process is used. + @type pid: int + @param raise_on_fail: raise an exception instead of returning False, if + the lockfile couldn't occupied. + @type raise_on_fail: bool + + @return: a lock object on success, else None + @rtype: LockObject or None + + """ + + if delay_start is None: + delay_start = self.lockretry_delay_start + else: + if not isinstance(delay_start, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=delay_start, what='delay_start') + raise LockHandlerError(msg) + if delay_start <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=delay_start, what='delay_start') + raise LockHandlerError(msg) + + if delay_increase is None: + delay_increase = self.lockretry_delay_increase + else: + if not isinstance(delay_increase, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=delay_increase, what='delay_increase') + raise LockHandlerError(msg) + if delay_increase < 0: + msg = ( + "The value for {what} must be greater than " + "or equal to zero (is {val!r}).").format( + val=delay_increase, what='delay_increase') + raise LockHandlerError(msg) + + if max_delay is None: + max_delay = self.lockretry_max_delay + else: + if not isinstance(max_delay, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=max_delay, what='max_delay') + raise LockHandlerError(msg) + if max_delay <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=max_delay, what='max_delay') + raise LockHandlerError(msg) + pass + + if use_pid is None: + use_pid = self.locking_use_pid + else: + use_pid = bool(use_pid) + + if max_age is None: + max_age = self.max_lockfile_age + else: + if not isinstance(max_age, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=max_age, what='max_age') + raise LockHandlerError(msg) + if max_age <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=max_age, what='max_age') + raise LockHandlerError(msg) + + if pid is None: + pid = os.getpid() + else: + pid = int(pid) + if pid <= 0: + msg = "Invalid PID {} given on calling create_lockfile().".format(pid) + raise LockHandlerError(msg) + + if os.path.isabs(lockfile): + lockfile = os.path.normpath(lockfile) + else: + lockfile = os.path.normpath(os.path.join(self.lockdir, lockfile)) + + lockdir = os.path.dirname(lockfile) + log.debug("Trying to lock lockfile {!r} ...".format(lockfile)) + if self.verbose > 1: + log.debug("Using lock directory {!r} ...".format(lockdir)) + + if not os.path.isdir(lockdir): + raise LockdirNotExistsError(lockdir) + + if not os.access(lockdir, os.W_OK): + msg = "Locking directory {!r} isn't writeable.".format(lockdir) + if self.simulate: + log.error(msg) + else: + raise LockdirNotWriteableError(lockdir) + + counter = 0 + delay = delay_start + + fd = None + time_diff = 0 + start_time = time.time() + + ctime = None + mtime = None + + # Big try block to ensure closing open file descriptor + try: + + # Big loop on trying to create the lockfile + while fd is None and time_diff < max_delay: + + time_diff = time.time() - start_time + counter += 1 + + if self.verbose > 3: + log.debug("Current time difference: {:0.3f} seconds.".format(time_diff)) + if time_diff >= max_delay: + break + + # Try creating lockfile exclusive + log.debug("Try {try_nr} on creating lockfile {lfile!r} ...".format( + try_nr=counter, lfile=lockfile)) + ctime = datetime.datetime.utcnow() + fd = self._create_lockfile(lockfile) + if fd is not None: + # success, then exit + break + + # Check for other process, using this lockfile + if not self.check_lockfile(lockfile, max_age, use_pid): + # No other process is using this lockfile + if os.path.exists(lockfile): + log.info("Removing lockfile {!r} ...".format(lockfile)) + try: + if not self.simulate: + os.remove(lockfile) + except Exception as e: + msg = "Error on removing lockfile {lfile!r): {err}".format( + lfile=lockfile, err=e) + log.error(msg) + time.sleep(delay) + delay += delay_increase + continue + + fd = self._create_lockfile(lockfile) + if fd: + break + + # No success, then retry later + if self.verbose > 2: + log.debug("Sleeping for {:0.1f} seconds.".format(float(delay))) + time.sleep(delay) + delay += delay_increase + + # fd is either None, for no success on locking + if fd is None: + time_diff = time.time() - start_time + e = CouldntOccupyLockfileError(lockfile, time_diff, counter) + if raise_on_fail: + raise e + else: + log.error(msg) + return None + + # or an int for success + msg = "Got a lock for lockfile {!r}.".format(lockfile) + if self.silent: + log.debug(msg) + else: + log.info(msg) + out = to_utf8("{}\n".format(pid)) + log.debug("Write {what!r} in lockfile {lfile!r} ...".format( + what=out, lfile=lockfile)) + + finally: + + if fd is not None and not self.simulate: + os.write(fd, out) + os.close(fd) + + fd = None + + mtime = datetime.datetime.utcnow() + + lock_object = LockObject( + lockfile, ctime=ctime, mtime=mtime, fcontent=out, simulate=self.simulate, + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, silent=self.silent, + ) + + return lock_object + + # ------------------------------------------------------------------------- + def _create_lockfile(self, lockfile): + """ + Handles exclusive creation of a lockfile. + + @return: a file decriptor of the opened lockfile (if possible), + or None, if it isn't. + @rtype: int or None + + """ + + if self.verbose > 1: + log.debug("Trying to open {!r} exclusive ...".format(lockfile)) + if self.simulate: + log.debug("Simulation mode, no real creation of a lockfile.") + return -1 + + fd = None + try: + fd = os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o644) + except OSError as e: + msg = "Error on creating lockfile {lfile!r}: {err}".format( + lfile=lockfile, err=e) + if e.errno == errno.EEXIST: + log.debug(msg) + return None + else: + error_tuple = sys.exc_info() + reraise(LockHandlerError, msg, error_tuple[2]) + + return fd + + # ------------------------------------------------------------------------- + def remove_lockfile(self, lockfile): + """ + Removing lockfile. + + @param lockfile: the lockfile to remove. + @type lockfile: str + + @return: the lockfile was removed (or not) + @rtype: bool + + """ + + if os.path.isabs(lockfile): + lockfile = os.path.normpath(lockfile) + else: + lockfile = os.path.normpath(os.path.join(self.lockdir, lockfile)) + + if not os.path.exists(lockfile): + log.debug("Lockfile {!r} to remove doesn't exists.".format(lockfile)) + return True + + log.info("Removing lockfile {!r} ...".format(lockfile)) + if self.simulate: + log.debug("Simulation mode - lockfile won't removed.") + return True + + try: + os.remove(lockfile) + except Exception as e: + log.error("Error on removing lockfile {lfile!r}: {err}".format(lfile=lockfile, err=e)) + if self.verbose: + tb = traceback.format_exc() + log.debug("Stacktrace:\n" + tb) + return False + + return True + + # ------------------------------------------------------------------------- + def check_lockfile(self, lockfile, max_age=None, use_pid=None): + """ + Checks the validity of the given lockfile. + + If use_pid is True, and there is a PID inside the lockfile, then + this PID is checked for a running process. + If use_pid is not True, then the age of the lockfile is checked + against the parameter max_age. + + @param lockfile: the lockfile to check + @type lockfile: str + @param max_age: the maximum age of the lockfile (in seconds), for + this lockfile is valid (if use_pid is False). + @type max_age: int + @param use_pid: check the content of the lockfile for a PID + of a running process + @type use_pid: bool + + @return: Validity of the lockfile (PID exists and shows to a + running process or the lockfile is not too old). + Returns False, if the lockfile is not existing, contains an + invalid PID or is too old. + @rtype: bool + + """ + + if use_pid is None: + use_pid = self.locking_use_pid + else: + use_pid = bool(use_pid) + + if max_age is None: + max_age = self.max_lockfile_age + else: + if not isinstance(max_age, Number): + msg = "Value {val!r} for {what} is not a Number.".format( + val=max_age, what='max_age') + raise LockHandlerError(msg) + if max_age <= 0: + msg = "The value for {what} must be greater than zero (is {val!r}).".format( + val=max_age, what='max_age') + raise LockHandlerError(msg) + + log.debug("Checking lockfile {!r} ...".format(lockfile)) + + if not os.path.exists(lockfile): + if self.verbose > 2: + log.debug("Lockfile {!r} doesn't exists.".format(lockfile)) + return False + + if not os.access(lockfile, os.R_OK): + log.warn("No read access for lockfile {!r}.".format(lockfile)) + return True + + if not os.access(lockfile, os.W_OK): + log.warn("No write access for lockfile {!r}.".format(lockfile)) + return True + + if use_pid: + pid = self.get_pid_from_file(lockfile, True) + if pid is None: + log.warn("Unusable lockfile {!r}.".format(lockfile)) + else: + if self.dead(pid): + log.warn("Process with PID {} is unfortunately dead.".format(pid)) + return False + else: + log.debug("Process with PID {} is still running.".format(pid)) + return True + + fstat = None + try: + fstat = os.stat(lockfile) + except OSError as e: + if e.errno == errno.ENOENT: + log.info("Could not stat for file {lfile!r}: {err}".format( + lfile=lockfile, err=e.strerror)) + return False + raise + + age = time.time() - fstat.st_mtime + if age >= max_age: + log.debug("Lockfile {lfile!r} is older than {max} seconds ({age} seconds).".format( + lfile=lockfile, max=max_age, age=age)) + return False + msg = "Lockfile {lfile!r} is {age} seconds old, but not old enough ({max}seconds).".format( + lfile=lockfile, max=max_age, age=age) + log.debug(msg) + return True + + # ------------------------------------------------------------------------- + def get_pid_from_file(self, pidfile, force=False): + """ + Tries to read the PID of some process from the given file. + + @raise LockHandlerError: if the pidfile could not be read + + @param pidfile: The file, where the PID should be in. + @type pidfile: str + @param force: Don't raise an exception, if something is going wrong. + Only return None. + @type force: bool + + @return: PID from pidfile + @rtype: int (or None) + + """ + + if self.verbose > 1: + log.debug("Trying to open pidfile {!r} ...".format(pidfile)) + try: + fh = open(pidfile, "rb") + except Exception as e: + msg = "Could not open pidfile {!r} for reading: ".format(pidfile) + msg += str(e) + if force: + log.warn(msg) + return None + else: + raise LockHandlerError(str(e)) + + content = fh.readline() + fh.close() + + content = content.strip() + if content == "": + msg = "First line of pidfile {!r} was empty.".format(pidfile) + if force: + log.warn(msg) + return None + else: + raise LockHandlerError(msg) + + pid = None + try: + pid = int(content) + except Exception as e: + msg = "Could not interprete {cont!r} as a PID from {file!r}: {err}".format( + cont=content, file=pidfile, err=e) + if force: + log.warn(msg) + return None + else: + raise LockHandlerError(msg) + + if pid <= 0: + msg = "Invalid PID {pid} in {file!r} found.".format(pid=pid, file=pidfile) + if force: + log.warn(msg) + return None + else: + raise LockHandlerError(msg) + + return pid + + # ------------------------------------------------------------------------- + def kill(self, pid, signal=0): + """ + Sends a signal to a process. + + @raise OSError: on some unpredictable errors + + @param pid: the PID of the process + @type pid: int + @param signal: the signal to send to the process, if the signal is 0 + (the default), no real signal is sent to the process, + it will only checked, whether the process is dead or not + @type signal: int + + @return: the process is dead or not + @rtype: bool + + """ + + try: + return os.kill(pid, signal) + except OSError as e: + # process is dead + if e.errno == errno.ESRCH: + return True + # no permissions + elif e.errno == errno.EPERM: + return False + else: + # reraise the error + raise + + # ------------------------------------------------------------------------- + def dead(self, pid): + """ + Gives back, whether the process with the given pid is dead + + @raise OSError: on some unpredictable errors + + @param pid: the PID of the process to check + @type pid: int + + @return: the process is dead or not + @rtype: bool + + """ + + if self.kill(pid): + return True + + # maybe the pid is a zombie that needs us to wait4 it + from os import waitpid, WNOHANG + + try: + dead = waitpid(pid, WNOHANG)[0] + except OSError as e: + # pid is not a child + if e.errno == errno.ECHILD: + return False + else: + raise + + return dead + +# ============================================================================= +if __name__ == "__main__": + + pass + +# ============================================================================= + +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4