--- /dev/null
+#!/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