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

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

eric ide

mercurial