view sat/tools/common/ @ 3161:be5fffe34987

tools (common/async_process): fixed stderr handling + added global "run"
author Goffi <>
date Sun, 09 Feb 2020 23:56:40 +0100
parents 559a625a236b
children e86b71b1aa31
line wrap: on
line source

#!/usr/bin/env python3

# SAT: a jabber client
# Copyright (C) 2009-2020 Jérôme Poisson (

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <>.

"""tools to launch process in a async way (using Twisted)"""

import os.path
from twisted.internet import defer, reactor, protocol
from twisted.python.failure import Failure
from sat.core.i18n import _
from sat.core import exceptions
from sat.core.log import getLogger
log = getLogger(__name__)

class CommandProtocol(protocol.ProcessProtocol):
    """handle an external command"""
    # name of the command (unicode)
    name = None
    # full path to the command (bytes)
    command = None
    # True to activate logging of command outputs (bool)
    log = False

    def __init__(self, deferred, stdin=None):
        @param deferred(defer.Deferred): will be called when command is completed
        @param stdin(str, None): if not None, will be push to standard input
        self._stdin = stdin
        self._deferred = deferred = []
        self.err_data = []

    def command_name(self):
        """returns command name or empty string if it can't be guessed"""
        if is not None:
        elif self.command is not None:
            return os.path.splitext(os.path.basename(self.command))[0].decode('utf-8',
            return ''

    def connectionMade(self):
        if self._stdin is not None:

    def outReceived(self, data):
        if self.log:
  'utf-8', 'replace'))

    def errReceived(self, data):
        if self.log:
            log.warning(data.decode('utf-8', 'replace'))

    def processEnded(self, reason):
        data = b''.join(
        if (reason.value.exitCode == 0):
            log.debug(_(f'{self.command_name!r} command succeed'))
            # we don't use "replace" on purpose, we want an exception if decoding
            # is not working properly
            err_data = b''.join(self.err_data)

            msg = (_("Can't complete {name} command (error code: {code}):\n"
                   .format(name = self.command_name,
                           code = reason.value.exitCode,
                           stderr= err_data.decode(errors='replace'),
                           stdout = "stdout: " + data.decode(errors='replace')
                                    if data else '',
                msg, data, err_data)))

    def run(cls, *args, **kwargs):
        """Create a new CommandProtocol and execute the given command.

        @param *args(unicode): command arguments
            if cls.command is specified, it will be the path to the command to execture
            otherwise, first argument must be the path
        @param **kwargs: can be:
            - stdin(unicode, None): data to push to standard input
            - verbose(bool): if True stdout and stderr will be logged
            other keyword arguments will be used in reactor.spawnProcess
        @return ((D)bytes): stdout in case of success
        @raise RuntimeError: command returned a non zero status
            stdin and stdout will be given as arguments

        stdin = kwargs.pop('stdin', None)
        if stdin is not None:
            stdin = stdin.encode('utf-8')
        verbose = kwargs.pop('verbose', False)
        args = [a.encode('utf-8') for a in args]
        kwargs = {k:v.encode('utf-8') for k,v in list(kwargs.items())}
        d = defer.Deferred()
        prot = cls(d, stdin=stdin)
        if verbose:
            prot.log = True
        if cls.command is None:
            if not args:
                raise ValueError(
                    "You must either specify cls.command or use a full path to command "
                    "to execute as first argument")
            command = args.pop(0)
            if is None:
                name = os.path.splitext(os.path.basename(command))[0]
       = name
            command = cls.command
        cmd_args = [os.path.basename(command)] + args
        return d

run =