Show More
@@ -591,6 +591,40 b' 0x02' | |||||
591 | server. The command has been fully issued and no new data for this |
|
591 | server. The command has been fully issued and no new data for this | |
592 | command will be sent. The next frame will belong to a new command. |
|
592 | command will be sent. The next frame will belong to a new command. | |
593 |
|
593 | |||
|
594 | Bytes Response Data (``0x04``) | |||
|
595 | ------------------------------ | |||
|
596 | ||||
|
597 | This frame contains raw bytes response data to an issued command. | |||
|
598 | ||||
|
599 | The following flag values are defined for this type: | |||
|
600 | ||||
|
601 | 0x01 | |||
|
602 | Data continuation. When set, an additional frame containing raw | |||
|
603 | response data will follow. | |||
|
604 | 0x02 | |||
|
605 | End of data. When sent, the response data has been fully sent and | |||
|
606 | no additional frames for this response will be sent. | |||
|
607 | ||||
|
608 | The ``0x01`` flag is mutually exclusive with the ``0x02`` flag. | |||
|
609 | ||||
|
610 | Error Response (``0x05``) | |||
|
611 | ------------------------- | |||
|
612 | ||||
|
613 | An error occurred when processing a request. This could indicate | |||
|
614 | a protocol-level failure or an application level failure depending | |||
|
615 | on the flags for this message type. | |||
|
616 | ||||
|
617 | The payload for this type is an error message that should be | |||
|
618 | displayed to the user. | |||
|
619 | ||||
|
620 | The following flag values are defined for this type: | |||
|
621 | ||||
|
622 | 0x01 | |||
|
623 | The error occurred at the transport/protocol level. If set, the | |||
|
624 | connection should be closed. | |||
|
625 | 0x02 | |||
|
626 | The error occurred at the application level. e.g. invalid command. | |||
|
627 | ||||
594 | Issuing Commands |
|
628 | Issuing Commands | |
595 | ---------------- |
|
629 | ---------------- | |
596 |
|
630 |
@@ -25,11 +25,15 b' DEFAULT_MAX_FRAME_SIZE = 32768' | |||||
25 | FRAME_TYPE_COMMAND_NAME = 0x01 |
|
25 | FRAME_TYPE_COMMAND_NAME = 0x01 | |
26 | FRAME_TYPE_COMMAND_ARGUMENT = 0x02 |
|
26 | FRAME_TYPE_COMMAND_ARGUMENT = 0x02 | |
27 | FRAME_TYPE_COMMAND_DATA = 0x03 |
|
27 | FRAME_TYPE_COMMAND_DATA = 0x03 | |
|
28 | FRAME_TYPE_BYTES_RESPONSE = 0x04 | |||
|
29 | FRAME_TYPE_ERROR_RESPONSE = 0x05 | |||
28 |
|
30 | |||
29 | FRAME_TYPES = { |
|
31 | FRAME_TYPES = { | |
30 | b'command-name': FRAME_TYPE_COMMAND_NAME, |
|
32 | b'command-name': FRAME_TYPE_COMMAND_NAME, | |
31 | b'command-argument': FRAME_TYPE_COMMAND_ARGUMENT, |
|
33 | b'command-argument': FRAME_TYPE_COMMAND_ARGUMENT, | |
32 | b'command-data': FRAME_TYPE_COMMAND_DATA, |
|
34 | b'command-data': FRAME_TYPE_COMMAND_DATA, | |
|
35 | b'bytes-response': FRAME_TYPE_BYTES_RESPONSE, | |||
|
36 | b'error-response': FRAME_TYPE_ERROR_RESPONSE, | |||
33 | } |
|
37 | } | |
34 |
|
38 | |||
35 | FLAG_COMMAND_NAME_EOS = 0x01 |
|
39 | FLAG_COMMAND_NAME_EOS = 0x01 | |
@@ -58,11 +62,29 b' FLAGS_COMMAND_DATA = {' | |||||
58 | b'eos': FLAG_COMMAND_DATA_EOS, |
|
62 | b'eos': FLAG_COMMAND_DATA_EOS, | |
59 | } |
|
63 | } | |
60 |
|
64 | |||
|
65 | FLAG_BYTES_RESPONSE_CONTINUATION = 0x01 | |||
|
66 | FLAG_BYTES_RESPONSE_EOS = 0x02 | |||
|
67 | ||||
|
68 | FLAGS_BYTES_RESPONSE = { | |||
|
69 | b'continuation': FLAG_BYTES_RESPONSE_CONTINUATION, | |||
|
70 | b'eos': FLAG_BYTES_RESPONSE_EOS, | |||
|
71 | } | |||
|
72 | ||||
|
73 | FLAG_ERROR_RESPONSE_PROTOCOL = 0x01 | |||
|
74 | FLAG_ERROR_RESPONSE_APPLICATION = 0x02 | |||
|
75 | ||||
|
76 | FLAGS_ERROR_RESPONSE = { | |||
|
77 | b'protocol': FLAG_ERROR_RESPONSE_PROTOCOL, | |||
|
78 | b'application': FLAG_ERROR_RESPONSE_APPLICATION, | |||
|
79 | } | |||
|
80 | ||||
61 | # Maps frame types to their available flags. |
|
81 | # Maps frame types to their available flags. | |
62 | FRAME_TYPE_FLAGS = { |
|
82 | FRAME_TYPE_FLAGS = { | |
63 | FRAME_TYPE_COMMAND_NAME: FLAGS_COMMAND, |
|
83 | FRAME_TYPE_COMMAND_NAME: FLAGS_COMMAND, | |
64 | FRAME_TYPE_COMMAND_ARGUMENT: FLAGS_COMMAND_ARGUMENT, |
|
84 | FRAME_TYPE_COMMAND_ARGUMENT: FLAGS_COMMAND_ARGUMENT, | |
65 | FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA, |
|
85 | FRAME_TYPE_COMMAND_DATA: FLAGS_COMMAND_DATA, | |
|
86 | FRAME_TYPE_BYTES_RESPONSE: FLAGS_BYTES_RESPONSE, | |||
|
87 | FRAME_TYPE_ERROR_RESPONSE: FLAGS_ERROR_RESPONSE, | |||
66 | } |
|
88 | } | |
67 |
|
89 | |||
68 | ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH') |
|
90 | ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH') | |
@@ -202,6 +224,47 b' def createcommandframes(cmd, args, dataf' | |||||
202 | if done: |
|
224 | if done: | |
203 | break |
|
225 | break | |
204 |
|
226 | |||
|
227 | def createbytesresponseframesfrombytes(data, | |||
|
228 | maxframesize=DEFAULT_MAX_FRAME_SIZE): | |||
|
229 | """Create a raw frame to send a bytes response from static bytes input. | |||
|
230 | ||||
|
231 | Returns a generator of bytearrays. | |||
|
232 | """ | |||
|
233 | ||||
|
234 | # Simple case of a single frame. | |||
|
235 | if len(data) <= maxframesize: | |||
|
236 | yield makeframe(FRAME_TYPE_BYTES_RESPONSE, | |||
|
237 | FLAG_BYTES_RESPONSE_EOS, data) | |||
|
238 | return | |||
|
239 | ||||
|
240 | offset = 0 | |||
|
241 | while True: | |||
|
242 | chunk = data[offset:offset + maxframesize] | |||
|
243 | offset += len(chunk) | |||
|
244 | done = offset == len(data) | |||
|
245 | ||||
|
246 | if done: | |||
|
247 | flags = FLAG_BYTES_RESPONSE_EOS | |||
|
248 | else: | |||
|
249 | flags = FLAG_BYTES_RESPONSE_CONTINUATION | |||
|
250 | ||||
|
251 | yield makeframe(FRAME_TYPE_BYTES_RESPONSE, flags, chunk) | |||
|
252 | ||||
|
253 | if done: | |||
|
254 | break | |||
|
255 | ||||
|
256 | def createerrorframe(msg, protocol=False, application=False): | |||
|
257 | # TODO properly handle frame size limits. | |||
|
258 | assert len(msg) <= DEFAULT_MAX_FRAME_SIZE | |||
|
259 | ||||
|
260 | flags = 0 | |||
|
261 | if protocol: | |||
|
262 | flags |= FLAG_ERROR_RESPONSE_PROTOCOL | |||
|
263 | if application: | |||
|
264 | flags |= FLAG_ERROR_RESPONSE_APPLICATION | |||
|
265 | ||||
|
266 | yield makeframe(FRAME_TYPE_ERROR_RESPONSE, flags, msg) | |||
|
267 | ||||
205 | class serverreactor(object): |
|
268 | class serverreactor(object): | |
206 | """Holds state of a server handling frame-based protocol requests. |
|
269 | """Holds state of a server handling frame-based protocol requests. | |
207 |
|
270 | |||
@@ -230,6 +293,11 b' class serverreactor(object):' | |||||
230 |
|
293 | |||
231 | Valid actions that consumers can be instructed to take are: |
|
294 | Valid actions that consumers can be instructed to take are: | |
232 |
|
295 | |||
|
296 | sendframes | |||
|
297 | Indicates that frames should be sent to the client. The ``framegen`` | |||
|
298 | key contains a generator of frames that should be sent. The server | |||
|
299 | assumes that all frames are sent to the client. | |||
|
300 | ||||
233 | error |
|
301 | error | |
234 | Indicates that an error occurred. Consumer should probably abort. |
|
302 | Indicates that an error occurred. Consumer should probably abort. | |
235 |
|
303 | |||
@@ -271,6 +339,20 b' class serverreactor(object):' | |||||
271 |
|
339 | |||
272 | return meth(frametype, frameflags, payload) |
|
340 | return meth(frametype, frameflags, payload) | |
273 |
|
341 | |||
|
342 | def onbytesresponseready(self, data): | |||
|
343 | """Signal that a bytes response is ready to be sent to the client. | |||
|
344 | ||||
|
345 | The raw bytes response is passed as an argument. | |||
|
346 | """ | |||
|
347 | return 'sendframes', { | |||
|
348 | 'framegen': createbytesresponseframesfrombytes(data), | |||
|
349 | } | |||
|
350 | ||||
|
351 | def onapplicationerror(self, msg): | |||
|
352 | return 'sendframes', { | |||
|
353 | 'framegen': createerrorframe(msg, application=True), | |||
|
354 | } | |||
|
355 | ||||
274 | def _makeerrorresult(self, msg): |
|
356 | def _makeerrorresult(self, msg): | |
275 | return 'error', { |
|
357 | return 'error', { | |
276 | 'message': msg, |
|
358 | 'message': msg, |
@@ -493,15 +493,20 b' def _httpv2runcommand(ui, repo, req, res' | |||||
493 |
|
493 | |||
494 | rsp = wireproto.dispatch(repo, proto, command['command']) |
|
494 | rsp = wireproto.dispatch(repo, proto, command['command']) | |
495 |
|
495 | |||
496 | # TODO use proper response format. |
|
|||
497 | res.status = b'200 OK' |
|
496 | res.status = b'200 OK' | |
498 |
res.headers[b'Content-Type'] = |
|
497 | res.headers[b'Content-Type'] = FRAMINGTYPE | |
499 |
|
498 | |||
500 | if isinstance(rsp, wireprototypes.bytesresponse): |
|
499 | if isinstance(rsp, wireprototypes.bytesresponse): | |
501 | res.setbodybytes(rsp.data) |
|
500 | action, meta = reactor.onbytesresponseready(rsp.data) | |
502 | else: |
|
501 | else: | |
503 | res.setbodybytes(b'unhandled response type from wire proto ' |
|
502 | action, meta = reactor.onapplicationerror( | |
504 | 'command') |
|
503 | _('unhandled response type from wire proto command')) | |
|
504 | ||||
|
505 | if action == 'sendframes': | |||
|
506 | res.setbodygen(meta['framegen']) | |||
|
507 | else: | |||
|
508 | raise error.ProgrammingError('unhandled event from reactor: %s' % | |||
|
509 | action) | |||
505 |
|
510 | |||
506 | # Maps API name to metadata so custom API can be registered. |
|
511 | # Maps API name to metadata so custom API can be registered. | |
507 | API_HANDLERS = { |
|
512 | API_HANDLERS = { |
@@ -195,10 +195,14 b' Request to read-only command works out o' | |||||
195 | s> HTTP/1.1 200 OK\r\n |
|
195 | s> HTTP/1.1 200 OK\r\n | |
196 | s> Server: testing stub value\r\n |
|
196 | s> Server: testing stub value\r\n | |
197 | s> Date: $HTTP_DATE$\r\n |
|
197 | s> Date: $HTTP_DATE$\r\n | |
198 |
s> Content-Type: |
|
198 | s> Content-Type: application/mercurial-exp-framing-0001\r\n | |
199 | s> Content-Length: 29\r\n |
|
199 | s> Transfer-Encoding: chunked\r\n | |
200 | s> \r\n |
|
200 | s> \r\n | |
201 | s> customreadonly bytes response |
|
201 | s> 21\r\n | |
|
202 | s> \x1d\x00\x00Bcustomreadonly bytes response | |||
|
203 | s> \r\n | |||
|
204 | s> 0\r\n | |||
|
205 | s> \r\n | |||
202 |
|
206 | |||
203 | Request to read-write command fails because server is read-only by default |
|
207 | Request to read-write command fails because server is read-only by default | |
204 |
|
208 | |||
@@ -302,10 +306,14 b' Authorized request for valid read-write ' | |||||
302 | s> HTTP/1.1 200 OK\r\n |
|
306 | s> HTTP/1.1 200 OK\r\n | |
303 | s> Server: testing stub value\r\n |
|
307 | s> Server: testing stub value\r\n | |
304 | s> Date: $HTTP_DATE$\r\n |
|
308 | s> Date: $HTTP_DATE$\r\n | |
305 |
s> Content-Type: |
|
309 | s> Content-Type: application/mercurial-exp-framing-0001\r\n | |
306 | s> Content-Length: 29\r\n |
|
310 | s> Transfer-Encoding: chunked\r\n | |
307 | s> \r\n |
|
311 | s> \r\n | |
308 | s> customreadonly bytes response |
|
312 | s> 21\r\n | |
|
313 | s> \x1d\x00\x00Bcustomreadonly bytes response | |||
|
314 | s> \r\n | |||
|
315 | s> 0\r\n | |||
|
316 | s> \r\n | |||
309 |
|
317 | |||
310 | Authorized request for unknown command is rejected |
|
318 | Authorized request for unknown command is rejected | |
311 |
|
319 |
@@ -79,6 +79,10 b' class ServerReactorTests(unittest.TestCa' | |||||
79 | self.assertIsInstance(res[1], dict) |
|
79 | self.assertIsInstance(res[1], dict) | |
80 | self.assertEqual(res[0], expected) |
|
80 | self.assertEqual(res[0], expected) | |
81 |
|
81 | |||
|
82 | def assertframesequal(self, frames, framestrings): | |||
|
83 | expected = [ffs(s) for s in framestrings] | |||
|
84 | self.assertEqual(list(frames), expected) | |||
|
85 | ||||
82 | def test1framecommand(self): |
|
86 | def test1framecommand(self): | |
83 | """Receiving a command in a single frame yields request to run it.""" |
|
87 | """Receiving a command in a single frame yields request to run it.""" | |
84 | reactor = makereactor() |
|
88 | reactor = makereactor() | |
@@ -270,6 +274,42 b' class ServerReactorTests(unittest.TestCa' | |||||
270 | 'message': b'command data frame without flags', |
|
274 | 'message': b'command data frame without flags', | |
271 | }) |
|
275 | }) | |
272 |
|
276 | |||
|
277 | def testsimpleresponse(self): | |||
|
278 | """Bytes response to command sends result frames.""" | |||
|
279 | reactor = makereactor() | |||
|
280 | list(sendcommandframes(reactor, b'mycommand', {})) | |||
|
281 | ||||
|
282 | result = reactor.onbytesresponseready(b'response') | |||
|
283 | self.assertaction(result, 'sendframes') | |||
|
284 | self.assertframesequal(result[1]['framegen'], [ | |||
|
285 | b'bytes-response eos response', | |||
|
286 | ]) | |||
|
287 | ||||
|
288 | def testmultiframeresponse(self): | |||
|
289 | """Bytes response spanning multiple frames is handled.""" | |||
|
290 | first = b'x' * framing.DEFAULT_MAX_FRAME_SIZE | |||
|
291 | second = b'y' * 100 | |||
|
292 | ||||
|
293 | reactor = makereactor() | |||
|
294 | list(sendcommandframes(reactor, b'mycommand', {})) | |||
|
295 | ||||
|
296 | result = reactor.onbytesresponseready(first + second) | |||
|
297 | self.assertaction(result, 'sendframes') | |||
|
298 | self.assertframesequal(result[1]['framegen'], [ | |||
|
299 | b'bytes-response continuation %s' % first, | |||
|
300 | b'bytes-response eos %s' % second, | |||
|
301 | ]) | |||
|
302 | ||||
|
303 | def testapplicationerror(self): | |||
|
304 | reactor = makereactor() | |||
|
305 | list(sendcommandframes(reactor, b'mycommand', {})) | |||
|
306 | ||||
|
307 | result = reactor.onapplicationerror(b'some message') | |||
|
308 | self.assertaction(result, 'sendframes') | |||
|
309 | self.assertframesequal(result[1]['framegen'], [ | |||
|
310 | b'error-response application some message', | |||
|
311 | ]) | |||
|
312 | ||||
273 | if __name__ == '__main__': |
|
313 | if __name__ == '__main__': | |
274 | import silenttestrunner |
|
314 | import silenttestrunner | |
275 | silenttestrunner.main(__name__) |
|
315 | silenttestrunner.main(__name__) |
General Comments 0
You need to be logged in to leave comments.
Login now