##// END OF EJS Templates
wireproto: define and implement responses in framing protocol...
Gregory Szorc -
r37073:61393f88 default
parent child Browse files
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'] = b'text/plain'
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: text/plain\r\n
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: text/plain\r\n
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