|
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 |