comparison libervia/backend/tools/common/async_process.py @ 4270:0d7bb4df2343

Reformatted code base using black.
author Goffi <goffi@goffi.org>
date Wed, 19 Jun 2024 18:44:57 +0200
parents 601e72332907
children
comparison
equal deleted inserted replaced
4269:64a85ce8be70 4270:0d7bb4df2343
24 from twisted.internet import defer, reactor, protocol 24 from twisted.internet import defer, reactor, protocol
25 from twisted.python.failure import Failure 25 from twisted.python.failure import Failure
26 from libervia.backend.core.i18n import _ 26 from libervia.backend.core.i18n import _
27 from libervia.backend.core import exceptions 27 from libervia.backend.core import exceptions
28 from libervia.backend.core.log import getLogger 28 from libervia.backend.core.log import getLogger
29
29 log = getLogger(__name__) 30 log = getLogger(__name__)
30 31
31 32
32 class CommandProtocol(protocol.ProcessProtocol): 33 class CommandProtocol(protocol.ProcessProtocol):
33 """handle an external command""" 34 """handle an external command"""
35
34 # name of the command (unicode) 36 # name of the command (unicode)
35 name = None 37 name = None
36 # full path to the command (bytes) 38 # full path to the command (bytes)
37 command = None 39 command = None
38 # True to activate logging of command outputs (bool) 40 # True to activate logging of command outputs (bool)
45 """ 47 """
46 self._stdin = stdin 48 self._stdin = stdin
47 self._deferred = deferred 49 self._deferred = deferred
48 self.data = [] 50 self.data = []
49 self.err_data = [] 51 self.err_data = []
50 self.cmd_args: list[str]|None = None 52 self.cmd_args: list[str] | None = None
51 self.cmd_kwargs: dict[str, Any]|None = None 53 self.cmd_kwargs: dict[str, Any] | None = None
52 54
53 @property 55 @property
54 def command_name(self): 56 def command_name(self):
55 """returns command name or empty string if it can't be guessed""" 57 """returns command name or empty string if it can't be guessed"""
56 if self.name is not None: 58 if self.name is not None:
57 return self.name 59 return self.name
58 elif self.command is not None: 60 elif self.command is not None:
59 return os.path.splitext(os.path.basename(self.command))[0].decode('utf-8', 61 return os.path.splitext(os.path.basename(self.command))[0].decode(
60 'ignore') 62 "utf-8", "ignore"
63 )
61 else: 64 else:
62 return '' 65 return ""
63 66
64 def connectionMade(self): 67 def connectionMade(self):
65 if self._stdin is not None: 68 if self._stdin is not None:
66 self.transport.write(self._stdin) 69 self.transport.write(self._stdin)
67 self.transport.closeStdin() 70 self.transport.closeStdin()
68 71
69 def outReceived(self, data): 72 def outReceived(self, data):
70 if self.log: 73 if self.log:
71 log.info(data.decode('utf-8', 'replace')) 74 log.info(data.decode("utf-8", "replace"))
72 self.data.append(data) 75 self.data.append(data)
73 76
74 def errReceived(self, data): 77 def errReceived(self, data):
75 if self.log: 78 if self.log:
76 log.warning(data.decode('utf-8', 'replace')) 79 log.warning(data.decode("utf-8", "replace"))
77 self.err_data.append(data) 80 self.err_data.append(data)
78 81
79 def processEnded(self, reason): 82 def processEnded(self, reason):
80 data = b''.join(self.data) 83 data = b"".join(self.data)
81 if (reason.value.exitCode == 0): 84 if reason.value.exitCode == 0:
82 log.debug(f'{self.command_name!r} command succeed') 85 log.debug(f"{self.command_name!r} command succeed")
83 # we don't use "replace" on purpose, we want an exception if decoding 86 # we don't use "replace" on purpose, we want an exception if decoding
84 # is not working properly 87 # is not working properly
85 self._deferred.callback(data) 88 self._deferred.callback(data)
86 else: 89 else:
87 err_data = b''.join(self.err_data) 90 err_data = b"".join(self.err_data)
88 91
89 assert self.cmd_args is not None 92 assert self.cmd_args is not None
90 assert self.cmd_kwargs is not None 93 assert self.cmd_kwargs is not None
91 msg = ( 94 msg = _(
92 _( 95 "Can't complete {name} command (error code: {code}):\n"
93 "Can't complete {name} command (error code: {code}):\n" 96 "Executed command: {command}\n"
94 "Executed command: {command}\n" 97 "Keyword arguments:\n"
95 "Keyword arguments:\n" 98 "{command_kw}\n\n"
96 "{command_kw}\n\n" 99 "stderr:\n{stderr}\n{stdout}\n"
97 "stderr:\n{stderr}\n{stdout}\n" 100 ).format(
98 ) 101 name=self.command_name,
99 .format( 102 code=reason.value.exitCode,
100 name = self.command_name, 103 command=" ".join(self.cmd_args),
101 code = reason.value.exitCode, 104 command_kw="\n".join(
102 command = " ".join(self.cmd_args), 105 f" - {k} = {v!r}" for k, v in self.cmd_kwargs.items()
103 command_kw = "\n".join( 106 ),
104 f" - {k} = {v!r}" for k,v in self.cmd_kwargs.items() 107 stderr=err_data.decode(errors="replace"),
105 ), 108 stdout="stdout: " + data.decode(errors="replace") if data else "",
106 stderr= err_data.decode(errors='replace'),
107 stdout = "stdout: " + data.decode(errors='replace')
108 if data else '',
109 )
110 ) 109 )
111 self._deferred.errback(Failure(exceptions.CommandException( 110 self._deferred.errback(
112 msg, data, err_data))) 111 Failure(exceptions.CommandException(msg, data, err_data))
112 )
113 113
114 @classmethod 114 @classmethod
115 def run(cls, *args, **kwargs): 115 def run(cls, *args, **kwargs):
116 """Create a new CommandProtocol and execute the given command. 116 """Create a new CommandProtocol and execute the given command.
117 117
125 @return ((D)bytes): stdout in case of success 125 @return ((D)bytes): stdout in case of success
126 @raise RuntimeError: command returned a non zero status 126 @raise RuntimeError: command returned a non zero status
127 stdin and stdout will be given as arguments 127 stdin and stdout will be given as arguments
128 128
129 """ 129 """
130 stdin = kwargs.pop('stdin', None) 130 stdin = kwargs.pop("stdin", None)
131 if stdin is not None: 131 if stdin is not None:
132 stdin = stdin.encode('utf-8') 132 stdin = stdin.encode("utf-8")
133 verbose = kwargs.pop('verbose', False) 133 verbose = kwargs.pop("verbose", False)
134 args = list(args) 134 args = list(args)
135 d = defer.Deferred() 135 d = defer.Deferred()
136 prot = cls(d, stdin=stdin) 136 prot = cls(d, stdin=stdin)
137 if verbose: 137 if verbose:
138 prot.log = True 138 prot.log = True
139 if cls.command is None: 139 if cls.command is None:
140 if not args: 140 if not args:
141 raise ValueError( 141 raise ValueError(
142 "You must either specify cls.command or use a full path to command " 142 "You must either specify cls.command or use a full path to command "
143 "to execute as first argument") 143 "to execute as first argument"
144 )
144 command = args.pop(0) 145 command = args.pop(0)
145 if prot.name is None: 146 if prot.name is None:
146 name = os.path.splitext(os.path.basename(command))[0] 147 name = os.path.splitext(os.path.basename(command))[0]
147 prot.name = name 148 prot.name = name
148 else: 149 else:
153 if "env" not in kwargs: 154 if "env" not in kwargs:
154 # we pass parent environment by default 155 # we pass parent environment by default
155 # FIXME: `None` doesn't seem to work, despite what documentation says, to be 156 # FIXME: `None` doesn't seem to work, despite what documentation says, to be
156 # checked and reported upstream if confirmed. 157 # checked and reported upstream if confirmed.
157 kwargs["env"] = os.environ 158 kwargs["env"] = os.environ
158 reactor.spawnProcess(prot, 159 reactor.spawnProcess(prot, command, cmd_args, **kwargs)
159 command,
160 cmd_args,
161 **kwargs)
162 return d 160 return d
163 161
164 162
165 run = CommandProtocol.run 163 run = CommandProtocol.run