From 02378192d5bb4b16498d87ace57da425166426bf Mon Sep 17 00:00:00 2001 From: Lorry Tar Creator Date: Tue, 2 Mar 2010 09:35:54 +0000 Subject: Imported from /home/lorry/working-area/delta_python-packages_python-daemon/python-daemon-1.5.5.tar.gz. --- daemon/__init__.py | 47 +++ daemon/daemon.py | 776 +++++++++++++++++++++++++++++++++++++++++ daemon/pidlockfile.py | 194 +++++++++++ daemon/runner.py | 229 ++++++++++++ daemon/version/__init__.py | 36 ++ daemon/version/version_info.py | 23 ++ 6 files changed, 1305 insertions(+) create mode 100644 daemon/__init__.py create mode 100644 daemon/daemon.py create mode 100644 daemon/pidlockfile.py create mode 100644 daemon/runner.py create mode 100644 daemon/version/__init__.py create mode 100644 daemon/version/version_info.py (limited to 'daemon') 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 +# 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 +# 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, + # . + 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 +# +# 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 +# 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 +# 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 -- cgit v1.2.1