blob: 1212e9e05824f298eb4d786730094907c2207c74 [file] [log] [blame]
#!/usr/bin/env python
# NOTE: Documentation is intended to be processed by epydoc and contains
# epydoc markup.
"""
Overview
========
The ``grizzled.os`` module contains some operating system-related functions and
classes. It is a conceptual extension of the standard Python ``os`` module.
"""
from __future__ import absolute_import
__docformat__ = "restructuredtext en"
# ---------------------------------------------------------------------------
# Imports
# ---------------------------------------------------------------------------
import logging
import os as _os
import sys
import glob
from contextlib import contextmanager
from grizzled.decorators import deprecated
# ---------------------------------------------------------------------------
# Exports
# ---------------------------------------------------------------------------
__all__ = ['daemonize', 'DaemonError', 'working_directory',
'file_separator', 'path_separator']
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
# Default daemon parameters.
# File mode creation mask of the daemon.
UMASK = 0
# Default working directory for the daemon.
WORKDIR = "/"
# Default maximum for the number of available file descriptors.
MAXFD = 1024
# The standard I/O file descriptors are redirected to /dev/null by default.
if (hasattr(_os, "devnull")):
NULL_DEVICE = _os.devnull
else:
NULL_DEVICE = "/dev/null"
# The path separator for the operating system.
PATH_SEPARATOR = {'nt' : ';', 'posix' : ':'}
FILE_SEPARATOR = {'nt' : '\\', 'posix' : '/'}
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log = logging.getLogger('grizzled.os')
# ---------------------------------------------------------------------------
# Public classes
# ---------------------------------------------------------------------------
class DaemonError(OSError):
"""
Thrown by ``daemonize()`` when an error occurs while attempting to create
a daemon.
"""
pass
# ---------------------------------------------------------------------------
# Public functions
# ---------------------------------------------------------------------------
def path_separator():
"""
Get the path separator for the current operating system. The path
separator is used to separate elements of a path string, such as
"PATH" or "CLASSPATH". (It's a ":" on Unix-like systems and a ";"
on Windows.)
:rtype: str
:return: the path separator
"""
return PATH_SEPARATOR[_os.name]
@deprecated(since='0.7', message='Use os.path.sep, instead.')
def file_separator():
"""
Get the file separator for the current operating system. The file
separator is used to separate file elements in a pathname. (It's
"/" on Unix-like systems and a "\\\\" on Windows.)
**Deprecated**. Use the standard Python ``os.path.sep`` variable,
instead.
:rtype: str
:return: the file separator
"""
return FILE_SEPARATOR[_os.name]
def path_elements(path):
"""
Given a path string value (e.g., the value of the environment variable
``PATH``), this generator function yields each item in the path.
:Parameters:
path
the path to break up
"""
for p in path.split(path_separator()):
yield p
@contextmanager
def working_directory(directory):
"""
This function is intended to be used as a ``with`` statement context
manager. It allows you to replace code like this:
.. python::
original_directory = _os.getcwd()
try:
_os.chdir(some_dir)
... bunch of code ...
finally:
_os.chdir(original_directory)
with something simpler:
.. python ::
from __future__ import with_statement
from grizzled.os import working_directory
with working_directory(some_dir):
... bunch of code ...
:Parameters:
directory : str
directory in which to execute
:return: yields the ``directory`` parameter
"""
original_directory = _os.getcwd()
try:
_os.chdir(directory)
yield directory
finally:
_os.chdir(original_directory)
def find_command(command_name, path=None):
"""
Determine whether the specified system command exists in the specified
path.
:Parameters:
command_name
The (simple) filename of the command to find. May be a glob
string.
path
The path to search, as a list or a string. If this parameter
is a string, then it is split using the operating system-specific
path separator. If this parameter is missing, then the ``PATH``
environment variable is used
:rtype: str
:return: The path to the first command that matches ``command_name``, or
``None`` if not found
"""
if not path:
path = _os.environ.get('PATH', '.')
if type(path) == str:
path = path.split(path_separator())
elif type(path) == list:
pass
else:
assert False, 'path parameter must be a list or a string'
found = None
for p in path:
full_path = _os.path.join(p, command_name)
for p2 in glob.glob(full_path):
if _os.access(p2, _os.X_OK):
found = p2
break
if found:
break
return found
def spawnd(path, args, pidfile=None):
"""
Run a command as a daemon. This method is really just shorthand for the
following code:
.. python::
daemonize(pidfile=pidfile)
_os.execv(path, args)
:Parameters:
path : str
Full path to program to run
args : list
List of command arguments. The first element in this list must
be the command name (i.e., arg0).
pidfile : str
Path to file to which to write daemon's process ID. The string may
contain a ``${pid}`` token, which is replaced with the process ID
of the daemon. e.g.: ``/var/run/myserver-${pid}``
"""
daemonize(no_close=True, pidfile=pidfile)
_os.execv(path, args)
def daemonize(no_close=False, pidfile=None):
"""
Convert the calling process into a daemon. To make the current Python
process into a daemon process, you need two lines of code:
.. python::
from grizzled.os import daemonize
daemonize.daemonize()
If ``daemonize()`` fails for any reason, it throws a ``DaemonError``,
which is a subclass of the standard ``OSError`` exception. also logs debug
messages, using the standard Python ``logging`` package, to channel
"grizzled.os.daemon".
**Adapted from:** http://software.clapper.org/daemonize/
**See Also:**
- Stevens, W. Richard. *Unix Network Programming* (Addison-Wesley, 1990).
:Parameters:
no_close : bool
If ``True``, don't close the file descriptors. Useful if the
calling process has already redirected file descriptors to an
output file. **Warning**: Only set this parameter to ``True`` if
you're *sure* there are no open file descriptors to the calling
terminal. Otherwise, you'll risk having the daemon re-acquire a
control terminal, which can cause it to be killed if someone logs
off that terminal.
pidfile : str
Path to file to which to write daemon's process ID. The string may
contain a ``${pid}`` token, which is replaced with the process ID
of the daemon. e.g.: ``/var/run/myserver-${pid}``
:raise DaemonError: Error during daemonizing
"""
log = logging.getLogger('grizzled.os.daemon')
def __fork():
try:
return _os.fork()
except OSError, e:
raise DaemonError, ('Cannot fork', e.errno, e.strerror)
def __redirect_file_descriptors():
import resource # POSIX resource information
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
if maxfd == resource.RLIM_INFINITY:
maxfd = MAXFD
# Close all file descriptors.
for fd in range(0, maxfd):
# Only close TTYs.
try:
_os.ttyname(fd)
except:
continue
try:
_os.close(fd)
except OSError:
# File descriptor wasn't open. Ignore.
pass
# Redirect standard input, output and error to something safe.
# os.open() is guaranteed to return the lowest available file
# descriptor (0, or standard input). Then, we can dup that
# descriptor for standard output and standard error.
_os.open(NULL_DEVICE, _os.O_RDWR)
_os.dup2(0, 1)
_os.dup2(0, 2)
if _os.name != 'posix':
import errno
raise DaemonError, \
('daemonize() is only supported on Posix-compliant systems.',
errno.ENOSYS, _os.strerror(errno.ENOSYS))
try:
# Fork once to go into the background.
log.debug('Forking first child.')
pid = __fork()
if pid != 0:
# Parent. Exit using os._exit(), which doesn't fire any atexit
# functions.
_os._exit(0)
# First child. Create a new session. os.setsid() creates the session
# and makes this (child) process the process group leader. The process
# is guaranteed not to have a control terminal.
log.debug('Creating new session')
_os.setsid()
# Fork a second child to ensure that the daemon never reacquires
# a control terminal.
log.debug('Forking second child.')
pid = __fork()
if pid != 0:
# Original child. Exit.
_os._exit(0)
# This is the second child. Set the umask.
log.debug('Setting umask')
_os.umask(UMASK)
# Go to a neutral corner (i.e., the primary file system, so
# the daemon doesn't prevent some other file system from being
# unmounted).
log.debug('Changing working directory to "%s"' % WORKDIR)
_os.chdir(WORKDIR)
# Unless no_close was specified, close all file descriptors.
if not no_close:
log.debug('Redirecting file descriptors')
__redirect_file_descriptors()
if pidfile:
from string import Template
t = Template(pidfile)
pidfile = t.safe_substitute(pid=str(_os.getpid()))
open(pidfile, 'w').write(str(_os.getpid()) + '\n')
except DaemonError:
raise
except OSError, e:
raise DaemonError, ('Unable to daemonize()', e.errno, e.strerror)
# ---------------------------------------------------------------------------
# Main program (for testing)
# ---------------------------------------------------------------------------
if __name__ == '__main__':
log = logging.getLogger('grizzled.os')
hdlr = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s', '%T')
hdlr.setFormatter(formatter)
log.addHandler(hdlr)
log.setLevel(logging.DEBUG)
log.debug('Before daemonizing, PID=%d' % _os.getpid())
daemonize(no_close=True)
log.debug('After daemonizing, PID=%d' % _os.getpid())
log.debug('Daemon is sleeping for 10 seconds')
import time
time.sleep(10)
log.debug('Daemon exiting')
sys.exit(0)