summaryrefslogtreecommitdiff
path: root/daemon
diff options
context:
space:
mode:
authorLorry Tar Creator <lorry-tar-importer@baserock.org>2010-03-02 09:35:54 +0000
committer <>2014-12-08 18:37:23 +0000
commit02378192d5bb4b16498d87ace57da425166426bf (patch)
tree2e940dd7284d31c7d32808d9c6635a57547363cb /daemon
downloadpython-daemon-02378192d5bb4b16498d87ace57da425166426bf.tar.gz
Imported from /home/lorry/working-area/delta_python-packages_python-daemon/python-daemon-1.5.5.tar.gz.python-daemon-1.5.5
Diffstat (limited to 'daemon')
-rw-r--r--daemon/__init__.py47
-rw-r--r--daemon/daemon.py776
-rw-r--r--daemon/pidlockfile.py194
-rw-r--r--daemon/runner.py229
-rw-r--r--daemon/version/__init__.py36
-rw-r--r--daemon/version/version_info.py23
6 files changed, 1305 insertions, 0 deletions
diff --git a/daemon/__init__.py b/daemon/__init__.py
new file mode 100644
index 0000000..d8dc171
--- /dev/null
+++ b/daemon/__init__.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+# daemon/__init__.py
+# Part of python-daemon, an implementation of PEP 3143.
+#
+# Copyright © 2009–2010 Ben Finney <ben+python@benfinney.id.au>
+# Copyright © 2006 Robert Niederreiter
+#
+# 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.
+
+""" 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.
+
+ """
+
+import version
+from daemon import DaemonContext
+
+
+_version = version.version
+_copyright = version.copyright
+_license = version.license
+_url = "http://pypi.python.org/pypi/python-daemon/"
diff --git a/daemon/daemon.py b/daemon/daemon.py
new file mode 100644
index 0000000..28db695
--- /dev/null
+++ b/daemon/daemon.py
@@ -0,0 +1,776 @@
+# -*- coding: utf-8 -*-
+
+# daemon/daemon.py
+# Part of python-daemon, an implementation of PEP 3143.
+#
+# Copyright © 2008–2010 Ben Finney <ben+python@benfinney.id.au>
+# 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 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.
+
+""" Daemon process behaviour.
+ """
+
+import os
+import sys
+import resource
+import errno
+import signal
+import socket
+import atexit
+
+
+class DaemonError(Exception):
+ """ Base exception class for errors from this module. """
+
+
+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(object):
+ """ 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.
+
+ Since a process inherits its umask from its parent process,
+ starting the daemon will reset the umask to this value so that
+ files are created by the daemon with access modes as it expects.
+
+ `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 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`.
+
+ """
+
+ 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,
+ # <URL:http://docs.python.org/library/stdtypes.html#typecontextmanager>.
+ 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.
+ :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"
+ % vars())
+ raise exception
+
+ def _get_exclude_file_descriptors(self):
+ """ Return the set of file descriptors to exclude closing.
+
+ Returns a set containing the file descriptors for the
+ items in `files_preserve`, and also each of `stdin`,
+ `stdout`, and `stderr`:
+
+ * If the item is ``None``, it is omitted from the return
+ set.
+
+ * If the item has a ``fileno()`` method, that method's
+ return 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
+ if hasattr(item, 'fileno'):
+ exclude_descriptors.add(item.fileno())
+ else:
+ exclude_descriptors.add(item)
+ return exclude_descriptors
+
+ def _make_signal_handler(self, target):
+ """ Make the signal handler for a specified target object.
+
+ If `target` is ``None``, returns ``signal.SIG_IGN``. If
+ `target` is a string, returns the attribute of this
+ instance named by that string. Otherwise, returns `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.
+
+ Constructs 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 change_working_directory(directory):
+ """ Change the working directory of this process.
+ """
+ try:
+ os.chdir(directory)
+ except Exception, exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change working directory (%(exc)s)"
+ % vars())
+ raise error
+
+
+def change_root_directory(directory):
+ """ Change the root directory of this process.
+
+ Sets 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, exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change root directory (%(exc)s)"
+ % vars())
+ raise error
+
+
+def change_file_creation_mask(mask):
+ """ Change the file creation mask for this process.
+ """
+ try:
+ os.umask(mask)
+ except Exception, exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change file creation mask (%(exc)s)"
+ % vars())
+ raise error
+
+
+def change_process_owner(uid, gid):
+ """ Change the owning UID and GID of this process.
+
+ Sets 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, exc:
+ error = DaemonOSEnvironmentError(
+ "Unable to change file creation mask (%(exc)s)"
+ % vars())
+ raise error
+
+
+def prevent_core_dump():
+ """ Prevent this process from generating a core dump.
+
+ Sets the soft and hard limits for core dump size to zero. On
+ Unix, this prevents the process from creating core dump
+ altogether.
+
+ """
+ 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, exc:
+ error = DaemonOSEnvironmentError(
+ "System does not support RLIMIT_CORE resource limit (%(exc)s)"
+ % vars())
+ 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.
+
+ 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.
+
+ If the fork fails, raise a ``DaemonProcessDetachError``
+ with ``error_message``.
+
+ """
+ try:
+ pid = os.fork()
+ if pid > 0:
+ os._exit(0)
+ except OSError, exc:
+ exc_errno = exc.errno
+ exc_strerror = exc.strerror
+ error = DaemonProcessDetachError(
+ "%(error_message)s: [%(exc_errno)d] %(exc_strerror)s" % vars())
+ 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 if the current process is started by `init`.
+
+ The `init` process has the process ID of 1; if that is our
+ parent process ID, return ``True``, otherwise ``False``.
+
+ """
+ result = False
+
+ init_pid = 1
+ if os.getppid() == init_pid:
+ result = True
+
+ return result
+
+
+def is_socket(fd):
+ """ Determine if the file descriptor is a socket.
+
+ Return ``False`` if querying the socket type of `fd` raises an
+ error; otherwise return ``True``.
+
+ """
+ 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, 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 if the current process is started by the superserver.
+
+ 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 process context is required.
+
+ Return ``True`` if the process environment indicates the
+ process is already detached:
+
+ * Process was started by `init`; or
+
+ * Process was started by `inetd`.
+
+ """
+ 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.
+
+ Close the file descriptor `fd`, suppressing an error in the
+ case the file was not open.
+
+ """
+ try:
+ os.close(fd)
+ except OSError, exc:
+ if exc.errno == errno.EBADF:
+ # File descriptor was not open
+ pass
+ else:
+ error = DaemonOSEnvironmentError(
+ "Failed to close file descriptor %(fd)d"
+ " (%(exc)s)"
+ % vars())
+ raise error
+
+
+MAXFD = 2048
+
+def get_maximum_file_descriptors():
+ """ Return the maximum number of open file descriptors for this process.
+
+ Return 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.
+
+ 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.
+
+ `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.
+
+ 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.
+
+ The `signal_handler_map` argument is a map from signal number
+ to signal handler. See the `signal` module for details.
+
+ """
+ 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.
+
+ The function `func` is registered for a call with no arguments
+ at program exit.
+
+ """
+ atexit.register(func)
diff --git a/daemon/pidlockfile.py b/daemon/pidlockfile.py
new file mode 100644
index 0000000..c38beae
--- /dev/null
+++ b/daemon/pidlockfile.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+
+# daemon/pidlockfile.py
+# Part of python-daemon, an implementation of PEP 3143.
+#
+# Copyright © 2008–2010 Ben Finney <ben+python@benfinney.id.au>
+#
+# 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.
+ """
+
+import os
+import errno
+
+from lockfile import (
+ LinkFileLock,
+ AlreadyLocked, LockFailed,
+ NotLocked, NotMyLock,
+ )
+
+
+class PIDFileError(Exception):
+ """ Abstract base class for errors specific to PID files. """
+
+class PIDFileParseError(ValueError, PIDFileError):
+ """ Raised when parsing contents of PID file fails. """
+
+
+class PIDLockFile(LinkFileLock, object):
+ """ Lockfile implemented as a Unix PID file.
+
+ The PID file is named by the attribute `path`. When locked,
+ the file will be created with a single line of text,
+ containing the process ID (PID) of the process that acquired
+ the lock.
+
+ The lock is acquired and maintained as per `LinkFileLock`.
+
+ """
+
+ def read_pid(self):
+ """ Get the PID from the lock file.
+ """
+ result = read_pid_from_pidfile(self.path)
+ return result
+
+ def acquire(self, *args, **kwargs):
+ """ Acquire the lock.
+
+ Locks the PID file then creates the PID file for this
+ lock. The `timeout` parameter is used as for the
+ `LinkFileLock` class.
+
+ """
+ super(PIDLockFile, self).acquire(*args, **kwargs)
+ try:
+ write_pid_to_pidfile(self.path)
+ except OSError, exc:
+ error = LockFailed("%(exc)s" % vars())
+ raise error
+
+ def release(self):
+ """ Release the lock.
+
+ Removes the PID file then releases the lock, or raises an
+ error if the current process does not hold the lock.
+
+ """
+ if self.i_am_locking():
+ remove_existing_pidfile(self.path)
+ super(PIDLockFile, self).release()
+
+ def break_lock(self):
+ """ Break an existing lock.
+
+ If the lock is held, breaks the lock and removes the PID
+ file.
+
+ """
+ super(PIDLockFile, self).break_lock()
+ remove_existing_pidfile(self.path)
+
+
+class TimeoutPIDLockFile(PIDLockFile):
+ """ 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 DaemonRunnerLock. """
+ self.acquire_timeout = acquire_timeout
+ super(TimeoutPIDLockFile, self).__init__(path, *args, **kwargs)
+
+ def acquire(self, timeout=None, *args, **kwargs):
+ """ Acquire the lock. """
+ if timeout is None:
+ timeout = self.acquire_timeout
+ super(TimeoutPIDLockFile, self).acquire(timeout, *args, **kwargs)
+
+
+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 does not exist, return ``None``. If
+ the content is not a valid PID, raise ``PIDFileParseError``.
+
+ """
+ pid = None
+ pidfile = None
+ try:
+ pidfile = open(pidfile_path, 'r')
+ except IOError, exc:
+ if exc.errno == errno.ENOENT:
+ pass
+ else:
+ raise
+
+ if pidfile:
+ # 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:
+ raise PIDFileParseError(
+ "PID file %(pidfile_path)r contents invalid" % vars())
+ 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 = (
+ ((os.R_OK | os.W_OK) << 6) |
+ ((os.R_OK) << 3) |
+ ((os.R_OK)))
+ 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()
+ line = "%(pid)d\n" % vars()
+ pidfile.write(line)
+ pidfile.close()
+
+
+def remove_existing_pidfile(pidfile_path):
+ """ Remove the named PID file if it exists.
+
+ Remove the named PID file. Ignore the condition if the file
+ does not exist, since that only means we are already in the
+ desired state.
+
+ """
+ try:
+ os.remove(pidfile_path)
+ except OSError, exc:
+ if exc.errno == errno.ENOENT:
+ pass
+ else:
+ raise
diff --git a/daemon/runner.py b/daemon/runner.py
new file mode 100644
index 0000000..0642695
--- /dev/null
+++ b/daemon/runner.py
@@ -0,0 +1,229 @@
+# -*- coding: utf-8 -*-
+
+# daemon/runner.py
+# Part of python-daemon, an implementation of PEP 3143.
+#
+# Copyright © 2009–2010 Ben Finney <ben+python@benfinney.id.au>
+# 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 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.
+
+""" Daemon runner library.
+ """
+
+import sys
+import os
+import signal
+import errno
+
+import pidlockfile
+
+from daemon import DaemonContext
+
+
+class DaemonRunnerError(Exception):
+ """ Abstract base class for errors from DaemonRunner. """
+
+class DaemonRunnerInvalidActionError(ValueError, DaemonRunnerError):
+ """ Raised when specified action for DaemonRunner is invalid. """
+
+class DaemonRunnerStartFailureError(RuntimeError, DaemonRunnerError):
+ """ Raised when failure starting DaemonRunner. """
+
+class DaemonRunnerStopFailureError(RuntimeError, DaemonRunnerError):
+ """ Raised when failure stopping DaemonRunner. """
+
+
+class DaemonRunner(object):
+ """ 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.
+
+ """
+
+ start_message = "started with pid %(pid)d"
+
+ def __init__(self, app):
+ """ Set up the parameters of a new runner.
+
+ 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, 'r')
+ self.daemon_context.stdout = open(app.stdout_path, 'w+')
+ self.daemon_context.stderr = open(
+ app.stderr_path, 'w+', 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.
+ """
+ progname = os.path.basename(argv[0])
+ usage_exit_code = 2
+ action_usage = "|".join(self.action_funcs.keys())
+ message = "usage: %(progname)s %(action_usage)s" % vars()
+ emit_message(message)
+ sys.exit(usage_exit_code)
+
+ def parse_args(self, argv=None):
+ """ Parse command-line arguments.
+ """
+ if argv is None:
+ argv = sys.argv
+
+ min_args = 2
+ if len(argv) < min_args:
+ self._usage_exit(argv)
+
+ self.action = 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.
+ """
+ if is_pidfile_stale(self.pidfile):
+ self.pidfile.break_lock()
+
+ try:
+ self.daemon_context.open()
+ except pidlockfile.AlreadyLocked:
+ pidfile_path = self.pidfile.path
+ raise DaemonRunnerStartFailureError(
+ "PID file %(pidfile_path)r already locked" % vars())
+
+ pid = os.getpid()
+ message = self.start_message % vars()
+ emit_message(message)
+
+ self.app.run()
+
+ def _terminate_daemon_process(self):
+ """ Terminate the daemon process specified in the current PID file.
+ """
+ pid = self.pidfile.read_pid()
+ try:
+ os.kill(pid, signal.SIGTERM)
+ except OSError, exc:
+ raise DaemonRunnerStopFailureError(
+ "Failed to terminate %(pid)d: %(exc)s" % vars())
+
+ def _stop(self):
+ """ Exit the daemon process specified in the current PID file.
+ """
+ if not self.pidfile.is_locked():
+ pidfile_path = self.pidfile.path
+ raise DaemonRunnerStopFailureError(
+ "PID file %(pidfile_path)r not locked" % vars())
+
+ 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):
+ """ Return the function for the specified action.
+
+ Raises ``DaemonRunnerInvalidActionError`` if the action is
+ unknown.
+
+ """
+ try:
+ func = self.action_funcs[self.action]
+ except KeyError:
+ raise DaemonRunnerInvalidActionError(
+ "Unknown action: %(action)r" % vars(self))
+ return func
+
+ def do_action(self):
+ """ Perform the requested action.
+ """
+ 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)s\n" % vars())
+ 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" % vars())
+ raise error
+ if not os.path.isabs(path):
+ error = ValueError("Not an absolute path: %(path)r" % vars())
+ raise error
+ lockfile = pidlockfile.TimeoutPIDLockFile(path, acquire_timeout)
+
+ return lockfile
+
+
+def is_pidfile_stale(pidfile):
+ """ Determine whether a PID file is stale.
+
+ Return ``True`` (“stale”) if the contents of the PID file are
+ valid but do not match the PID of a currently-running process;
+ otherwise return ``False``.
+
+ """
+ result = False
+
+ pidfile_pid = pidfile.read_pid()
+ if pidfile_pid is not None:
+ try:
+ os.kill(pidfile_pid, signal.SIG_DFL)
+ except OSError, exc:
+ if exc.errno == errno.ESRCH:
+ # The specified PID does not exist
+ result = True
+
+ return result
diff --git a/daemon/version/__init__.py b/daemon/version/__init__.py
new file mode 100644
index 0000000..d2eafa6
--- /dev/null
+++ b/daemon/version/__init__.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+
+# daemon/version/__init__.py
+# Part of python-daemon, an implementation of PEP 3143.
+#
+# Copyright © 2008–2010 Ben Finney <ben+python@benfinney.id.au>
+# 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.
+
+""" Version information for the python-daemon distribution. """
+
+from version_info import version_info
+
+version_info['version_string'] = u"1.5.5"
+
+version_short = u"%(version_string)s" % version_info
+version_full = u"%(version_string)s.r%(revno)s" % version_info
+version = version_short
+
+author_name = u"Ben Finney"
+author_email = u"ben+python@benfinney.id.au"
+author = u"%(author_name)s <%(author_email)s>" % vars()
+
+copyright_year_begin = u"2001"
+date = version_info['date'].split(' ', 1)[0]
+copyright_year = date.split('-')[0]
+copyright_year_range = copyright_year_begin
+if copyright_year > copyright_year_begin:
+ copyright_year_range += u"–%(copyright_year)s" % vars()
+
+copyright = (
+ u"Copyright © %(copyright_year_range)s %(author)s and others"
+ ) % vars()
+license = u"PSF-2+"
diff --git a/daemon/version/version_info.py b/daemon/version/version_info.py
new file mode 100644
index 0000000..cdbf280
--- /dev/null
+++ b/daemon/version/version_info.py
@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+"""This file is automatically generated by generate_version_info
+It uses the current working tree to determine the revision.
+So don't edit it. :)
+"""
+
+version_info = {'branch_nick': u'python-daemon.devel',
+ 'build_date': '2009-05-22 19:50:06 +1000',
+ 'clean': None,
+ 'date': '2009-05-22 19:47:30 +1000',
+ 'revision_id': 'ben+python@benfinney.id.au-20090522094730-p4vsa0reh7ktt4e1',
+ 'revno': 145}
+
+revisions = {}
+
+file_revisions = {}
+
+
+
+if __name__ == '__main__':
+ print 'revision: %(revno)d' % version_info
+ print 'nick: %(branch_nick)s' % version_info
+ print 'revision id: %(revision_id)s' % version_info