#!/usr/bin/env python2.7

# 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)
