Mercurial > libervia-backend
diff libervia/backend/tools/common/async_process.py @ 4071:4b842c1fb686
refactoring: renamed `sat` package to `libervia.backend`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 11:49:51 +0200 |
parents | sat/tools/common/async_process.py@799d4f6fa7ca |
children | 730f542e4ad0 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/backend/tools/common/async_process.py Fri Jun 02 11:49:51 2023 +0200 @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 + + +# SAT: a jabber client +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# 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 +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# 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 <http://www.gnu.org/licenses/>. + +"""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 libervia.backend.core.i18n import _ +from libervia.backend.core import exceptions +from libervia.backend.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.data = [] + self.err_data = [] + + @property + def command_name(self): + """returns command name or empty string if it can't be guessed""" + if self.name is not None: + return self.name + elif self.command is not None: + return os.path.splitext(os.path.basename(self.command))[0].decode('utf-8', + 'ignore') + else: + return '' + + def connectionMade(self): + if self._stdin is not None: + self.transport.write(self._stdin) + self.transport.closeStdin() + + def outReceived(self, data): + if self.log: + log.info(data.decode('utf-8', 'replace')) + self.data.append(data) + + def errReceived(self, data): + if self.log: + log.warning(data.decode('utf-8', 'replace')) + self.err_data.append(data) + + def processEnded(self, reason): + data = b''.join(self.data) + 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 + self._deferred.callback(data) + else: + err_data = b''.join(self.err_data) + + msg = (_("Can't complete {name} command (error code: {code}):\n" + "stderr:\n{stderr}\n{stdout}\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 '', + )) + self._deferred.errback(Failure(exceptions.CommandException( + msg, data, err_data))) + + @classmethod + 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 execute + 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 = list(args) + 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 prot.name is None: + name = os.path.splitext(os.path.basename(command))[0] + prot.name = name + else: + command = cls.command + cmd_args = [command] + args + if "env" not in kwargs: + # we pass parent environment by default + kwargs["env"] = None + reactor.spawnProcess(prot, + command, + cmd_args, + **kwargs) + return d + + +run = CommandProtocol.run