comparison sat/tools/common/async_process.py @ 2793:181735d1b062

plugin mr mercurial, tools(common/utils): moved command protocol to a new module
author Goffi <goffi@goffi.org>
date Fri, 25 Jan 2019 09:06:29 +0100
parents
children ab2696e34d29
comparison
equal deleted inserted replaced
2792:441b536e28ed 2793:181735d1b062
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3
4 # SAT: a jabber client
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
16
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
20 """tools to launch process in a async way (using Twisted)"""
21
22 import os.path
23 from twisted.internet import defer, reactor, protocol
24 from twisted.python.failure import Failure
25 from sat.core.i18n import _
26 from sat.core import exceptions
27 from sat.core.log import getLogger
28 log = getLogger(__name__)
29
30
31 class CommandProtocol(protocol.ProcessProtocol):
32 """handle an external command"""
33 # name of the command (unicode)
34 name = None
35 # full path to the command (bytes)
36 command = None
37 # True to activate logging of command outputs (bool)
38 log = False
39
40 def __init__(self, deferred, stdin=None):
41 """
42 @param deferred(defer.Deferred): will be called when command is completed
43 @param stdin(str, None): if not None, will be push to standard input
44 """
45 self._stdin = stdin
46 self._deferred = deferred
47 self.data = []
48 self.err_data = []
49
50 @property
51 def command_name(self):
52 """returns command name or empty string if it can't be guessed"""
53 if self.name is not None:
54 return self.name
55 elif self.command is not None:
56 return os.path.splitext(os.path.basename(self.command))[0].decode('utf-8',
57 'ignore')
58 else:
59 return u''
60
61 def connectionMade(self):
62 if self._stdin is not None:
63 self.transport.write(self._stdin)
64 self.transport.closeStdin()
65
66 def outReceived(self, data):
67 if self.log:
68 log.info(data.decode('utf-8', 'replace'))
69 self.data.append(data)
70
71 def errReceived(self, data):
72 if self.log:
73 log.warning(data.decode('utf-8', 'replace'))
74 self.err_data.append(data)
75
76 def processEnded(self, reason):
77 data = ''.join(self.data)
78 if (reason.value.exitCode == 0):
79 log.debug(_(u'{name} command succeed').format(name=self.command_name))
80 # we don't use "replace" on purpose, we want an exception if decoding
81 # is not working properly
82 self._deferred.callback(data.encode('utf-8'))
83 else:
84 err_data = u''.join(self.err_data)
85
86 msg = (_(u"Can't complete {name} command (error code: {code}):\n"
87 u"stderr:\n{stderr}\n{stdout}\n")
88 .format(name = self.command_name,
89 code = reason.value.exitCode,
90 stderr= err_data.encode('utf-8', 'replace'),
91 stdout = "stdout: " + data.encode('utf-8', 'replace')
92 if data else u'',
93 ))
94 self._deferred.errback(Failure(exceptions.CommandException(
95 msg, data, err_data)))
96
97 @classmethod
98 def run(cls, *args, **kwargs):
99 """Create a new CommandProtocol and execute the given command.
100
101 @param *args(unicode): command arguments
102 if cls.command is specified, it will be the path to the command to execture
103 otherwise, first argument must be the path
104 @param **kwargs: can be:
105 - stdin(unicode, None): data to push to standard input
106 - verbose(bool): if True stdout and stderr will be logged
107 other keyword arguments will be used in reactor.spawnProcess
108 @return ((D)): stdout in case of success
109 @raise RuntimeError: command returned a non zero status
110 stdin and stdout will be given as arguments
111
112 """
113 stdin = kwargs.pop('stdin', None)
114 if stdin is not None:
115 stdin = stdin.encode('utf-8')
116 verbose = kwargs.pop('verbose', False)
117 if u'path' in kwargs:
118 kwargs[u'path'] = kwargs[u'path'].encode('utf-8')
119 args = [a.encode('utf-8') for a in args]
120 kwargs = {k:v.encode('utf-8') for k,v in kwargs.items()}
121 d = defer.Deferred()
122 prot = cls(d, stdin=stdin)
123 if verbose:
124 prot.log = True
125 if cls.command is None:
126 if not args:
127 raise ValueError(
128 u"You must either specify cls.command or use a full path to command "
129 u"to execute as first argument")
130 command = args.pop(0)
131 if prot.name is None:
132 name = os.path.splitext(os.path.basename(command))[0]
133 prot.name = name.encode(u'utf-8', u'ignore')
134 else:
135 command = cls.command
136 cmd_args = [os.path.basename(command)] + args
137 reactor.spawnProcess(prot,
138 command,
139 cmd_args,
140 **kwargs)
141 return d