From 927aacca2b3569a874af38fa5cdbe80fe4f278b3 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Wed, 27 Apr 2016 09:19:43 +0200 Subject: [PATCH] add require libs; install script --- daemon/__init__.py | 49 ++ daemon/__init__.pyc | Bin 0 -> 1169 bytes daemon/__init__.pyo | Bin 0 -> 1169 bytes daemon/_metadata.py | 152 ++++++ daemon/_metadata.pyc | Bin 0 -> 4687 bytes daemon/_metadata.pyo | Bin 0 -> 4687 bytes daemon/daemon.py | 926 +++++++++++++++++++++++++++++++++++ daemon/daemon.pyc | Bin 0 -> 31631 bytes daemon/daemon.pyo | Bin 0 -> 31631 bytes daemon/pidfile.py | 67 +++ daemon/pidfile.pyc | Bin 0 -> 2141 bytes daemon/pidfile.pyo | Bin 0 -> 2141 bytes daemon/runner.py | 324 ++++++++++++ daemon/runner.pyc | Bin 0 -> 10977 bytes daemon/runner.pyo | Bin 0 -> 10977 bytes install.sh | 5 + lockfile/__init__.py | 347 +++++++++++++ lockfile/__init__.pyc | Bin 0 -> 11938 bytes lockfile/__init__.pyo | Bin 0 -> 11938 bytes lockfile/linklockfile.py | 73 +++ lockfile/linklockfile.pyc | Bin 0 -> 2867 bytes lockfile/linklockfile.pyo | Bin 0 -> 2867 bytes lockfile/mkdirlockfile.py | 84 ++++ lockfile/mkdirlockfile.pyc | Bin 0 -> 3378 bytes lockfile/mkdirlockfile.pyo | Bin 0 -> 3378 bytes lockfile/pidlockfile.py | 190 +++++++ lockfile/pidlockfile.pyc | Bin 0 -> 5837 bytes lockfile/pidlockfile.pyo | Bin 0 -> 5837 bytes lockfile/sqlitelockfile.py | 156 ++++++ lockfile/sqlitelockfile.pyc | Bin 0 -> 4649 bytes lockfile/sqlitelockfile.pyo | Bin 0 -> 4649 bytes lockfile/symlinklockfile.py | 70 +++ lockfile/symlinklockfile.pyc | Bin 0 -> 2777 bytes lockfile/symlinklockfile.pyo | Bin 0 -> 2777 bytes 34 files changed, 2443 insertions(+) create mode 100644 daemon/__init__.py create mode 100644 daemon/__init__.pyc create mode 100644 daemon/__init__.pyo create mode 100644 daemon/_metadata.py create mode 100644 daemon/_metadata.pyc create mode 100644 daemon/_metadata.pyo create mode 100644 daemon/daemon.py create mode 100644 daemon/daemon.pyc create mode 100644 daemon/daemon.pyo create mode 100644 daemon/pidfile.py create mode 100644 daemon/pidfile.pyc create mode 100644 daemon/pidfile.pyo create mode 100644 daemon/runner.py create mode 100644 daemon/runner.pyc create mode 100644 daemon/runner.pyo create mode 100755 install.sh create mode 100644 lockfile/__init__.py create mode 100644 lockfile/__init__.pyc create mode 100644 lockfile/__init__.pyo create mode 100644 lockfile/linklockfile.py create mode 100644 lockfile/linklockfile.pyc create mode 100644 lockfile/linklockfile.pyo create mode 100644 lockfile/mkdirlockfile.py create mode 100644 lockfile/mkdirlockfile.pyc create mode 100644 lockfile/mkdirlockfile.pyo create mode 100644 lockfile/pidlockfile.py create mode 100644 lockfile/pidlockfile.pyc create mode 100644 lockfile/pidlockfile.pyo create mode 100644 lockfile/sqlitelockfile.py create mode 100644 lockfile/sqlitelockfile.pyc create mode 100644 lockfile/sqlitelockfile.pyo create mode 100644 lockfile/symlinklockfile.py create mode 100644 lockfile/symlinklockfile.pyc create mode 100644 lockfile/symlinklockfile.pyo diff --git a/daemon/__init__.py b/daemon/__init__.py new file mode 100644 index 0000000..4731a6e --- /dev/null +++ b/daemon/__init__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# daemon/__init__.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2009–2015 Ben Finney +# Copyright © 2006 Robert Niederreiter +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Library to implement a well-behaved Unix daemon process. + + This library implements the well-behaved daemon specification of + :pep:`3143`, “Standard daemon process library”. + + A well-behaved Unix daemon process is tricky to get right, but the + required steps are much the same for every daemon program. A + `DaemonContext` instance holds the behaviour and configured + process environment for the program; use the instance as a context + manager to enter a daemon state. + + Simple example of usage:: + + import daemon + + from spam import do_main_program + + with daemon.DaemonContext(): + do_main_program() + + Customisation of the steps to become a daemon is available by + setting options on the `DaemonContext` instance; see the + documentation for that class for each option. + + """ + +from __future__ import (absolute_import, unicode_literals) + +from .daemon import DaemonContext + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff --git a/daemon/__init__.pyc b/daemon/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71deb9c357ccc72b0b7e942ac9315c2cab18f40c GIT binary patch literal 1169 zcmZ`&&2AGh5O$Kbphy**IL`^CQkxWtgh&x;RSp~whqhdD**LqCtX2Ok_H3Ho`XKNI zJO)qFSKtL0d$);#zz47G^?cvV_l555Ln4XyQM5pM9Vn^s5VRpr2m-oBkYOu#a^{1W?@LA2g^mjlGl&Lftv;-1vQR|}_ zmU4*&e6?x?g+${lRNmzn!m!^59@nJ`pj(J_d;ltO8={R473ONLa!HD@+&qDdE3C%T zlUHv}ribuw|NV-jEu=60qI~3exc||zpKo*gBTeA+$gBJ&6}UhGuNEa8!mOs4Wa9Vu ztyZ4v41`qx=@Cqwm#KZ_@cz$LOu8xa;>njxGgfa%Ai&4sgsE1H6`flJ5{N~eoP zj3lLUwFj;Za%bmiQ8Q2WxQ2sYay8nP7~xa)BJ? z7>RfxAM@pO2wTyvl1+ds8SQ2d7@5PyGUPNPUgY@xx1AhaH~wx-Yta0D8J)GXMYp literal 0 HcmV?d00001 diff --git a/daemon/__init__.pyo b/daemon/__init__.pyo new file mode 100644 index 0000000000000000000000000000000000000000..71deb9c357ccc72b0b7e942ac9315c2cab18f40c GIT binary patch literal 1169 zcmZ`&&2AGh5O$Kbphy**IL`^CQkxWtgh&x;RSp~whqhdD**LqCtX2Ok_H3Ho`XKNI zJO)qFSKtL0d$);#zz47G^?cvV_l555Ln4XyQM5pM9Vn^s5VRpr2m-oBkYOu#a^{1W?@LA2g^mjlGl&Lftv;-1vQR|}_ zmU4*&e6?x?g+${lRNmzn!m!^59@nJ`pj(J_d;ltO8={R473ONLa!HD@+&qDdE3C%T zlUHv}ribuw|NV-jEu=60qI~3exc||zpKo*gBTeA+$gBJ&6}UhGuNEa8!mOs4Wa9Vu ztyZ4v41`qx=@Cqwm#KZ_@cz$LOu8xa;>njxGgfa%Ai&4sgsE1H6`flJ5{N~eoP zj3lLUwFj;Za%bmiQ8Q2WxQ2sYay8nP7~xa)BJ? z7>RfxAM@pO2wTyvl1+ds8SQ2d7@5PyGUPNPUgY@xx1AhaH~wx-Yta0D8J)GXMYp literal 0 HcmV?d00001 diff --git a/daemon/_metadata.py b/daemon/_metadata.py new file mode 100644 index 0000000..6d22a2b --- /dev/null +++ b/daemon/_metadata.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- + +# daemon/_metadata.py +# Part of ‘python-daemon’, an implementation of PEP 3143. +# +# Copyright © 2008–2015 Ben Finney +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Package metadata for the ‘python-daemon’ distribution. """ + +from __future__ import (absolute_import, unicode_literals) + +import json +import re +import collections +import datetime + +import pkg_resources + + +distribution_name = "python-daemon" +version_info_filename = "version_info.json" + +def get_distribution_version_info(filename=version_info_filename): + """ Get the version info from the installed distribution. + + :param filename: Base filename of the version info resource. + :return: The version info as a mapping of fields. If the + distribution is not available, the mapping is empty. + + The version info is stored as a metadata file in the + distribution. + + """ + version_info = { + 'release_date': "UNKNOWN", + 'version': "UNKNOWN", + 'maintainer': "UNKNOWN", + } + + try: + distribution = pkg_resources.get_distribution(distribution_name) + except pkg_resources.DistributionNotFound: + distribution = None + + if distribution is not None: + if distribution.has_metadata(version_info_filename): + content = distribution.get_metadata(version_info_filename) + version_info = json.loads(content) + + return version_info + +version_info = get_distribution_version_info() + +version_installed = version_info['version'] + + +rfc822_person_regex = re.compile( + "^(?P[^<]+) <(?P[^>]+)>$") + +ParsedPerson = collections.namedtuple('ParsedPerson', ['name', 'email']) + +def parse_person_field(value): + """ Parse a person field into name and email address. + + :param value: The text value specifying a person. + :return: A 2-tuple (name, email) for the person's details. + + If the `value` does not match a standard person with email + address, the `email` item is ``None``. + + """ + result = (None, None) + + match = rfc822_person_regex.match(value) + if len(value): + if match is not None: + result = ParsedPerson( + name=match.group('name'), + email=match.group('email')) + else: + result = ParsedPerson(name=value, email=None) + + return result + +author_name = "Ben Finney" +author_email = "ben+python@benfinney.id.au" +author = "{name} <{email}>".format(name=author_name, email=author_email) + + +class YearRange: + """ A range of years spanning a period. """ + + def __init__(self, begin, end=None): + self.begin = begin + self.end = end + + def __unicode__(self): + text = "{range.begin:04d}".format(range=self) + if self.end is not None: + if self.end > self.begin: + text = "{range.begin:04d}–{range.end:04d}".format(range=self) + return text + + __str__ = __unicode__ + + +def make_year_range(begin_year, end_date=None): + """ Construct the year range given a start and possible end date. + + :param begin_date: The beginning year (text) for the range. + :param end_date: The end date (text, ISO-8601 format) for the + range, or a non-date token string. + :return: The range of years as a `YearRange` instance. + + If the `end_date` is not a valid ISO-8601 date string, the + range has ``None`` for the end year. + + """ + begin_year = int(begin_year) + + try: + end_date = datetime.datetime.strptime(end_date, "%Y-%m-%d") + except (TypeError, ValueError): + # Specified end_date value is not a valid date. + end_year = None + else: + end_year = end_date.year + + year_range = YearRange(begin=begin_year, end=end_year) + + return year_range + +copyright_year_begin = "2001" +build_date = version_info['release_date'] +copyright_year_range = make_year_range(copyright_year_begin, build_date) + +copyright = "Copyright © {year_range} {author} and others".format( + year_range=copyright_year_range, author=author) +license = "Apache-2" +url = "https://alioth.debian.org/projects/python-daemon/" + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff --git a/daemon/_metadata.pyc b/daemon/_metadata.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a1453691e764dad51c0cbcf09580d98e2343e92 GIT binary patch literal 4687 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 +# Copyright © 2007–2008 Robert Niederreiter, Jens Klein +# Copyright © 2004–2005 Chad J. Schroeder +# Copyright © 2003 Clark Evans +# Copyright © 2002 Noah Spurrier +# Copyright © 2001 Jürgen Hermann +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Daemon process behaviour. + """ + +from __future__ import (absolute_import, unicode_literals) + +import os +import sys +import resource +import errno +import signal +import socket +import atexit +try: + # Python 2 has both ‘str’ (bytes) and ‘unicode’ (text). + basestring = basestring + unicode = unicode +except NameError: + # Python 3 names the Unicode data type ‘str’. + basestring = str + unicode = str + + +class DaemonError(Exception): + """ Base exception class for errors from this module. """ + + def __init__(self, *args, **kwargs): + self._chain_from_context() + + super(DaemonError, self).__init__(*args, **kwargs) + + def _chain_from_context(self): + _chain_exception_from_existing_exception_context(self, as_cause=True) + + +class DaemonOSEnvironmentError(DaemonError, OSError): + """ Exception raised when daemon OS environment setup receives error. """ + + +class DaemonProcessDetachError(DaemonError, OSError): + """ Exception raised when process detach fails. """ + + +class DaemonContext: + """ Context for turning the current program into a daemon process. + + A `DaemonContext` instance represents the behaviour settings and + process context for the program when it becomes a daemon. The + behaviour and environment is customised by setting options on the + instance, before calling the `open` method. + + Each option can be passed as a keyword argument to the `DaemonContext` + constructor, or subsequently altered by assigning to an attribute on + the instance at any time prior to calling `open`. That is, for + options named `wibble` and `wubble`, the following invocation:: + + foo = daemon.DaemonContext(wibble=bar, wubble=baz) + foo.open() + + is equivalent to:: + + foo = daemon.DaemonContext() + foo.wibble = bar + foo.wubble = baz + foo.open() + + The following options are defined. + + `files_preserve` + :Default: ``None`` + + List of files that should *not* be closed when starting the + daemon. If ``None``, all open file descriptors will be closed. + + Elements of the list are file descriptors (as returned by a file + object's `fileno()` method) or Python `file` objects. Each + specifies a file that is not to be closed during daemon start. + + `chroot_directory` + :Default: ``None`` + + Full path to a directory to set as the effective root directory of + the process. If ``None``, specifies that the root directory is not + to be changed. + + `working_directory` + :Default: ``'/'`` + + Full path of the working directory to which the process should + change on daemon start. + + Since a filesystem cannot be unmounted if a process has its + current working directory on that filesystem, this should either + be left at default or set to a directory that is a sensible “home + directory” for the daemon while it is running. + + `umask` + :Default: ``0`` + + File access creation mask (“umask”) to set for the process on + daemon start. + + A daemon should not rely on the parent process's umask value, + which is beyond its control and may prevent creating a file with + the required access mode. So when the daemon context opens, the + umask is set to an explicit known value. + + If the conventional value of 0 is too open, consider setting a + value such as 0o022, 0o027, 0o077, or another specific value. + Otherwise, ensure the daemon creates every file with an + explicit access mode for the purpose. + + `pidfile` + :Default: ``None`` + + Context manager for a PID lock file. When the daemon context opens + and closes, it enters and exits the `pidfile` context manager. + + `detach_process` + :Default: ``None`` + + If ``True``, detach the process context when opening the daemon + context; if ``False``, do not detach. + + If unspecified (``None``) during initialisation of the instance, + this will be set to ``True`` by default, and ``False`` only if + detaching the process is determined to be redundant; for example, + in the case when the process was started by `init`, by `initd`, or + by `inetd`. + + `signal_map` + :Default: system-dependent + + Mapping from operating system signals to callback actions. + + The mapping is used when the daemon context opens, and determines + the action for each signal's signal handler: + + * A value of ``None`` will ignore the signal (by setting the + signal action to ``signal.SIG_IGN``). + + * A string value will be used as the name of an attribute on the + ``DaemonContext`` instance. The attribute's value will be used + as the action for the signal handler. + + * Any other value will be used as the action for the + signal handler. See the ``signal.signal`` documentation + for details of the signal handler interface. + + The default value depends on which signals are defined on the + running system. Each item from the list below whose signal is + actually defined in the ``signal`` module will appear in the + default map: + + * ``signal.SIGTTIN``: ``None`` + + * ``signal.SIGTTOU``: ``None`` + + * ``signal.SIGTSTP``: ``None`` + + * ``signal.SIGTERM``: ``'terminate'`` + + Depending on how the program will interact with its child + processes, it may need to specify a signal map that + includes the ``signal.SIGCHLD`` signal (received when a + child process exits). See the specific operating system's + documentation for more detail on how to determine what + circumstances dictate the need for signal handlers. + + `uid` + :Default: ``os.getuid()`` + + `gid` + :Default: ``os.getgid()`` + + The user ID (“UID”) value and group ID (“GID”) value to switch + the process to on daemon start. + + The default values, the real UID and GID of the process, will + relinquish any effective privilege elevation inherited by the + process. + + `prevent_core` + :Default: ``True`` + + If true, prevents the generation of core files, in order to avoid + leaking sensitive information from daemons run as `root`. + + `stdin` + :Default: ``None`` + + `stdout` + :Default: ``None`` + + `stderr` + :Default: ``None`` + + Each of `stdin`, `stdout`, and `stderr` is a file-like object + which will be used as the new file for the standard I/O stream + `sys.stdin`, `sys.stdout`, and `sys.stderr` respectively. The file + should therefore be open, with a minimum of mode 'r' in the case + of `stdin`, and mimimum of mode 'w+' in the case of `stdout` and + `stderr`. + + If the object has a `fileno()` method that returns a file + descriptor, the corresponding file will be excluded from being + closed during daemon start (that is, it will be treated as though + it were listed in `files_preserve`). + + If ``None``, the corresponding system stream is re-bound to the + file named by `os.devnull`. + + """ + + __metaclass__ = type + + def __init__( + self, + chroot_directory=None, + working_directory="/", + umask=0, + uid=None, + gid=None, + prevent_core=True, + detach_process=None, + files_preserve=None, + pidfile=None, + stdin=None, + stdout=None, + stderr=None, + signal_map=None, + ): + """ Set up a new instance. """ + self.chroot_directory = chroot_directory + self.working_directory = working_directory + self.umask = umask + self.prevent_core = prevent_core + self.files_preserve = files_preserve + self.pidfile = pidfile + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + + if uid is None: + uid = os.getuid() + self.uid = uid + if gid is None: + gid = os.getgid() + self.gid = gid + + if detach_process is None: + detach_process = is_detach_process_context_required() + self.detach_process = detach_process + + if signal_map is None: + signal_map = make_default_signal_map() + self.signal_map = signal_map + + self._is_open = False + + @property + def is_open(self): + """ ``True`` if the instance is currently open. """ + return self._is_open + + def open(self): + """ Become a daemon process. + + :return: ``None``. + + Open the daemon context, turning the current program into a daemon + process. This performs the following steps: + + * If this instance's `is_open` property is true, return + immediately. This makes it safe to call `open` multiple times on + an instance. + + * If the `prevent_core` attribute is true, set the resource limits + for the process to prevent any core dump from the process. + + * If the `chroot_directory` attribute is not ``None``, set the + effective root directory of the process to that directory (via + `os.chroot`). + + This allows running the daemon process inside a “chroot gaol” + as a means of limiting the system's exposure to rogue behaviour + by the process. Note that the specified directory needs to + already be set up for this purpose. + + * Set the process UID and GID to the `uid` and `gid` attribute + values. + + * Close all open file descriptors. This excludes those listed in + the `files_preserve` attribute, and those that correspond to the + `stdin`, `stdout`, or `stderr` attributes. + + * Change current working directory to the path specified by the + `working_directory` attribute. + + * Reset the file access creation mask to the value specified by + the `umask` attribute. + + * If the `detach_process` option is true, detach the current + process into its own process group, and disassociate from any + controlling terminal. + + * Set signal handlers as specified by the `signal_map` attribute. + + * If any of the attributes `stdin`, `stdout`, `stderr` are not + ``None``, bind the system streams `sys.stdin`, `sys.stdout`, + and/or `sys.stderr` to the files represented by the + corresponding attributes. Where the attribute has a file + descriptor, the descriptor is duplicated (instead of re-binding + the name). + + * If the `pidfile` attribute is not ``None``, enter its context + manager. + + * Mark this instance as open (for the purpose of future `open` and + `close` calls). + + * Register the `close` method to be called during Python's exit + processing. + + When the function returns, the running program is a daemon + process. + + """ + if self.is_open: + return + + if self.chroot_directory is not None: + change_root_directory(self.chroot_directory) + + if self.prevent_core: + prevent_core_dump() + + change_file_creation_mask(self.umask) + change_working_directory(self.working_directory) + change_process_owner(self.uid, self.gid) + + if self.detach_process: + detach_process_context() + + signal_handler_map = self._make_signal_handler_map() + set_signal_handlers(signal_handler_map) + + exclude_fds = self._get_exclude_file_descriptors() + close_all_open_files(exclude=exclude_fds) + + redirect_stream(sys.stdin, self.stdin) + redirect_stream(sys.stdout, self.stdout) + redirect_stream(sys.stderr, self.stderr) + + if self.pidfile is not None: + self.pidfile.__enter__() + + self._is_open = True + + register_atexit_function(self.close) + + def __enter__(self): + """ Context manager entry point. """ + self.open() + return self + + def close(self): + """ Exit the daemon process context. + + :return: ``None``. + + Close the daemon context. This performs the following steps: + + * If this instance's `is_open` property is false, return + immediately. This makes it safe to call `close` multiple times + on an instance. + + * If the `pidfile` attribute is not ``None``, exit its context + manager. + + * Mark this instance as closed (for the purpose of future `open` + and `close` calls). + + """ + if not self.is_open: + return + + if self.pidfile is not None: + # Follow the interface for telling a context manager to exit, + # . + self.pidfile.__exit__(None, None, None) + + self._is_open = False + + def __exit__(self, exc_type, exc_value, traceback): + """ Context manager exit point. """ + self.close() + + def terminate(self, signal_number, stack_frame): + """ Signal handler for end-process signals. + + :param signal_number: The OS signal number received. + :param stack_frame: The frame object at the point the + signal was received. + :return: ``None``. + + Signal handler for the ``signal.SIGTERM`` signal. Performs the + following step: + + * Raise a ``SystemExit`` exception explaining the signal. + + """ + exception = SystemExit( + "Terminating on signal {signal_number!r}".format( + signal_number=signal_number)) + raise exception + + def _get_exclude_file_descriptors(self): + """ Get the set of file descriptors to exclude closing. + + :return: A set containing the file descriptors for the + files to be preserved. + + The file descriptors to be preserved are those from the + items in `files_preserve`, and also each of `stdin`, + `stdout`, and `stderr`. For each item: + + * If the item is ``None``, it is omitted from the return + set. + + * If the item's ``fileno()`` method returns a value, that + value is in the return set. + + * Otherwise, the item is in the return set verbatim. + + """ + files_preserve = self.files_preserve + if files_preserve is None: + files_preserve = [] + files_preserve.extend( + item for item in [self.stdin, self.stdout, self.stderr] + if hasattr(item, 'fileno')) + + exclude_descriptors = set() + for item in files_preserve: + if item is None: + continue + file_descriptor = _get_file_descriptor(item) + if file_descriptor is not None: + exclude_descriptors.add(file_descriptor) + else: + exclude_descriptors.add(item) + + return exclude_descriptors + + def _make_signal_handler(self, target): + """ Make the signal handler for a specified target object. + + :param target: A specification of the target for the + handler; see below. + :return: The value for use by `signal.signal()`. + + If `target` is ``None``, return ``signal.SIG_IGN``. If `target` + is a text string, return the attribute of this instance named + by that string. Otherwise, return `target` itself. + + """ + if target is None: + result = signal.SIG_IGN + elif isinstance(target, basestring): + name = target + result = getattr(self, name) + else: + result = target + + return result + + def _make_signal_handler_map(self): + """ Make the map from signals to handlers for this instance. + + :return: The constructed signal map for this instance. + + Construct a map from signal numbers to handlers for this + context instance, suitable for passing to + `set_signal_handlers`. + + """ + signal_handler_map = dict( + (signal_number, self._make_signal_handler(target)) + for (signal_number, target) in self.signal_map.items()) + return signal_handler_map + + +def _get_file_descriptor(obj): + """ Get the file descriptor, if the object has one. + + :param obj: The object expected to be a file-like object. + :return: The file descriptor iff the file supports it; otherwise + ``None``. + + The object may be a non-file object. It may also be a + file-like object with no support for a file descriptor. In + either case, return ``None``. + + """ + file_descriptor = None + if hasattr(obj, 'fileno'): + try: + file_descriptor = obj.fileno() + except ValueError: + # The item doesn't support a file descriptor. + pass + + return file_descriptor + + +def change_working_directory(directory): + """ Change the working directory of this process. + + :param directory: The target directory path. + :return: ``None``. + + """ + try: + os.chdir(directory) + except Exception as exc: + error = DaemonOSEnvironmentError( + "Unable to change working directory ({exc})".format(exc=exc)) + raise error + + +def change_root_directory(directory): + """ Change the root directory of this process. + + :param directory: The target directory path. + :return: ``None``. + + Set the current working directory, then the process root directory, + to the specified `directory`. Requires appropriate OS privileges + for this process. + + """ + try: + os.chdir(directory) + os.chroot(directory) + except Exception as exc: + error = DaemonOSEnvironmentError( + "Unable to change root directory ({exc})".format(exc=exc)) + raise error + + +def change_file_creation_mask(mask): + """ Change the file creation mask for this process. + + :param mask: The numeric file creation mask to set. + :return: ``None``. + + """ + try: + os.umask(mask) + except Exception as exc: + error = DaemonOSEnvironmentError( + "Unable to change file creation mask ({exc})".format(exc=exc)) + raise error + + +def change_process_owner(uid, gid): + """ Change the owning UID and GID of this process. + + :param uid: The target UID for the daemon process. + :param gid: The target GID for the daemon process. + :return: ``None``. + + Set the GID then the UID of the process (in that order, to avoid + permission errors) to the specified `gid` and `uid` values. + Requires appropriate OS privileges for this process. + + """ + try: + os.setgid(gid) + os.setuid(uid) + except Exception as exc: + error = DaemonOSEnvironmentError( + "Unable to change process owner ({exc})".format(exc=exc)) + raise error + + +def prevent_core_dump(): + """ Prevent this process from generating a core dump. + + :return: ``None``. + + Set the soft and hard limits for core dump size to zero. On Unix, + this entirely prevents the process from creating core dump. + + """ + core_resource = resource.RLIMIT_CORE + + try: + # Ensure the resource limit exists on this platform, by requesting + # its current value. + core_limit_prev = resource.getrlimit(core_resource) + except ValueError as exc: + error = DaemonOSEnvironmentError( + "System does not support RLIMIT_CORE resource limit" + " ({exc})".format(exc=exc)) + raise error + + # Set hard and soft limits to zero, i.e. no core dump at all. + core_limit = (0, 0) + resource.setrlimit(core_resource, core_limit) + + +def detach_process_context(): + """ Detach the process context from parent and session. + + :return: ``None``. + + Detach from the parent process and session group, allowing the + parent to exit while this process continues running. + + Reference: “Advanced Programming in the Unix Environment”, + section 13.3, by W. Richard Stevens, published 1993 by + Addison-Wesley. + + """ + + def fork_then_exit_parent(error_message): + """ Fork a child process, then exit the parent process. + + :param error_message: Message for the exception in case of a + detach failure. + :return: ``None``. + :raise DaemonProcessDetachError: If the fork fails. + + """ + try: + pid = os.fork() + if pid > 0: + os._exit(0) + except OSError as exc: + error = DaemonProcessDetachError( + "{message}: [{exc.errno:d}] {exc.strerror}".format( + message=error_message, exc=exc)) + raise error + + fork_then_exit_parent(error_message="Failed first fork") + os.setsid() + fork_then_exit_parent(error_message="Failed second fork") + + +def is_process_started_by_init(): + """ Determine whether the current process is started by `init`. + + :return: ``True`` iff the parent process is `init`; otherwise + ``False``. + + The `init` process is the one with process ID of 1. + + """ + result = False + + init_pid = 1 + if os.getppid() == init_pid: + result = True + + return result + + +def is_socket(fd): + """ Determine whether the file descriptor is a socket. + + :param fd: The file descriptor to interrogate. + :return: ``True`` iff the file descriptor is a socket; otherwise + ``False``. + + Query the socket type of `fd`. If there is no error, the file is a + socket. + + """ + result = False + + file_socket = socket.fromfd(fd, socket.AF_INET, socket.SOCK_RAW) + + try: + socket_type = file_socket.getsockopt( + socket.SOL_SOCKET, socket.SO_TYPE) + except socket.error as exc: + exc_errno = exc.args[0] + if exc_errno == errno.ENOTSOCK: + # Socket operation on non-socket. + pass + else: + # Some other socket error. + result = True + else: + # No error getting socket type. + result = True + + return result + + +def is_process_started_by_superserver(): + """ Determine whether the current process is started by the superserver. + + :return: ``True`` if this process was started by the internet + superserver; otherwise ``False``. + + The internet superserver creates a network socket, and + attaches it to the standard streams of the child process. If + that is the case for this process, return ``True``, otherwise + ``False``. + + """ + result = False + + stdin_fd = sys.__stdin__.fileno() + if is_socket(stdin_fd): + result = True + + return result + + +def is_detach_process_context_required(): + """ Determine whether detaching the process context is required. + + :return: ``True`` iff the process is already detached; otherwise + ``False``. + + The process environment is interrogated for the following: + + * Process was started by `init`; or + + * Process was started by `inetd`. + + If any of the above are true, the process is deemed to be already + detached. + + """ + result = True + if is_process_started_by_init() or is_process_started_by_superserver(): + result = False + + return result + + +def close_file_descriptor_if_open(fd): + """ Close a file descriptor if already open. + + :param fd: The file descriptor to close. + :return: ``None``. + + Close the file descriptor `fd`, suppressing an error in the + case the file was not open. + + """ + try: + os.close(fd) + except EnvironmentError as exc: + if exc.errno == errno.EBADF: + # File descriptor was not open. + pass + else: + error = DaemonOSEnvironmentError( + "Failed to close file descriptor {fd:d} ({exc})".format( + fd=fd, exc=exc)) + raise error + + +MAXFD = 2048 + +def get_maximum_file_descriptors(): + """ Get the maximum number of open file descriptors for this process. + + :return: The number (integer) to use as the maximum number of open + files for this process. + + The maximum is the process hard resource limit of maximum number of + open file descriptors. If the limit is “infinity”, a default value + of ``MAXFD`` is returned. + + """ + limits = resource.getrlimit(resource.RLIMIT_NOFILE) + result = limits[1] + if result == resource.RLIM_INFINITY: + result = MAXFD + return result + + +def close_all_open_files(exclude=set()): + """ Close all open file descriptors. + + :param exclude: Collection of file descriptors to skip when closing + files. + :return: ``None``. + + Closes every file descriptor (if open) of this process. If + specified, `exclude` is a set of file descriptors to *not* + close. + + """ + maxfd = get_maximum_file_descriptors() + for fd in reversed(range(maxfd)): + if fd not in exclude: + close_file_descriptor_if_open(fd) + + +def redirect_stream(system_stream, target_stream): + """ Redirect a system stream to a specified file. + + :param standard_stream: A file object representing a standard I/O + stream. + :param target_stream: The target file object for the redirected + stream, or ``None`` to specify the null device. + :return: ``None``. + + `system_stream` is a standard system stream such as + ``sys.stdout``. `target_stream` is an open file object that + should replace the corresponding system stream object. + + If `target_stream` is ``None``, defaults to opening the + operating system's null device and using its file descriptor. + + """ + if target_stream is None: + target_fd = os.open(os.devnull, os.O_RDWR) + else: + target_fd = target_stream.fileno() + os.dup2(target_fd, system_stream.fileno()) + + +def make_default_signal_map(): + """ Make the default signal map for this system. + + :return: A mapping from signal number to handler object. + + The signals available differ by system. The map will not contain + any signals not defined on the running system. + + """ + name_map = { + 'SIGTSTP': None, + 'SIGTTIN': None, + 'SIGTTOU': None, + 'SIGTERM': 'terminate', + } + signal_map = dict( + (getattr(signal, name), target) + for (name, target) in name_map.items() + if hasattr(signal, name)) + + return signal_map + + +def set_signal_handlers(signal_handler_map): + """ Set the signal handlers as specified. + + :param signal_handler_map: A map from signal number to handler + object. + :return: ``None``. + + See the `signal` module for details on signal numbers and signal + handlers. + + """ + for (signal_number, handler) in signal_handler_map.items(): + signal.signal(signal_number, handler) + + +def register_atexit_function(func): + """ Register a function for processing at program exit. + + :param func: A callable function expecting no arguments. + :return: ``None``. + + The function `func` is registered for a call with no arguments + at program exit. + + """ + atexit.register(func) + + +def _chain_exception_from_existing_exception_context(exc, as_cause=False): + """ Decorate the specified exception with the existing exception context. + + :param exc: The exception instance to decorate. + :param as_cause: If true, the existing context is declared to be + the cause of the exception. + :return: ``None``. + + :PEP:`344` describes syntax and attributes (`__traceback__`, + `__context__`, `__cause__`) for use in exception chaining. + + Python 2 does not have that syntax, so this function decorates + the exception with values from the current exception context. + + """ + (existing_exc_type, existing_exc, existing_traceback) = sys.exc_info() + if as_cause: + exc.__cause__ = existing_exc + else: + exc.__context__ = existing_exc + exc.__traceback__ = existing_traceback + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff --git a/daemon/daemon.pyc b/daemon/daemon.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9fc3599b50238247b72c1a04142a46a5fe66d788 GIT binary patch literal 31631 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 z +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Lockfile behaviour implemented via Unix PID files. + """ + +from __future__ import (absolute_import, unicode_literals) + +from lockfile.pidlockfile import PIDLockFile + + +class TimeoutPIDLockFile(PIDLockFile, object): + """ Lockfile with default timeout, implemented as a Unix PID file. + + This uses the ``PIDLockFile`` implementation, with the + following changes: + + * The `acquire_timeout` parameter to the initialiser will be + used as the default `timeout` parameter for the `acquire` + method. + + """ + + def __init__(self, path, acquire_timeout=None, *args, **kwargs): + """ Set up the parameters of a TimeoutPIDLockFile. + + :param path: Filesystem path to the PID file. + :param acquire_timeout: Value to use by default for the + `acquire` call. + :return: ``None``. + + """ + self.acquire_timeout = acquire_timeout + super(TimeoutPIDLockFile, self).__init__(path, *args, **kwargs) + + def acquire(self, timeout=None, *args, **kwargs): + """ Acquire the lock. + + :param timeout: Specifies the timeout; see below for valid + values. + :return: ``None``. + + The `timeout` defaults to the value set during + initialisation with the `acquire_timeout` parameter. It is + passed to `PIDLockFile.acquire`; see that method for + details. + + """ + if timeout is None: + timeout = self.acquire_timeout + super(TimeoutPIDLockFile, self).acquire(timeout, *args, **kwargs) + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff --git a/daemon/pidfile.pyc b/daemon/pidfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..176c408e9f0a2e6d08b7f1c49b81d74ac9d33eba GIT binary patch literal 2141 zcmb_d&5zqe6n}O;x*Jlc5L6t83ks-BBrc#7p#@cyRzd+e3lh256MMXw&Di65W_G*M za%#`~EA-Fe&XEi6jh)XTxU8jldGq3VpTFO@^Vh?{pMSi)NTGc?_V9~>U7eo(4AMy~g9=ri~4e;yY0c3rcj~<{m_caRk zgHG4ERDvzVieF3ZY9@PyCV)lP>!H_s5Y0zjeAS&{v|VvG(ymx0a_*F6wh8y-pipkuQKjR7uav62 zSV_a25r)~7U`eu=#Mmo&r!Jx;SM0iSq5Oe{yK!K4%`KBhq4R52Of@d4-fY@9$|A~nX7!{ zZDEG#9DDxm@b@yH=1=_zY=~V=8DYlU8Y>EK*feK@&IX)xik|l5QE#X07p|P3qu4dJ zyxlgeZO&dx8b6!qBrN4h9mS}Hb#*yKRLpcKlH@4+nTKOTlEWEW>a1~J|rEemVYX zVvvhZE1s@7qHvONQRs3~$&8piu5Kgp_$e+!f#CEsNQr>7q2(b!{&CbuzV!*zpAZ(V z0{AU}5>pGoA_VZRK}2*tA`l>*BSgF+YJS%=!$SfyT_3fr9bJpyDVtX!mAPyg*qWa) zBk1Bnaxt3SHPRz{XL@LLn`^6?CnDp2rsg@hrB6#JWLHB)?-65Lmbn^feGqcXQ-2$5 zFN6Q0_?VqrCe4AHiW{OdM%=$`#x0hUrgXdFmNm4c1{^rZgymAR4xQ)nQy=X4Y=#<_aptvu{4bj(AT!yd*4~M}pn2ktp2~pxT7{``E#&MxD zr)Wzl#c`(7IF9I(;*lGXuU%gEzy~7c$GCiQ{Xst*4rikiibr3#HmTg<4S-2SXoARp u4V9~>U7eo(4AMy~g9=ri~4e;yY0c3rcj~<{m_caRk zgHG4ERDvzVieF3ZY9@PyCV)lP>!H_s5Y0zjeAS&{v|VvG(ymx0a_*F6wh8y-pipkuQKjR7uav62 zSV_a25r)~7U`eu=#Mmo&r!Jx;SM0iSq5Oe{yK!K4%`KBhq4R52Of@d4-fY@9$|A~nX7!{ zZDEG#9DDxm@b@yH=1=_zY=~V=8DYlU8Y>EK*feK@&IX)xik|l5QE#X07p|P3qu4dJ zyxlgeZO&dx8b6!qBrN4h9mS}Hb#*yKRLpcKlH@4+nTKOTlEWEW>a1~J|rEemVYX zVvvhZE1s@7qHvONQRs3~$&8piu5Kgp_$e+!f#CEsNQr>7q2(b!{&CbuzV!*zpAZ(V z0{AU}5>pGoA_VZRK}2*tA`l>*BSgF+YJS%=!$SfyT_3fr9bJpyDVtX!mAPyg*qWa) zBk1Bnaxt3SHPRz{XL@LLn`^6?CnDp2rsg@hrB6#JWLHB)?-65Lmbn^feGqcXQ-2$5 zFN6Q0_?VqrCe4AHiW{OdM%=$`#x0hUrgXdFmNm4c1{^rZgymAR4xQ)nQy=X4Y=#<_aptvu{4bj(AT!yd*4~M}pn2ktp2~pxT7{``E#&MxD zr)Wzl#c`(7IF9I(;*lGXuU%gEzy~7c$GCiQ{Xst*4rikiibr3#HmTg<4S-2SXoARp u4 +# Copyright © 2007–2008 Robert Niederreiter, Jens Klein +# Copyright © 2003 Clark Evans +# Copyright © 2002 Noah Spurrier +# Copyright © 2001 Jürgen Hermann +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Apache License, version 2.0 as published by the +# Apache Software Foundation. +# No warranty expressed or implied. See the file ‘LICENSE.ASF-2’ for details. + +""" Daemon runner library. + """ + +from __future__ import (absolute_import, unicode_literals) + +import sys +import os +import signal +import errno +try: + # Python 3 standard library. + ProcessLookupError +except NameError: + # No such class in Python 2. + ProcessLookupError = NotImplemented + +import lockfile + +from . import pidfile +from .daemon import (basestring, unicode) +from .daemon import DaemonContext +from .daemon import _chain_exception_from_existing_exception_context + + +class DaemonRunnerError(Exception): + """ Abstract base class for errors from DaemonRunner. """ + + def __init__(self, *args, **kwargs): + self._chain_from_context() + + super(DaemonRunnerError, self).__init__(*args, **kwargs) + + def _chain_from_context(self): + _chain_exception_from_existing_exception_context(self, as_cause=True) + + +class DaemonRunnerInvalidActionError(DaemonRunnerError, ValueError): + """ Raised when specified action for DaemonRunner is invalid. """ + + def _chain_from_context(self): + # This exception is normally not caused by another. + _chain_exception_from_existing_exception_context(self, as_cause=False) + + +class DaemonRunnerStartFailureError(DaemonRunnerError, RuntimeError): + """ Raised when failure starting DaemonRunner. """ + + +class DaemonRunnerStopFailureError(DaemonRunnerError, RuntimeError): + """ Raised when failure stopping DaemonRunner. """ + + +class DaemonRunner: + """ Controller for a callable running in a separate background process. + + The first command-line argument is the action to take: + + * 'start': Become a daemon and call `app.run()`. + * 'stop': Exit the daemon process specified in the PID file. + * 'restart': Stop, then start. + + """ + + __metaclass__ = type + + start_message = "started with pid {pid:d}" + + def __init__(self, app): + """ Set up the parameters of a new runner. + + :param app: The application instance; see below. + :return: ``None``. + + The `app` argument must have the following attributes: + + * `stdin_path`, `stdout_path`, `stderr_path`: Filesystem paths + to open and replace the existing `sys.stdin`, `sys.stdout`, + `sys.stderr`. + + * `pidfile_path`: Absolute filesystem path to a file that will + be used as the PID file for the daemon. If ``None``, no PID + file will be used. + + * `pidfile_timeout`: Used as the default acquisition timeout + value supplied to the runner's PID lock file. + + * `run`: Callable that will be invoked when the daemon is + started. + + """ + self.parse_args() + self.app = app + self.daemon_context = DaemonContext() + self.daemon_context.stdin = open(app.stdin_path, 'rt') + self.daemon_context.stdout = open(app.stdout_path, 'w+t') + self.daemon_context.stderr = open( + app.stderr_path, 'w+t', buffering=0) + + self.pidfile = None + if app.pidfile_path is not None: + self.pidfile = make_pidlockfile( + app.pidfile_path, app.pidfile_timeout) + self.daemon_context.pidfile = self.pidfile + + def _usage_exit(self, argv): + """ Emit a usage message, then exit. + + :param argv: The command-line arguments used to invoke the + program, as a sequence of strings. + :return: ``None``. + + """ + progname = os.path.basename(argv[0]) + usage_exit_code = 2 + action_usage = "|".join(self.action_funcs.keys()) + message = "usage: {progname} {usage}".format( + progname=progname, usage=action_usage) + emit_message(message) + sys.exit(usage_exit_code) + + def parse_args(self, argv=None): + """ Parse command-line arguments. + + :param argv: The command-line arguments used to invoke the + program, as a sequence of strings. + + :return: ``None``. + + The parser expects the first argument as the program name, the + second argument as the action to perform. + + If the parser fails to parse the arguments, emit a usage + message and exit the program. + + """ + if argv is None: + argv = sys.argv + + min_args = 2 + if len(argv) < min_args: + self._usage_exit(argv) + + self.action = unicode(argv[1]) + if self.action not in self.action_funcs: + self._usage_exit(argv) + + def _start(self): + """ Open the daemon context and run the application. + + :return: ``None``. + :raises DaemonRunnerStartFailureError: If the PID file cannot + be locked by this process. + + """ + if is_pidfile_stale(self.pidfile): + self.pidfile.break_lock() + + try: + self.daemon_context.open() + except lockfile.AlreadyLocked: + error = DaemonRunnerStartFailureError( + "PID file {pidfile.path!r} already locked".format( + pidfile=self.pidfile)) + raise error + + pid = os.getpid() + message = self.start_message.format(pid=pid) + emit_message(message) + + self.app.run() + + def _terminate_daemon_process(self): + """ Terminate the daemon process specified in the current PID file. + + :return: ``None``. + :raises DaemonRunnerStopFailureError: If terminating the daemon + fails with an OS error. + + """ + pid = self.pidfile.read_pid() + try: + os.kill(pid, signal.SIGTERM) + except OSError as exc: + error = DaemonRunnerStopFailureError( + "Failed to terminate {pid:d}: {exc}".format( + pid=pid, exc=exc)) + raise error + + def _stop(self): + """ Exit the daemon process specified in the current PID file. + + :return: ``None``. + :raises DaemonRunnerStopFailureError: If the PID file is not + already locked. + + """ + if not self.pidfile.is_locked(): + error = DaemonRunnerStopFailureError( + "PID file {pidfile.path!r} not locked".format( + pidfile=self.pidfile)) + raise error + + if is_pidfile_stale(self.pidfile): + self.pidfile.break_lock() + else: + self._terminate_daemon_process() + + def _restart(self): + """ Stop, then start. + """ + self._stop() + self._start() + + action_funcs = { + 'start': _start, + 'stop': _stop, + 'restart': _restart, + } + + def _get_action_func(self): + """ Get the function for the specified action. + + :return: The function object corresponding to the specified + action. + :raises DaemonRunnerInvalidActionError: if the action is + unknown. + + The action is specified by the `action` attribute, which is set + during `parse_args`. + + """ + try: + func = self.action_funcs[self.action] + except KeyError: + error = DaemonRunnerInvalidActionError( + "Unknown action: {action!r}".format( + action=self.action)) + raise error + return func + + def do_action(self): + """ Perform the requested action. + + :return: ``None``. + + The action is specified by the `action` attribute, which is set + during `parse_args`. + + """ + func = self._get_action_func() + func(self) + + +def emit_message(message, stream=None): + """ Emit a message to the specified stream (default `sys.stderr`). """ + if stream is None: + stream = sys.stderr + stream.write("{message}\n".format(message=message)) + stream.flush() + + +def make_pidlockfile(path, acquire_timeout): + """ Make a PIDLockFile instance with the given filesystem path. """ + if not isinstance(path, basestring): + error = ValueError("Not a filesystem path: {path!r}".format( + path=path)) + raise error + if not os.path.isabs(path): + error = ValueError("Not an absolute path: {path!r}".format( + path=path)) + raise error + lockfile = pidfile.TimeoutPIDLockFile(path, acquire_timeout) + + return lockfile + + +def is_pidfile_stale(pidfile): + """ Determine whether a PID file is stale. + + :return: ``True`` iff the PID file is stale; otherwise ``False``. + + The PID file is “stale” if its contents are valid but do not + match the PID of a currently-running process. + + """ + result = False + + pidfile_pid = pidfile.read_pid() + if pidfile_pid is not None: + try: + os.kill(pidfile_pid, signal.SIG_DFL) + except ProcessLookupError: + # The specified PID does not exist. + result = True + except OSError as exc: + if exc.errno == errno.ESRCH: + # Under Python 2, process lookup error is an OSError. + # The specified PID does not exist. + result = True + + return result + + +# Local variables: +# coding: utf-8 +# mode: python +# End: +# vim: fileencoding=utf-8 filetype=python : diff --git a/daemon/runner.pyc b/daemon/runner.pyc new file mode 100644 index 0000000000000000000000000000000000000000..70c6cbd8623e5385ee9140e9b33f92b718743bcf GIT binary patch literal 10977 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 literal 0 HcmV?d00001 diff --git a/daemon/runner.pyo b/daemon/runner.pyo new file mode 100644 index 0000000000000000000000000000000000000000..70c6cbd8623e5385ee9140e9b33f92b718743bcf GIT binary patch literal 10977 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 literal 0 HcmV?d00001 diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..5be0475 --- /dev/null +++ b/install.sh @@ -0,0 +1,5 @@ +#!/bin/sh + + +mkdir $HOME/bin +cp -rp hbd hbdclass.py hbc daemon lockfile $HOME/bin/ diff --git a/lockfile/__init__.py b/lockfile/__init__.py new file mode 100644 index 0000000..a6f44a5 --- /dev/null +++ b/lockfile/__init__.py @@ -0,0 +1,347 @@ +# -*- coding: utf-8 -*- + +""" +lockfile.py - Platform-independent advisory file locks. + +Requires Python 2.5 unless you apply 2.4.diff +Locking is done on a per-thread basis instead of a per-process basis. + +Usage: + +>>> lock = LockFile('somefile') +>>> try: +... lock.acquire() +... except AlreadyLocked: +... print 'somefile', 'is locked already.' +... except LockFailed: +... print 'somefile', 'can\\'t be locked.' +... else: +... print 'got lock' +got lock +>>> print lock.is_locked() +True +>>> lock.release() + +>>> lock = LockFile('somefile') +>>> print lock.is_locked() +False +>>> with lock: +... print lock.is_locked() +True +>>> print lock.is_locked() +False + +>>> lock = LockFile('somefile') +>>> # It is okay to lock twice from the same thread... +>>> with lock: +... lock.acquire() +... +>>> # Though no counter is kept, so you can't unlock multiple times... +>>> print lock.is_locked() +False + +Exceptions: + + Error - base class for other exceptions + LockError - base class for all locking exceptions + AlreadyLocked - Another thread or process already holds the lock + LockFailed - Lock failed for some other reason + UnlockError - base class for all unlocking exceptions + AlreadyUnlocked - File was not locked. + NotMyLock - File was locked but not by the current thread/process +""" + +from __future__ import absolute_import + +import functools +import os +import socket +import threading +import warnings + +# Work with PEP8 and non-PEP8 versions of threading module. +if not hasattr(threading, "current_thread"): + threading.current_thread = threading.currentThread +if not hasattr(threading.Thread, "get_name"): + threading.Thread.get_name = threading.Thread.getName + +__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked', + 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock', + 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock', + 'LockBase', 'locked'] + + +class Error(Exception): + """ + Base class for other exceptions. + + >>> try: + ... raise Error + ... except Exception: + ... pass + """ + pass + + +class LockError(Error): + """ + Base class for error arising from attempts to acquire the lock. + + >>> try: + ... raise LockError + ... except Error: + ... pass + """ + pass + + +class LockTimeout(LockError): + """Raised when lock creation fails within a user-defined period of time. + + >>> try: + ... raise LockTimeout + ... except LockError: + ... pass + """ + pass + + +class AlreadyLocked(LockError): + """Some other thread/process is locking the file. + + >>> try: + ... raise AlreadyLocked + ... except LockError: + ... pass + """ + pass + + +class LockFailed(LockError): + """Lock file creation failed for some other reason. + + >>> try: + ... raise LockFailed + ... except LockError: + ... pass + """ + pass + + +class UnlockError(Error): + """ + Base class for errors arising from attempts to release the lock. + + >>> try: + ... raise UnlockError + ... except Error: + ... pass + """ + pass + + +class NotLocked(UnlockError): + """Raised when an attempt is made to unlock an unlocked file. + + >>> try: + ... raise NotLocked + ... except UnlockError: + ... pass + """ + pass + + +class NotMyLock(UnlockError): + """Raised when an attempt is made to unlock a file someone else locked. + + >>> try: + ... raise NotMyLock + ... except UnlockError: + ... pass + """ + pass + + +class _SharedBase(object): + def __init__(self, path): + self.path = path + + def acquire(self, timeout=None): + """ + Acquire the lock. + + * If timeout is omitted (or None), wait forever trying to lock the + file. + + * If timeout > 0, try to acquire the lock for that many seconds. If + the lock period expires and the file is still locked, raise + LockTimeout. + + * If timeout <= 0, raise AlreadyLocked immediately if the file is + already locked. + """ + raise NotImplemented("implement in subclass") + + def release(self): + """ + Release the lock. + + If the file is not locked, raise NotLocked. + """ + raise NotImplemented("implement in subclass") + + def __enter__(self): + """ + Context manager support. + """ + self.acquire() + return self + + def __exit__(self, *_exc): + """ + Context manager support. + """ + self.release() + + def __repr__(self): + return "<%s: %r>" % (self.__class__.__name__, self.path) + + +class LockBase(_SharedBase): + """Base class for platform-specific lock classes.""" + def __init__(self, path, threaded=True, timeout=None): + """ + >>> lock = LockBase('somefile') + >>> lock = LockBase('somefile', threaded=False) + """ + super(LockBase, self).__init__(path) + self.lock_file = os.path.abspath(path) + ".lock" + self.hostname = socket.gethostname() + self.pid = os.getpid() + if threaded: + t = threading.current_thread() + # Thread objects in Python 2.4 and earlier do not have ident + # attrs. Worm around that. + ident = getattr(t, "ident", hash(t)) + self.tname = "-%x" % (ident & 0xffffffff) + else: + self.tname = "" + dirname = os.path.dirname(self.lock_file) + + # unique name is mostly about the current process, but must + # also contain the path -- otherwise, two adjacent locked + # files conflict (one file gets locked, creating lock-file and + # unique file, the other one gets locked, creating lock-file + # and overwriting the already existing lock-file, then one + # gets unlocked, deleting both lock-file and unique file, + # finally the last lock errors out upon releasing. + self.unique_name = os.path.join(dirname, + "%s%s.%s%s" % (self.hostname, + self.tname, + self.pid, + hash(self.path))) + self.timeout = timeout + + def is_locked(self): + """ + Tell whether or not the file is locked. + """ + raise NotImplemented("implement in subclass") + + def i_am_locking(self): + """ + Return True if this object is locking the file. + """ + raise NotImplemented("implement in subclass") + + def break_lock(self): + """ + Remove a lock. Useful if a locking thread failed to unlock. + """ + raise NotImplemented("implement in subclass") + + def __repr__(self): + return "<%s: %r -- %r>" % (self.__class__.__name__, self.unique_name, + self.path) + + +def _fl_helper(cls, mod, *args, **kwds): + warnings.warn("Import from %s module instead of lockfile package" % mod, + DeprecationWarning, stacklevel=2) + # This is a bit funky, but it's only for awhile. The way the unit tests + # are constructed this function winds up as an unbound method, so it + # actually takes three args, not two. We want to toss out self. + if not isinstance(args[0], str): + # We are testing, avoid the first arg + args = args[1:] + if len(args) == 1 and not kwds: + kwds["threaded"] = True + return cls(*args, **kwds) + + +def LinkFileLock(*args, **kwds): + """Factory function provided for backwards compatibility. + + Do not use in new code. Instead, import LinkLockFile from the + lockfile.linklockfile module. + """ + from . import linklockfile + return _fl_helper(linklockfile.LinkLockFile, "lockfile.linklockfile", + *args, **kwds) + + +def MkdirFileLock(*args, **kwds): + """Factory function provided for backwards compatibility. + + Do not use in new code. Instead, import MkdirLockFile from the + lockfile.mkdirlockfile module. + """ + from . import mkdirlockfile + return _fl_helper(mkdirlockfile.MkdirLockFile, "lockfile.mkdirlockfile", + *args, **kwds) + + +def SQLiteFileLock(*args, **kwds): + """Factory function provided for backwards compatibility. + + Do not use in new code. Instead, import SQLiteLockFile from the + lockfile.mkdirlockfile module. + """ + from . import sqlitelockfile + return _fl_helper(sqlitelockfile.SQLiteLockFile, "lockfile.sqlitelockfile", + *args, **kwds) + + +def locked(path, timeout=None): + """Decorator which enables locks for decorated function. + + Arguments: + - path: path for lockfile. + - timeout (optional): Timeout for acquiring lock. + + Usage: + @locked('/var/run/myname', timeout=0) + def myname(...): + ... + """ + def decor(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + lock = FileLock(path, timeout=timeout) + lock.acquire() + try: + return func(*args, **kwargs) + finally: + lock.release() + return wrapper + return decor + + +if hasattr(os, "link"): + from . import linklockfile as _llf + LockFile = _llf.LinkLockFile +else: + from . import mkdirlockfile as _mlf + LockFile = _mlf.MkdirLockFile + +FileLock = LockFile diff --git a/lockfile/__init__.pyc b/lockfile/__init__.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f57306ebeeb8810a18ed3a1527634ec1bf6f246 GIT binary patch literal 11938 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%0AAudr zo2UL0v88CqJOMKc02h%MO#^5~c-~RFt4%-H#@A}%g1PT+LN|koGFP3sR=kj28#3*P z?{~x>U4;S5y6aL(`n9BBF`g$IkqI791qbojb)6?pWr0f@(#_-2BA1$f-B$R#dV_;V zWG=7W2-3B5*k9}IF@9P`Ea|eDufH-1BQ@T*wVSYlPbbdi$QID}trF`JwUBZ&7$-bQ2~sP#P9fsK7{8u08iu& z%-p{pQvjZuD~QmyLvbRIQQ+uz(8@u(5Rlvzt55s#4yZ87){Nv$;#V58++>qlGAfpw zJQRP?K?FecsNtc6xVtGnZ8YguD3f#%Rpq$X@T%Uiie&2~G8Xws68ILh{kdqk%`o!RNTOp?lXCi%BJogpGce7i#=mH#e_msl*bILBfgg|Gyf2FW^5 zSLB~#U6ovmOH9yOyLOSI*u*9Bd1mQoweS@kJ`!TETYhLH-}U$;pHGVV1me%Im}SAa zKG`oq=ptCj^tmL=MGn(l@M(K|BIZH{QfB1E2w7S&9>Ff5Uj&}QPN5f~xbYswzruB2 zL*e*^imN>;t7OmDoU4BR3xH~gNv*O_nS*yDKUHQxj(aDir;bd>;v9aC$dCKGy0~6{ TqJD%wbEl4a@QW%7UcK@+s#Ok+ literal 0 HcmV?d00001 diff --git a/lockfile/__init__.pyo b/lockfile/__init__.pyo new file mode 100644 index 0000000000000000000000000000000000000000..2f57306ebeeb8810a18ed3a1527634ec1bf6f246 GIT binary patch literal 11938 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%0AAudr zo2UL0v88CqJOMKc02h%MO#^5~c-~RFt4%-H#@A}%g1PT+LN|koGFP3sR=kj28#3*P z?{~x>U4;S5y6aL(`n9BBF`g$IkqI791qbojb)6?pWr0f@(#_-2BA1$f-B$R#dV_;V zWG=7W2-3B5*k9}IF@9P`Ea|eDufH-1BQ@T*wVSYlPbbdi$QID}trF`JwUBZ&7$-bQ2~sP#P9fsK7{8u08iu& z%-p{pQvjZuD~QmyLvbRIQQ+uz(8@u(5Rlvzt55s#4yZ87){Nv$;#V58++>qlGAfpw zJQRP?K?FecsNtc6xVtGnZ8YguD3f#%Rpq$X@T%Uiie&2~G8Xws68ILh{kdqk%`o!RNTOp?lXCi%BJogpGce7i#=mH#e_msl*bILBfgg|Gyf2FW^5 zSLB~#U6ovmOH9yOyLOSI*u*9Bd1mQoweS@kJ`!TETYhLH-}U$;pHGVV1me%Im}SAa zKG`oq=ptCj^tmL=MGn(l@M(K|BIZH{QfB1E2w7S&9>Ff5Uj&}QPN5f~xbYswzruB2 zL*e*^imN>;t7OmDoU4BR3xH~gNv*O_nS*yDKUHQxj(aDir;bd>;v9aC$dCKGy0~6{ TqJD%wbEl4a@QW%7UcK@+s#Ok+ literal 0 HcmV?d00001 diff --git a/lockfile/linklockfile.py b/lockfile/linklockfile.py new file mode 100644 index 0000000..2ca9be0 --- /dev/null +++ b/lockfile/linklockfile.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import + +import time +import os + +from . import (LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, + AlreadyLocked) + + +class LinkLockFile(LockBase): + """Lock access to a file using atomic property of link(2). + + >>> lock = LinkLockFile('somefile') + >>> lock = LinkLockFile('somefile', threaded=False) + """ + + def acquire(self, timeout=None): + try: + open(self.unique_name, "wb").close() + except IOError: + raise LockFailed("failed to create %s" % self.unique_name) + + timeout = timeout if timeout is not None else self.timeout + end_time = time.time() + if timeout is not None and timeout > 0: + end_time += timeout + + while True: + # Try and create a hard link to it. + try: + os.link(self.unique_name, self.lock_file) + except OSError: + # Link creation failed. Maybe we've double-locked? + nlinks = os.stat(self.unique_name).st_nlink + if nlinks == 2: + # The original link plus the one I created == 2. We're + # good to go. + return + else: + # Otherwise the lock creation failed. + if timeout is not None and time.time() > end_time: + os.unlink(self.unique_name) + if timeout > 0: + raise LockTimeout("Timeout waiting to acquire" + " lock for %s" % + self.path) + else: + raise AlreadyLocked("%s is already locked" % + self.path) + time.sleep(timeout is not None and timeout / 10 or 0.1) + else: + # Link creation succeeded. We're good to go. + return + + def release(self): + if not self.is_locked(): + raise NotLocked("%s is not locked" % self.path) + elif not os.path.exists(self.unique_name): + raise NotMyLock("%s is locked, but not by me" % self.path) + os.unlink(self.unique_name) + os.unlink(self.lock_file) + + def is_locked(self): + return os.path.exists(self.lock_file) + + def i_am_locking(self): + return (self.is_locked() and + os.path.exists(self.unique_name) and + os.stat(self.unique_name).st_nlink == 2) + + def break_lock(self): + if os.path.exists(self.lock_file): + os.unlink(self.lock_file) diff --git a/lockfile/linklockfile.pyc b/lockfile/linklockfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c8b773343352746418d0e4977905e03c0bbbf6a7 GIT binary patch literal 2867 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 lock = MkdirLockFile('somefile') + >>> lock = MkdirLockFile('somefile', threaded=False) + """ + LockBase.__init__(self, path, threaded, timeout) + # Lock file itself is a directory. Place the unique file name into + # it. + self.unique_name = os.path.join(self.lock_file, + "%s.%s%s" % (self.hostname, + self.tname, + self.pid)) + + def acquire(self, timeout=None): + timeout = timeout if timeout is not None else self.timeout + end_time = time.time() + if timeout is not None and timeout > 0: + end_time += timeout + + if timeout is None: + wait = 0.1 + else: + wait = max(0, timeout / 10) + + while True: + try: + os.mkdir(self.lock_file) + except OSError: + err = sys.exc_info()[1] + if err.errno == errno.EEXIST: + # Already locked. + if os.path.exists(self.unique_name): + # Already locked by me. + return + if timeout is not None and time.time() > end_time: + if timeout > 0: + raise LockTimeout("Timeout waiting to acquire" + " lock for %s" % + self.path) + else: + # Someone else has the lock. + raise AlreadyLocked("%s is already locked" % + self.path) + time.sleep(wait) + else: + # Couldn't create the lock for some other reason + raise LockFailed("failed to create %s" % self.lock_file) + else: + open(self.unique_name, "wb").close() + return + + def release(self): + if not self.is_locked(): + raise NotLocked("%s is not locked" % self.path) + elif not os.path.exists(self.unique_name): + raise NotMyLock("%s is locked, but not by me" % self.path) + os.unlink(self.unique_name) + os.rmdir(self.lock_file) + + def is_locked(self): + return os.path.exists(self.lock_file) + + def i_am_locking(self): + return (self.is_locked() and + os.path.exists(self.unique_name)) + + def break_lock(self): + if os.path.exists(self.lock_file): + for name in os.listdir(self.lock_file): + os.unlink(os.path.join(self.lock_file, name)) + os.rmdir(self.lock_file) diff --git a/lockfile/mkdirlockfile.pyc b/lockfile/mkdirlockfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..09e8224d82e226108aeef8fe756a8c2d18ef6fca GIT binary patch literal 3378 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 +# +# This is free software: you may copy, modify, and/or distribute this work +# under the terms of the Python Software Foundation License, version 2 or +# later as published by the Python Software Foundation. +# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. + +""" Lockfile behaviour implemented via Unix PID files. + """ + +from __future__ import absolute_import + +import errno +import os +import time + +from . import (LockBase, AlreadyLocked, LockFailed, NotLocked, NotMyLock, + LockTimeout) + + +class PIDLockFile(LockBase): + """ Lockfile implemented as a Unix PID file. + + The lock file is a normal file named by the attribute `path`. + A lock's PID file contains a single line of text, containing + the process ID (PID) of the process that acquired the lock. + + >>> lock = PIDLockFile('somefile') + >>> lock = PIDLockFile('somefile') + """ + + def __init__(self, path, threaded=False, timeout=None): + # pid lockfiles don't support threaded operation, so always force + # False as the threaded arg. + LockBase.__init__(self, path, False, timeout) + self.unique_name = self.path + + def read_pid(self): + """ Get the PID from the lock file. + """ + return read_pid_from_pidfile(self.path) + + def is_locked(self): + """ Test if the lock is currently held. + + The lock is held if the PID file for this lock exists. + + """ + return os.path.exists(self.path) + + def i_am_locking(self): + """ Test if the lock is held by the current process. + + Returns ``True`` if the current process ID matches the + number stored in the PID file. + """ + return self.is_locked() and os.getpid() == self.read_pid() + + def acquire(self, timeout=None): + """ Acquire the lock. + + Creates the PID file for this lock, or raises an error if + the lock could not be acquired. + """ + + timeout = timeout if timeout is not None else self.timeout + end_time = time.time() + if timeout is not None and timeout > 0: + end_time += timeout + + while True: + try: + write_pid_to_pidfile(self.path) + except OSError as exc: + if exc.errno == errno.EEXIST: + # The lock creation failed. Maybe sleep a bit. + if time.time() > end_time: + if timeout is not None and timeout > 0: + raise LockTimeout("Timeout waiting to acquire" + " lock for %s" % + self.path) + else: + raise AlreadyLocked("%s is already locked" % + self.path) + time.sleep(timeout is not None and timeout / 10 or 0.1) + else: + raise LockFailed("failed to create %s" % self.path) + else: + return + + def release(self): + """ Release the lock. + + Removes the PID file to release the lock, or raises an + error if the current process does not hold the lock. + + """ + if not self.is_locked(): + raise NotLocked("%s is not locked" % self.path) + if not self.i_am_locking(): + raise NotMyLock("%s is locked, but not by me" % self.path) + remove_existing_pidfile(self.path) + + def break_lock(self): + """ Break an existing lock. + + Removes the PID file if it already exists, otherwise does + nothing. + + """ + remove_existing_pidfile(self.path) + + +def read_pid_from_pidfile(pidfile_path): + """ Read the PID recorded in the named PID file. + + Read and return the numeric PID recorded as text in the named + PID file. If the PID file cannot be read, or if the content is + not a valid PID, return ``None``. + + """ + pid = None + try: + pidfile = open(pidfile_path, 'r') + except IOError: + pass + else: + # According to the FHS 2.3 section on PID files in /var/run: + # + # The file must consist of the process identifier in + # ASCII-encoded decimal, followed by a newline character. + # + # Programs that read PID files should be somewhat flexible + # in what they accept; i.e., they should ignore extra + # whitespace, leading zeroes, absence of the trailing + # newline, or additional lines in the PID file. + + line = pidfile.readline().strip() + try: + pid = int(line) + except ValueError: + pass + pidfile.close() + + return pid + + +def write_pid_to_pidfile(pidfile_path): + """ Write the PID in the named PID file. + + Get the numeric process ID (“PID”) of the current process + and write it to the named file as a line of text. + + """ + open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY) + open_mode = 0o644 + pidfile_fd = os.open(pidfile_path, open_flags, open_mode) + pidfile = os.fdopen(pidfile_fd, 'w') + + # According to the FHS 2.3 section on PID files in /var/run: + # + # The file must consist of the process identifier in + # ASCII-encoded decimal, followed by a newline character. For + # example, if crond was process number 25, /var/run/crond.pid + # would contain three characters: two, five, and newline. + + pid = os.getpid() + pidfile.write("%s\n" % pid) + pidfile.close() + + +def remove_existing_pidfile(pidfile_path): + """ Remove the named PID file if it exists. + + Removing a PID file that doesn't already exist puts us in the + desired state, so we ignore the condition if the file does not + exist. + + """ + try: + os.remove(pidfile_path) + except OSError as exc: + if exc.errno == errno.ENOENT: + pass + else: + raise diff --git a/lockfile/pidlockfile.pyc b/lockfile/pidlockfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e09f659a6ab68460a6b89b816bfe1bd5c67feb8c GIT binary patch literal 5837 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 lock = SQLiteLockFile('somefile') + >>> lock = SQLiteLockFile('somefile', threaded=False) + """ + LockBase.__init__(self, path, threaded, timeout) + self.lock_file = unicode(self.lock_file) + self.unique_name = unicode(self.unique_name) + + if SQLiteLockFile.testdb is None: + import tempfile + _fd, testdb = tempfile.mkstemp() + os.close(_fd) + os.unlink(testdb) + del _fd, tempfile + SQLiteLockFile.testdb = testdb + + import sqlite3 + self.connection = sqlite3.connect(SQLiteLockFile.testdb) + + c = self.connection.cursor() + try: + c.execute("create table locks" + "(" + " lock_file varchar(32)," + " unique_name varchar(32)" + ")") + except sqlite3.OperationalError: + pass + else: + self.connection.commit() + import atexit + atexit.register(os.unlink, SQLiteLockFile.testdb) + + def acquire(self, timeout=None): + timeout = timeout if timeout is not None else self.timeout + end_time = time.time() + if timeout is not None and timeout > 0: + end_time += timeout + + if timeout is None: + wait = 0.1 + elif timeout <= 0: + wait = 0 + else: + wait = timeout / 10 + + cursor = self.connection.cursor() + + while True: + if not self.is_locked(): + # Not locked. Try to lock it. + cursor.execute("insert into locks" + " (lock_file, unique_name)" + " values" + " (?, ?)", + (self.lock_file, self.unique_name)) + self.connection.commit() + + # Check to see if we are the only lock holder. + cursor.execute("select * from locks" + " where unique_name = ?", + (self.unique_name,)) + rows = cursor.fetchall() + if len(rows) > 1: + # Nope. Someone else got there. Remove our lock. + cursor.execute("delete from locks" + " where unique_name = ?", + (self.unique_name,)) + self.connection.commit() + else: + # Yup. We're done, so go home. + return + else: + # Check to see if we are the only lock holder. + cursor.execute("select * from locks" + " where unique_name = ?", + (self.unique_name,)) + rows = cursor.fetchall() + if len(rows) == 1: + # We're the locker, so go home. + return + + # Maybe we should wait a bit longer. + if timeout is not None and time.time() > end_time: + if timeout > 0: + # No more waiting. + raise LockTimeout("Timeout waiting to acquire" + " lock for %s" % + self.path) + else: + # Someone else has the lock and we are impatient.. + raise AlreadyLocked("%s is already locked" % self.path) + + # Well, okay. We'll give it a bit longer. + time.sleep(wait) + + def release(self): + if not self.is_locked(): + raise NotLocked("%s is not locked" % self.path) + if not self.i_am_locking(): + raise NotMyLock("%s is locked, but not by me (by %s)" % + (self.unique_name, self._who_is_locking())) + cursor = self.connection.cursor() + cursor.execute("delete from locks" + " where unique_name = ?", + (self.unique_name,)) + self.connection.commit() + + def _who_is_locking(self): + cursor = self.connection.cursor() + cursor.execute("select unique_name from locks" + " where lock_file = ?", + (self.lock_file,)) + return cursor.fetchone()[0] + + def is_locked(self): + cursor = self.connection.cursor() + cursor.execute("select * from locks" + " where lock_file = ?", + (self.lock_file,)) + rows = cursor.fetchall() + return not not rows + + def i_am_locking(self): + cursor = self.connection.cursor() + cursor.execute("select * from locks" + " where lock_file = ?" + " and unique_name = ?", + (self.lock_file, self.unique_name)) + return not not cursor.fetchall() + + def break_lock(self): + cursor = self.connection.cursor() + cursor.execute("delete from locks" + " where lock_file = ?", + (self.lock_file,)) + self.connection.commit() diff --git a/lockfile/sqlitelockfile.pyc b/lockfile/sqlitelockfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc779017e94e4092c24ac31490a00a7de75fdf82 GIT binary patch literal 4649 zcmcgw-EJGl6+W}Oq)3XCWZ71lCQTMjT~nzn1vXk2NMki|n%=ldyGrYn2(VglN7j=2 zBkc@r3y^}MG1|)l=|#~#K;NN29w9e1(&lh}&YU^(%{kwh zk^8S+=imSN)vt$Ce;WAx$qteE6qOkN6{SR(qJg5!p@Bn+dmTz$8n_~Xx<><#j#{J} zr0(S}kf)n8?ojzYDW&ur4H}dy`Y^@>kJ1)pZ5p&G>(HP>**p#ADeKapOIeQwJ*?0m z-KIT7ag!bseM0n`!2&P3-JgS53BJ^KP}oHj(XJ`diPd3}9TcUtE%e37gTy37zLhAJ ze}8JqEmU1az?30Y*Kkv$})s?5D6S$?vZZJ&1JhWf1P{@El-b ziAIW!iH=b^bnMb69x00&2GEeP(51S^oA!8Q8#IF94vkw>{)xB3-q4~GkB&VWwfVR{ za_FQ%Crz>)8qL#jgQqKQbm_P$jgK4jF=jxdL1B|}aW&S|qmwy0o}qL{Ww zqa|M9q(w&;=(t6r3k)2qzn5Pkk0&mR@BNc~FMoqR{0>^5j-Bio^rVoSoY(rkse-M?!{iy8Uys^55fk~bmPIQ<@*}0+B{t9#j z5;7bfVVLBJ4MWS}ggL_^u0mp_Sc2e;wtyvVlM8KZylWYuts>VFR#s;RY>w#5#)jMQ z9HPSDc(IGLK+N&%NuDP8SmexM3K(xneOTnV9$I(^gsVEDS7r<+r9oKp`udR`BGBy; z>hB+DI6Q=+^rvMB0oFb&vMjMu1!|8_<=vM0egc!qK5G}ljZXKZauC`5Y6taE$U3$P z{D8yUazKZBanQwYaFKhMQij2|QTFkz3iCE<8xvD*0KzEUNR!=-gCo0No zhh{B=fR8xZPh84hok}YzA217WXGc0+dI^n+NJd}|M5{-S-c)df@^i)gpa598u9z@D zeBc3ewxk5_!!*QUJ-;JiTMIU(BB~O>^m)27L zww?b-s0)^S-31&ftEs2(ea7rr!3ylygwGU@R)GFZA&r3f*_H)b0bl)2(IMt6QVb?K z+Y7u|(~^)fc)x6G1G?JM0U-$RQBAaS>xUWlnb}ra&Q&OrpXpA`^r>~r0zJ~?s zZ0bLZ5{ta#!_H`UI7zSo+v)EWrGM3!OAx(k{KWWCm2zcN$NdW!2sjl7ucDCXq+UYwe@pei32oHc zPRChvP;=VeMb&fL!Vb$0|1P<6v$p5H0-2_>1(&lh}&YU^(%{kwh zk^8S+=imSN)vt$Ce;WAx$qteE6qOkN6{SR(qJg5!p@Bn+dmTz$8n_~Xx<><#j#{J} zr0(S}kf)n8?ojzYDW&ur4H}dy`Y^@>kJ1)pZ5p&G>(HP>**p#ADeKapOIeQwJ*?0m z-KIT7ag!bseM0n`!2&P3-JgS53BJ^KP}oHj(XJ`diPd3}9TcUtE%e37gTy37zLhAJ ze}8JqEmU1az?30Y*Kkv$})s?5D6S$?vZZJ&1JhWf1P{@El-b ziAIW!iH=b^bnMb69x00&2GEeP(51S^oA!8Q8#IF94vkw>{)xB3-q4~GkB&VWwfVR{ za_FQ%Crz>)8qL#jgQqKQbm_P$jgK4jF=jxdL1B|}aW&S|qmwy0o}qL{Ww zqa|M9q(w&;=(t6r3k)2qzn5Pkk0&mR@BNc~FMoqR{0>^5j-Bio^rVoSoY(rkse-M?!{iy8Uys^55fk~bmPIQ<@*}0+B{t9#j z5;7bfVVLBJ4MWS}ggL_^u0mp_Sc2e;wtyvVlM8KZylWYuts>VFR#s;RY>w#5#)jMQ z9HPSDc(IGLK+N&%NuDP8SmexM3K(xneOTnV9$I(^gsVEDS7r<+r9oKp`udR`BGBy; z>hB+DI6Q=+^rvMB0oFb&vMjMu1!|8_<=vM0egc!qK5G}ljZXKZauC`5Y6taE$U3$P z{D8yUazKZBanQwYaFKhMQij2|QTFkz3iCE<8xvD*0KzEUNR!=-gCo0No zhh{B=fR8xZPh84hok}YzA217WXGc0+dI^n+NJd}|M5{-S-c)df@^i)gpa598u9z@D zeBc3ewxk5_!!*QUJ-;JiTMIU(BB~O>^m)27L zww?b-s0)^S-31&ftEs2(ea7rr!3ylygwGU@R)GFZA&r3f*_H)b0bl)2(IMt6QVb?K z+Y7u|(~^)fc)x6G1G?JM0U-$RQBAaS>xUWlnb}ra&Q&OrpXpA`^r>~r0zJ~?s zZ0bLZ5{ta#!_H`UI7zSo+v)EWrGM3!OAx(k{KWWCm2zcN$NdW!2sjl7ucDCXq+UYwe@pei32oHc zPRChvP;=VeMb&fL!Vb$0|1P<6v$p5H0-2_> 0: + end_time += timeout + + while True: + # Try and create a symbolic link to it. + try: + os.symlink(self.unique_name, self.lock_file) + except OSError: + # Link creation failed. Maybe we've double-locked? + if self.i_am_locking(): + # Linked to out unique name. Proceed. + return + else: + # Otherwise the lock creation failed. + if timeout is not None and time.time() > end_time: + if timeout > 0: + raise LockTimeout("Timeout waiting to acquire" + " lock for %s" % + self.path) + else: + raise AlreadyLocked("%s is already locked" % + self.path) + time.sleep(timeout / 10 if timeout is not None else 0.1) + else: + # Link creation succeeded. We're good to go. + return + + def release(self): + if not self.is_locked(): + raise NotLocked("%s is not locked" % self.path) + elif not self.i_am_locking(): + raise NotMyLock("%s is locked, but not by me" % self.path) + os.unlink(self.lock_file) + + def is_locked(self): + return os.path.islink(self.lock_file) + + def i_am_locking(self): + return (os.path.islink(self.lock_file) + and os.readlink(self.lock_file) == self.unique_name) + + def break_lock(self): + if os.path.islink(self.lock_file): # exists && link + os.unlink(self.lock_file) diff --git a/lockfile/symlinklockfile.pyc b/lockfile/symlinklockfile.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ae6bf0d5422df9c0c0401d3eefb6e953dc4e7b0 GIT binary patch literal 2777 zcmcguO>fjz6ur;$kxZDBw3G-!g)CZ%D?uW$D4Twyfp!IhvS~;Eczf_&uwQ z1e=C^*X5t|2Xx(E!7u2li%LD`dPqoNNotMld*8eEz31EyPwTJcrN96A{)dswj~4Em zn<8!x!WcgzxyaPW(1@WPNFK;A&^^d48Mb7MUt99FOgl2{$h0fNu1ptXxFFL-87|7S zC&M0|?}+Wlu90z9o{Joc{5V|VE4QNsxXW-#WN`6kaY=Vvnb+PX*>qM`evok|>Tr7z zm%HU?vXwgPdr$_Yr*6luw0j5C{h7JpgKTQc+WTeb-{h4|$GQ)DdfGmi=2XHjU19C7zVNrmZLT`>5h~w>u2;4PX z=rl}6BkNq~%PjNs<+rPZGRAq;#GRXQ_YU>DA5h z%qydHk?q$uDblHpXrgw`=DRU-q1o|INHX|kVtvDY1X1+jwjgcPuG+}UQJQb$+0Mr7 zz&|OAo9nkWT;}bKSvs1ekFBG{gq&>5*?6U|&kka~oGEpTLW0#`#K2j@M>JgkIIsiU zlb%!`P%gU4jFD$X$W_2fdL*651f1qvWK7}~%lJKWA^Q=GC<7mIm zDjSmZaJQ_&tImB0)2lAbT$nbkq`P*UEn(yFi{E(s{&n;oG_8z5S=jhUV=?IRgQ~VM zt4?cVp52%ot4&Og2@|i|_qV^TDolYswzDLiCfr7hF*C1a7c=Eeq2|`kB3406^TstD z*5rY;)`(gB~Hgk z#k?8#I+ouIyu6`{&8Sx>#MF@qzo`u-f;h$$e?(PTV5&Hz6(8H~6!R3bLN;+WliHkr z3&QKzeiLC8{THFZu+!<);A7jr4PU&5jZ=h#*(|aH0w>xJcc2}C8$>$B$Z(>cNqkai zo+Q(9T;~=lKV3;OE=NfczYjU)9})ea#TO{CAGIo!y7)5Gfjz6ur;$kxZDBw3G-!g)CZ%D?uW$D4Twyfp!IhvS~;Eczf_&uwQ z1e=C^*X5t|2Xx(E!7u2li%LD`dPqoNNotMld*8eEz31EyPwTJcrN96A{)dswj~4Em zn<8!x!WcgzxyaPW(1@WPNFK;A&^^d48Mb7MUt99FOgl2{$h0fNu1ptXxFFL-87|7S zC&M0|?}+Wlu90z9o{Joc{5V|VE4QNsxXW-#WN`6kaY=Vvnb+PX*>qM`evok|>Tr7z zm%HU?vXwgPdr$_Yr*6luw0j5C{h7JpgKTQc+WTeb-{h4|$GQ)DdfGmi=2XHjU19C7zVNrmZLT`>5h~w>u2;4PX z=rl}6BkNq~%PjNs<+rPZGRAq;#GRXQ_YU>DA5h z%qydHk?q$uDblHpXrgw`=DRU-q1o|INHX|kVtvDY1X1+jwjgcPuG+}UQJQb$+0Mr7 zz&|OAo9nkWT;}bKSvs1ekFBG{gq&>5*?6U|&kka~oGEpTLW0#`#K2j@M>JgkIIsiU zlb%!`P%gU4jFD$X$W_2fdL*651f1qvWK7}~%lJKWA^Q=GC<7mIm zDjSmZaJQ_&tImB0)2lAbT$nbkq`P*UEn(yFi{E(s{&n;oG_8z5S=jhUV=?IRgQ~VM zt4?cVp52%ot4&Og2@|i|_qV^TDolYswzDLiCfr7hF*C1a7c=Eeq2|`kB3406^TstD z*5rY;)`(gB~Hgk z#k?8#I+ouIyu6`{&8Sx>#MF@qzo`u-f;h$$e?(PTV5&Hz6(8H~6!R3bLN;+WliHkr z3&QKzeiLC8{THFZu+!<);A7jr4PU&5jZ=h#*(|aH0w>xJcc2}C8$>$B$Z(>cNqkai zo+Q(9T;~=lKV3;OE=NfczYjU)9})ea#TO{CAGIo!y7)5G