# Copyright (c) 2009 ActiveState Software Inc.
# See http://www.activestate.com/activepython/license/ for licensing
# information.

"""
    pypm.common.proc
    ~~~~~~~~~~~~~~~~

    Process management routines
"""

import os
import sys
import time
import subprocess
from tempfile import TemporaryFile
from contextlib import nested

from pypm.common.util import xjoin
from pypm.common import console

__all__ = ['run_command']

class CommandError(Exception): pass

class NonZeroReturnCode(CommandError):
    """The command returned non-zero exit code"""

    def __init__(self, p, cmd, stdout, stderr):
        self.stdout = stdout
        self.stderr = stderr
        
        msg = '\n'.join([
            'non-zero returncode: {0}'.format(p.returncode),
            'command: {0}'.format(cmd),
            'pwd: {0}'.format(xjoin(os.getcwd())),
            'stderr:\n{0}'.format(stderr),
            'stdout:\n{0}'.format(stdout),
            ])
        super(NonZeroReturnCode, self).__init__(msg)


class TimeoutError(CommandError):
    """process is taking too much time"""

    def __init__(self, cmd, timeout, stdout, stderr):
        msg = '\n'.join([
            'timed out; ergo process is terminated',
            'seconds elapsed: {0}'.format(timeout),
            'command: {0}'.format(cmd),
            'pwd: {0}'.format(xjoin(os.getcwd())),
            'stderr:\n{0}'.format(stderr),
            'stdout:\n{0}'.format(stdout),
            ])
        super(TimeoutError, self).__init__(msg)

    
# TODO: support for incremental results (sometimes a process run for a few
# minutes, but we need to send the stdout as soon as it appears.
def run_command(cmd, merge_streams=False, timeout=None, env=None):
    """Improved replacement for commands.getoutput()

    The following features are implemented:

     - timeout
     - support for merged streams (stdout+stderr together)

    Note that returned data is of *undecoded* str/bytes type (not unicode)

    Return (stdout, stderr)
    """
    # redirect stdout and stderr to temporary *files*
    with nested(TemporaryFile(), TemporaryFile()) as (outf, errf):
        p = subprocess.Popen(cmd, env=env, shell=True, stdout=outf,
                             stderr=outf if merge_streams else errf)

        if timeout is None:
            p.wait()
        else:
            # poll for terminated status till timeout is reached
            t_nought = time.time()
            seconds_passed = 0
            while True:
                if p.poll() is not None:
                    break
                seconds_passed = time.time() - t_nought
                if timeout and seconds_passed > timeout:
                    p.terminate()
                    raise TimeoutError(cmd, timeout,
                                       _read_tmpfd(outf), _read_tmpfd(errf))
                time.sleep(0.1)

        # the process has exited by now; nothing will to be written to
        # outfd/errfd anymore.
        stdout = _read_tmpfd(outf)
        stderr = _read_tmpfd(errf)

    if p.returncode != 0:
        raise NonZeroReturnCode(p, cmd, stdout, stderr)
    else:
        return stdout, stderr

def _read_tmpfd(fil):
    """Read from a temporary file object

    Call this method only when nothing more will be written to the temporary
    file - i.e., all the writing has be done.
    """
    fil.seek(0)
    return fil.read()

def _disable_windows_error_popup():
    """Set error mode to disable Windows error popup
    
    This setting is effective for current process and all the child processes
    """
    # disable nasty critical error pop-ups on Windows
    import win32api, win32con
    win32api.SetErrorMode(win32con.SEM_FAILCRITICALERRORS |
                          win32con.SEM_NOOPENFILEERRORBOX)
if sys.platform.startswith('win'):
    try:
        import win32api
    except ImportError:
        pass # XXX: this means, you will get annoying popups
    else:
        _disable_windows_error_popup()
