|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2011 - 2019 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an interface to the Mercurial command server. |
|
8 """ |
|
9 |
|
10 try: |
|
11 str = unicode |
|
12 except NameError: |
|
13 pass |
|
14 |
|
15 import struct |
|
16 import io |
|
17 |
|
18 from PyQt5.QtCore import QProcess, QObject, QByteArray, QCoreApplication, \ |
|
19 QThread |
|
20 from PyQt5.QtWidgets import QDialog |
|
21 |
|
22 from .HgUtilities import prepareProcess |
|
23 |
|
24 |
|
25 class HgClient(QObject): |
|
26 """ |
|
27 Class implementing the Mercurial command server interface. |
|
28 """ |
|
29 InputFormat = ">I" |
|
30 OutputFormat = ">cI" |
|
31 OutputFormatSize = struct.calcsize(OutputFormat) |
|
32 ReturnFormat = ">i" |
|
33 |
|
34 Channels = (b"I", b"L", b"o", b"e", b"r", b"d") |
|
35 |
|
36 def __init__(self, repoPath, encoding, vcs, parent=None): |
|
37 """ |
|
38 Constructor |
|
39 |
|
40 @param repoPath root directory of the repository (string) |
|
41 @param encoding encoding to be used by the command server (string) |
|
42 @param vcs reference to the VCS object (Hg) |
|
43 @param parent reference to the parent object (QObject) |
|
44 """ |
|
45 super(HgClient, self).__init__(parent) |
|
46 |
|
47 self.__server = None |
|
48 self.__started = False |
|
49 self.__version = None |
|
50 self.__encoding = vcs.getEncoding() |
|
51 self.__cancel = False |
|
52 self.__commandRunning = False |
|
53 self.__repoPath = repoPath |
|
54 |
|
55 # generate command line and environment |
|
56 self.__serverArgs = vcs.initCommand("serve") |
|
57 self.__serverArgs.append("--cmdserver") |
|
58 self.__serverArgs.append("pipe") |
|
59 self.__serverArgs.append("--config") |
|
60 self.__serverArgs.append("ui.interactive=True") |
|
61 if repoPath: |
|
62 self.__serverArgs.append("--repository") |
|
63 self.__serverArgs.append(repoPath) |
|
64 |
|
65 if encoding: |
|
66 self.__encoding = encoding |
|
67 if "--encoding" in self.__serverArgs: |
|
68 # use the defined encoding via the environment |
|
69 index = self.__serverArgs.index("--encoding") |
|
70 del self.__serverArgs[index:index + 2] |
|
71 |
|
72 def startServer(self): |
|
73 """ |
|
74 Public method to start the command server. |
|
75 |
|
76 @return tuple of flag indicating a successful start (boolean) and |
|
77 an error message (string) in case of failure |
|
78 """ |
|
79 self.__server = QProcess() |
|
80 self.__server.setWorkingDirectory(self.__repoPath) |
|
81 |
|
82 # connect signals |
|
83 self.__server.finished.connect(self.__serverFinished) |
|
84 |
|
85 prepareProcess(self.__server, self.__encoding) |
|
86 |
|
87 self.__server.start('hg', self.__serverArgs) |
|
88 serverStarted = self.__server.waitForStarted(5000) |
|
89 if not serverStarted: |
|
90 return False, self.tr( |
|
91 'The process {0} could not be started. ' |
|
92 'Ensure, that it is in the search path.' |
|
93 ).format('hg') |
|
94 |
|
95 self.__server.setReadChannel(QProcess.StandardOutput) |
|
96 ok, error = self.__readHello() |
|
97 self.__started = ok |
|
98 return ok, error |
|
99 |
|
100 def stopServer(self): |
|
101 """ |
|
102 Public method to stop the command server. |
|
103 """ |
|
104 if self.__server is not None: |
|
105 self.__server.closeWriteChannel() |
|
106 res = self.__server.waitForFinished(5000) |
|
107 if not res: |
|
108 self.__server.terminate() |
|
109 res = self.__server.waitForFinished(3000) |
|
110 if not res: |
|
111 self.__server.kill() |
|
112 self.__server.waitForFinished(3000) |
|
113 |
|
114 self.__started = False |
|
115 self.__server.deleteLater() |
|
116 self.__server = None |
|
117 |
|
118 def restartServer(self): |
|
119 """ |
|
120 Public method to restart the command server. |
|
121 |
|
122 @return tuple of flag indicating a successful start (boolean) and |
|
123 an error message (string) in case of failure |
|
124 """ |
|
125 self.stopServer() |
|
126 return self.startServer() |
|
127 |
|
128 def __readHello(self): |
|
129 """ |
|
130 Private method to read the hello message sent by the command server. |
|
131 |
|
132 @return tuple of flag indicating success (boolean) and an error message |
|
133 in case of failure (string) |
|
134 """ |
|
135 ch, msg = self.__readChannel() |
|
136 if not ch: |
|
137 return False, self.tr("Did not receive the 'hello' message.") |
|
138 elif ch != "o": |
|
139 return False, self.tr("Received data on unexpected channel.") |
|
140 |
|
141 msg = msg.split("\n") |
|
142 |
|
143 if not msg[0].startswith("capabilities: "): |
|
144 return False, self.tr( |
|
145 "Bad 'hello' message, expected 'capabilities: '" |
|
146 " but got '{0}'.").format(msg[0]) |
|
147 self.__capabilities = msg[0][len('capabilities: '):] |
|
148 if not self.__capabilities: |
|
149 return False, self.tr("'capabilities' message did not contain" |
|
150 " any capability.") |
|
151 |
|
152 self.__capabilities = set(self.__capabilities.split()) |
|
153 if "runcommand" not in self.__capabilities: |
|
154 return False, "'capabilities' did not contain 'runcommand'." |
|
155 |
|
156 if not msg[1].startswith("encoding: "): |
|
157 return False, self.tr( |
|
158 "Bad 'hello' message, expected 'encoding: '" |
|
159 " but got '{0}'.").format(msg[1]) |
|
160 encoding = msg[1][len('encoding: '):] |
|
161 if not encoding: |
|
162 return False, self.tr("'encoding' message did not contain" |
|
163 " any encoding.") |
|
164 self.__encoding = encoding |
|
165 |
|
166 return True, "" |
|
167 |
|
168 def __serverFinished(self, exitCode, exitStatus): |
|
169 """ |
|
170 Private slot connected to the finished signal. |
|
171 |
|
172 @param exitCode exit code of the process (integer) |
|
173 @param exitStatus exit status of the process (QProcess.ExitStatus) |
|
174 """ |
|
175 self.__started = False |
|
176 |
|
177 def __readChannel(self): |
|
178 """ |
|
179 Private method to read data from the command server. |
|
180 |
|
181 @return tuple of channel designator and channel data |
|
182 (string, integer or string or bytes) |
|
183 """ |
|
184 if self.__server.bytesAvailable() > 0 or \ |
|
185 self.__server.waitForReadyRead(10000): |
|
186 data = bytes(self.__server.peek(HgClient.OutputFormatSize)) |
|
187 if not data or len(data) < HgClient.OutputFormatSize: |
|
188 return "", "" |
|
189 |
|
190 channel, length = struct.unpack(HgClient.OutputFormat, data) |
|
191 channel = channel.decode(self.__encoding) |
|
192 if channel in "IL": |
|
193 self.__server.read(HgClient.OutputFormatSize) |
|
194 return channel, length |
|
195 else: |
|
196 if self.__server.bytesAvailable() < \ |
|
197 HgClient.OutputFormatSize + length: |
|
198 return "", "" |
|
199 self.__server.read(HgClient.OutputFormatSize) |
|
200 data = self.__server.read(length) |
|
201 if channel == "r": |
|
202 return (channel, data) |
|
203 else: |
|
204 return (channel, str(data, self.__encoding, "replace")) |
|
205 else: |
|
206 return "", "" |
|
207 |
|
208 def __writeDataBlock(self, data): |
|
209 """ |
|
210 Private slot to write some data to the command server. |
|
211 |
|
212 @param data data to be sent (string) |
|
213 """ |
|
214 if not isinstance(data, bytes): |
|
215 data = data.encode(self.__encoding) |
|
216 self.__server.write( |
|
217 QByteArray(struct.pack(HgClient.InputFormat, len(data)))) |
|
218 self.__server.write(QByteArray(data)) |
|
219 self.__server.waitForBytesWritten() |
|
220 |
|
221 def __runcommand(self, args, inputChannels, outputChannels): |
|
222 """ |
|
223 Private method to run a command in the server (low level). |
|
224 |
|
225 @param args list of arguments for the command (list of string) |
|
226 @param inputChannels dictionary of input channels. The dictionary must |
|
227 have the keys 'I' and 'L' and each entry must be a function |
|
228 receiving the number of bytes to write. |
|
229 @param outputChannels dictionary of output channels. The dictionary |
|
230 must have the keys 'o' and 'e' and each entry must be a function |
|
231 receiving the data. |
|
232 @return result code of the command, -1 if the command server wasn't |
|
233 started or -10, if the command was canceled (integer) |
|
234 @exception RuntimeError raised to indicate an unexpected command |
|
235 channel |
|
236 """ |
|
237 if not self.__started: |
|
238 return -1 |
|
239 |
|
240 self.__server.write(QByteArray(b'runcommand\n')) |
|
241 self.__writeDataBlock('\0'.join(args)) |
|
242 |
|
243 while True: |
|
244 QCoreApplication.processEvents() |
|
245 |
|
246 if self.__cancel: |
|
247 return -10 |
|
248 |
|
249 if self.__server is None: |
|
250 return -1 |
|
251 |
|
252 if self.__server is None or self.__server.bytesAvailable() == 0: |
|
253 QThread.msleep(50) |
|
254 continue |
|
255 channel, data = self.__readChannel() |
|
256 |
|
257 # input channels |
|
258 if channel in inputChannels: |
|
259 if channel == "L": |
|
260 inputData, isPassword = inputChannels[channel](data) |
|
261 # echo the input to the output if it was a prompt |
|
262 if not isPassword: |
|
263 outputChannels["o"](inputData) |
|
264 else: |
|
265 inputData = inputChannels[channel](data) |
|
266 self.__writeDataBlock(inputData) |
|
267 |
|
268 # output channels |
|
269 elif channel in outputChannels: |
|
270 outputChannels[channel](data) |
|
271 |
|
272 # result channel, command is finished |
|
273 elif channel == "r": |
|
274 return struct.unpack(HgClient.ReturnFormat, data)[0] |
|
275 |
|
276 # unexpected but required channel |
|
277 elif channel.isupper(): |
|
278 raise RuntimeError( |
|
279 "Unexpected but required channel '{0}'.".format(channel)) |
|
280 |
|
281 # optional channels or no channel at all |
|
282 else: |
|
283 pass |
|
284 |
|
285 def __prompt(self, size, message): |
|
286 """ |
|
287 Private method to prompt the user for some input. |
|
288 |
|
289 @param size maximum length of the requested input (integer) |
|
290 @param message message sent by the server (string) |
|
291 @return data entered by the user (string) |
|
292 """ |
|
293 from .HgClientPromptDialog import HgClientPromptDialog |
|
294 inputData = "" |
|
295 isPassword = False |
|
296 dlg = HgClientPromptDialog(size, message) |
|
297 if dlg.exec_() == QDialog.Accepted: |
|
298 inputData = dlg.getInput() + '\n' |
|
299 isPassword = dlg.isPassword() |
|
300 return inputData, isPassword |
|
301 |
|
302 def runcommand(self, args, prompt=None, inputData=None, output=None, |
|
303 error=None): |
|
304 """ |
|
305 Public method to execute a command via the command server. |
|
306 |
|
307 @param args list of arguments for the command (list of string) |
|
308 @keyparam prompt function to reply to prompts by the server. It |
|
309 receives the max number of bytes to return and the contents |
|
310 of the output channel received so far. |
|
311 @keyparam inputData function to reply to bulk data requests by the |
|
312 server. It receives the max number of bytes to return. |
|
313 @keyparam output function receiving the data from the server (string). |
|
314 If a prompt function is given, this parameter will be ignored. |
|
315 @keyparam error function receiving error messages from the server |
|
316 (string) |
|
317 @return output and errors of the command server (string). In case |
|
318 output and/or error functions were given, the respective return |
|
319 value will be an empty string. |
|
320 """ |
|
321 self.__commandRunning = True |
|
322 outputChannels = {} |
|
323 outputBuffer = None |
|
324 errorBuffer = None |
|
325 |
|
326 if prompt is not None or output is None: |
|
327 outputBuffer = io.StringIO() |
|
328 outputChannels["o"] = outputBuffer.write |
|
329 else: |
|
330 outputChannels["o"] = output |
|
331 if error: |
|
332 outputChannels["e"] = error |
|
333 else: |
|
334 errorBuffer = io.StringIO() |
|
335 outputChannels["e"] = errorBuffer.write |
|
336 |
|
337 inputChannels = {} |
|
338 if prompt is not None: |
|
339 def func(size): |
|
340 reply = prompt(size, outputBuffer.getvalue()) |
|
341 return reply, False |
|
342 inputChannels["L"] = func |
|
343 else: |
|
344 def myprompt(size): |
|
345 if outputBuffer is None: |
|
346 msg = self.tr("For message see output dialog.") |
|
347 else: |
|
348 msg = outputBuffer.getvalue() |
|
349 reply, isPassword = self.__prompt(size, msg) |
|
350 return reply, isPassword |
|
351 inputChannels["L"] = myprompt |
|
352 if inputData is not None: |
|
353 inputChannels["I"] = inputData |
|
354 |
|
355 self.__cancel = False |
|
356 self.__runcommand(args, inputChannels, outputChannels) |
|
357 if outputBuffer: |
|
358 out = outputBuffer.getvalue() |
|
359 else: |
|
360 out = "" |
|
361 if errorBuffer: |
|
362 err = errorBuffer.getvalue() |
|
363 else: |
|
364 err = "" |
|
365 |
|
366 self.__commandRunning = False |
|
367 |
|
368 return out, err |
|
369 |
|
370 def cancel(self): |
|
371 """ |
|
372 Public method to cancel the running command. |
|
373 """ |
|
374 self.__cancel = True |
|
375 self.restartServer() |
|
376 |
|
377 def wasCanceled(self): |
|
378 """ |
|
379 Public method to check, if the last command was canceled. |
|
380 |
|
381 @return flag indicating the cancel state (boolean) |
|
382 """ |
|
383 return self.__cancel |
|
384 |
|
385 def isExecuting(self): |
|
386 """ |
|
387 Public method to check, if the server is executing a command. |
|
388 |
|
389 @return flag indicating the execution of a command (boolean) |
|
390 """ |
|
391 return self.__commandRunning |
|
392 |
|
393 # |
|
394 # eflag: noqa = M702 |