Mercurial > libervia-backend
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 |