eric6/Plugins/VcsPlugins/vcsMercurial/HgClient.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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

eric ide

mercurial