Plugins/VcsPlugins/vcsMercurial/HgClient.py

changeset 1240
4d5fc346bd3b
child 1241
09c6155ee612
equal deleted inserted replaced
1239:697757468865 1240:4d5fc346bd3b
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2011 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 PyQt4.QtCore import QProcess, QProcessEnvironment, QObject, QByteArray, \
14 QCoreApplication
15
16 import Preferences
17
18
19 class HgClient(QObject):
20 """
21 Class implementing the Mercurial command server interface.
22 """
23 InputFormat = ">I"
24 OutputFormat = ">cI"
25 OutputFormatSize = struct.calcsize(OutputFormat)
26 ReturnFormat = ">i"
27
28 def __init__(self, repoPath, encoding, parent=None):
29 """
30 Constructor
31
32 @param repoPath root directory of the repository (string)
33 @param encoding encoding to be used by the command server (string)
34 @param parent reference to the parent object (QObject)
35 """
36 super().__init__(parent)
37
38 self.__server = QProcess()
39 self.__started = False
40 self.__version = None
41 self.__encoding = Preferences.getSystem("IOEncoding")
42
43 # connect signals
44 self.__server.finished.connect(self.__serverFinished)
45
46 # generate command line and environment
47 self.__serverArgs = []
48 self.__serverArgs.append("serve")
49 self.__serverArgs.append("--cmdserver")
50 self.__serverArgs.append("pipe")
51 self.__serverArgs.append("--config")
52 self.__serverArgs.append("ui.interactive=True")
53 if repoPath:
54 self.__serverArgs.append("--repository")
55 self.__serverArgs.append(repoPath)
56
57 if encoding:
58 env = QProcessEnvironment.systemEnvironment()
59 env.insert("HGENCODING", encoding)
60 self.__server.setProcessEnvironment(env)
61 self.__encoding = encoding
62
63 def startServer(self):
64 """
65 Public method to start the command server.
66
67 @return tuple of flag indicating a successful start (boolean) and
68 an error message (string) in case of failure
69 """
70 self.__server.start('hg', self.__serverArgs)
71 serverStarted = self.__server.waitForStarted()
72 if not serverStarted:
73 return False, self.trUtf8(
74 'The process {0} could not be started. '
75 'Ensure, that it is in the search path.'
76 ).format('hg')
77
78 self.__server.setReadChannel(QProcess.StandardOutput)
79 ok, error = self.__readHello()
80 self.__started = ok
81 return ok, error
82
83 def stopServer(self):
84 """
85 Public method to stop the command server.
86 """
87 self.__server.closeWriteChannel()
88 self.__server.waitForFinished()
89
90 def restartServer(self):
91 """
92 Public method to restart the command server.
93
94 @return tuple of flag indicating a successful start (boolean) and
95 an error message (string) in case of failure
96 """
97 self.stopServer()
98 return self.startServer()
99
100 def __readHello(self):
101 """
102 Private method to read the hello message sent by the command server.
103
104 @return tuple of flag indicating success (boolean) and an error message
105 in case of failure (string)
106 """
107 ch, msg = self.__readChannel()
108 if not ch:
109 return False, self.trUtf8("Did not receive the 'hello' message.")
110 elif ch != "o":
111 return False, self.trUtf8("Received data on unexpected channel.")
112
113 msg = msg.split("\n")
114
115 if not msg[0].startswith("capabilities: "):
116 return False, self.trUtf8("Bad 'hello' message, expected 'capabilities: '"
117 " but got '{0}'.").format(msg[0])
118 self.__capabilities = msg[0][len('capabilities: '):]
119 if not self.__capabilities:
120 return False, self.trUtf8("'capabilities' message did not contain"
121 " any capability.")
122
123 self.__capabilities = set(self.__capabilities.split())
124 if "runcommand" not in self.__capabilities:
125 return False, "'capabilities' did not contain 'runcommand'."
126
127 if not msg[1].startswith("encoding: "):
128 return False, self.trUtf8("Bad 'hello' message, expected 'encoding: '"
129 " but got '{0}'.").format(msg[1])
130 encoding = msg[1][len('encoding: '):]
131 if not encoding:
132 return False, self.trUtf8("'encoding' message did not contain"
133 " any encoding.")
134 self.__encoding = encoding
135
136 return True, ""
137
138 def __serverFinished(self, exitCode, exitStatus):
139 """
140 Private slot connected to the finished signal.
141
142 @param exitCode exit code of the process (integer)
143 @param exitStatus exit status of the process (QProcess.ExitStatus)
144 """
145 self.__started = False
146
147 def __readChannel(self):
148 """
149 Private method to read data from the command server.
150
151 @return tuple of channel designator and channel data (string, integer or string)
152 """
153 if self.__server.bytesAvailable() > 0 or \
154 self.__server.waitForReadyRead(10000):
155 data = bytes(self.__server.read(HgClient.OutputFormatSize))
156 if not data:
157 return "", ""
158
159 channel, length = struct.unpack(HgClient.OutputFormat, data)
160 channel = channel.decode(self.__encoding)
161 if channel in "IL":
162 return channel, length
163 else:
164 return (channel,
165 str(self.__server.read(length), self.__encoding, "replace"))
166 else:
167 return "", ""
168
169 def __writeDataBlock(self, data):
170 """
171 Private slot to write some data to the command server.
172
173 @param data data to be sent (string)
174 """
175 if not isinstance(data, bytes):
176 data = data.encode(self.__encoding)
177 self.__server.write(QByteArray(struct.pack(HgClient.InputFormat, len(data))))
178 self.__server.write(QByteArray(data))
179 self.__server.waitForBytesWritten()
180
181 def __runcommand(self, args, inputChannels, outputChannels):
182 """
183 Private method to run a command in the server (low level).
184
185 @param args list of arguments for the command (list of string)
186 @param inputChannels dictionary of input channels. The dictionary must
187 have the keys 'I' and 'L' and each entry must be a function receiving
188 the number of bytes to write.
189 @param outputChannels dictionary of output channels. The dictionary must
190 have the keys 'o' and 'e' and each entry must be a function receiving
191 the data.
192 """
193 if not self.__started:
194 return -1
195
196 self.__server.write(QByteArray(b'runcommand\n'))
197 self.__writeDataBlock('\0'.join(args))
198
199 while True:
200 QCoreApplication.processEvents()
201 if self.__server.bytesAvailable() == 0:
202 continue
203 channel, data = self.__readChannel()
204
205 # input channels
206 if channel in inputChannels:
207 self.__writeDataBlock(inputChannels[channel](data))
208
209 # output channels
210 elif channel in outputChannels:
211 outputChannels[channel](data)
212
213 # result channel, command is finished
214 elif channel == "r":
215 return struct.unpack(HgClient.ReturnFormat,
216 data.encode(self.__encoding))[0]
217
218 # unexpected but required channel
219 elif channel.isupper():
220 raise RuntimeError(
221 "Unexpected but required channel '{0}'.".format(channel))
222
223 # optional channels
224 else:
225 pass
226
227 def runcommand(self, args, prompt=None, input=None):
228 """
229 Public method to execute a command via the command server.
230
231 @param args list of arguments for the command (list of string)
232 @param prompt function to reply to prompts by the server. It
233 receives the max number of bytes to return and the contents
234 of the output channel received so far.
235 @param input function to reply to bulk data requests by the server.
236 It receives the max number of bytes to return.
237 @return output and errors of the command server (string)
238 """
239 output = io.StringIO()
240 error = io.StringIO()
241 outputChannels = {
242 "o": output.write,
243 "e": error.write
244 }
245
246 inputChannels = {}
247 if prompt is not None:
248 def func(size):
249 reply = prompt(size, output.getvalue())
250 return reply
251 inputChannels["L"] = func
252 if input is not None:
253 inputChannels["I"] = input
254
255 self.__runcommand(args, inputChannels, outputChannels)
256 out = output.getvalue()
257 err = error.getvalue()
258
259 return out, err
260

eric ide

mercurial