# -*- coding: utf-8 -*-
### Copyright (C) 2006-2012 Antonio Valentino <a_valentino@users.sf.net>
### This file is part of exectools.
### This module is free software; you can redistribute it and/or modify
### it under the terms of the GNU General Public License as published by
### the Free Software Foundation; either version 2 of the License, or
### (at your option) any later version.
### This module is distributed in the hope that it will be useful,
### but WITHOUT ANY WARRANTY; without even the implied warranty of
### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
### GNU General Public License for more details.
### You should have received a copy of the GNU General Public License
### along with this module; if not, write to the Free Software
### Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA.
'''Tools for running external processes using the subprocess module.'''
import sys
from exectools import BaseToolController, EX_OK
from . import subprocess2
__author__ = 'Antonio Valentino <a_valentino@users.sf.net>'
__revision__ = '$Revision: $'
__date__ = '$Date: $'
[docs]class StdToolController(BaseToolController):
    '''Class for controlling command line tools.
    A tool controller runs an external tool in controlled way.
    The output of the child process is handled by the controller and,
    optionally, notifications can be achieved at sub-process
    termination.
    A tool controller also allow to stop the controlled process.
    '''
    @property
[docs]    def isbusy(self):
        '''If True then the controller is already running a subprocess.'''
        return self.subclasses is not None
[docs]    def run_tool(self, tool, *args, **kwargs):
        '''Run an external tool in controlled way.
        The output of the child process is handled by the controller
        and, optionally, notifications can be achieved at sub-process
        termination.
        '''
        self.reset()
        self._tool = tool
        if sys.platform[:3] == 'win':
            closefds = False
            startupinfo = subprocess2.STARTUPINFO()
            startupinfo.dwFlags |= subprocess2.STARTF_USESHOWWINDOW
        else:
            closefds = True
            startupinfo = None
        if self._tool.stdout_handler:
            self._tool.stdout_handler.reset()
        if self._tool.stderr_handler:
            self._tool.stderr_handler.reset()
        cmd = self._tool.cmdline(*args, **kwargs)
        self.prerun_hook(*cmd)
        try:
            self.subprocess = subprocess2.Popen(cmd,
                                                stdin=subprocess2.PIPE,
                                                stdout=subprocess2.PIPE,
                                                stderr=subprocess2.STDOUT,
                                                cwd=self._tool.cwd,
                                                env=self._tool.env,
                                                close_fds=closefds,
                                                shell=self._tool.shell,
                                                startupinfo=startupinfo)
            self.subprocess.stdin.close()
            self.connect_output_handlers()
        except OSError:
            if not isinstance(args, basestring):
                args = ' '.join(args)
            msg = 'Unable to execute: "%s"' % args
            self.logger.error(msg, exc_info=True)
            self._reset()
        except:
            self._reset()
            raise
[docs]    def finalize_run(self, *args, **kwargs):
        '''Perform finalization actions.
        This method is called when the controlled process terminates
        to perform finalization actions like:
        * read and handle residual data in buffers,
        * flush and close output handlers,
        * close subprocess file descriptors
        * run the "finalize_run_hook" method
        * reset the controller instance
        This method is not meant to be called by the user but the user
        can provide custom implementations in subclasses.
        If one just needs to perfor some additional finalization action
        it should be better to use a custom "finalize_run_hook" instead
        of overriging "finalize_run".
        '''
        try:
            if self.subprocess:
                # retrieve residual data
                if sys.platform[:3] == 'win':
                    # using read() here hangs on win32
                    if self._tool.stdout_handler:
                        data = self.subprocess.recv()
                        while data:
                            data = data.decode(self._tool.output_encoding)
                            self._tool.stdout_handler.feed(data)
                            data = self.subprocess.recv()
                    if self._tool.stderr_handler:
                        data = self.subprocess.recv_err()
                        while data:
                            data = data.decode(self._tool.output_encoding)
                            self._tool.stderr_handler.feed(data)
                            data = self.subprocess.recv_err()
                else:
                    try:
                        if self._tool.stdout_handler:
                            data = self.subprocess.stdout.read()
                            data = data.decode(self._tool.output_encoding)
                            self._tool.stdout_handler.feed(data)
                    except ValueError:
                        # I/O operation on closed file.
                        pass
                    try:
                        if self._tool.stderr_handler:
                            data = self.subprocess.stderr.read()
                            data = data.decode(self._tool.output_encoding)
                            self._tool.stderr_handler.feed(data)
                    except ValueError:
                        # I/O operation on closed file.
                        pass
                # close the pipes
                # NOTE: recipe_440544.Popen closes the stdout if no more
                #       output is available
                if self.subprocess.stdout:
                    self.subprocess.stdout.close()
                if self.subprocess.stderr:
                    self.subprocess.stderr.close()
                # wait for the subprocess termination
                self.subprocess.wait()
                if self._tool.stdout_handler:
                    self._tool.stdout_handler.close()
                if self._tool.stderr_handler:
                    self._tool.stderr_handler.close()
                if self._userstop:
                    self.logger.info('Execution stopped by the user.')
                elif self.subprocess.returncode != EX_OK:
                    msg = ('Process (PID=%d) exited with return code %d.' %
                                           (self.subprocess.pid,
                                            self.subprocess.returncode))
                    self.logger.warning(msg)
                # Call finalize hook is available
                self.finalize_run_hook()
        finally:
            # Protect for unexpected errors in the feed and close methods of
            # the outputhandler
            self._reset()
    def _reset(self):
        '''Internal reset.
        Kill the controlled subprocess and reset I/O channels loosing
        all unprocessed data.
        '''
        if self.subprocess:
            self.subprocess.stop(force=True)
        assert (self.subprocess is None or
                self.subprocess.returncode is not None), \
                                        'the process is still running'
        super(StdToolController, self)._reset()
[docs]    def reset(self):
        '''Reset the tool controller instance.
        Kill the controlled subprocess and reset the controller
        instance loosing all unprocessed data.
        '''
        super(StdToolController, self).reset()
        self.subprocess = None
[docs]    def handle_stdout(self, *args):
        '''Handle standard output data.
        This method is not meant to be directly called by the user.
        The user, anyway, can provide a custom implementation in
        derived classes.
        '''
        if self.subprocess is None or self.subprocess.poll() is not None:
            # NOTE: 'self.subprocess is None' should never happen at this point
            #self.finalize_run() # @TODO: check
            return False
        else:
            data = self.subprocess.recv()
            if data:
                data = data.decode(self._tool.output_encoding)
                self._tool.stdout_handler.feed(data)
            return True
[docs]    def handle_stderr(self, *args):
        '''Handle standard  error.
        This method is not meant to be directly called by the user.
        The user, anyway, can provide a custom implementation in
        derived classes.
        '''
        if self.subprocess is None or self.subprocess.poll() is not None:
            # NOTE: 'self.subprocess is None' should never happen ar this point
            #self.finalize_run() # @TODO: check
            return False
        else:
            data = self.subprocess.recv_err()
            if data:
                data = data.decode(self._tool.output_encoding)
                self._tool.stderr_handler.feed(data)
            return True
[docs]    def stop_tool(self, force=True):
        '''Stop the execution of controlled subprocess.
        When this method is invoked the controller instance is always
        reset even if the controller is unable to stop the subprocess.
        When possible the controller try to kill the subprocess in a
        polite way.  If this fails it also tryes brute killing by
        default (force=True).  This behaviour can be controlled using
        the `force` parameter.
        '''
        # @TODO: fix stop function with shell=True
        if self.subprocess:
            self.logger.debug('Execution stopped by the user.')
            self._userstop = True
            stopped = self.subprocess.stop(force)
            if not stopped:
                msg = ('Unable to stop the sub-process (PID=%d).' %
                                                        self.subprocess.pid)
                self.logger.warning(msg)
                self._reset()
            # The subprocess is successfully stopped.
            # The output handler will provide to the finalization
            # NOTE: return without reset
        else:
            self._reset()
# @TODO: logging handler for progress