src/eric7/Plugins/VcsPlugins/vcsMercurial/HgClient.py

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

eric ide

mercurial