##// END OF EJS Templates
wireproto: add request IDs to frames...
Gregory Szorc -
r37075:2ec1fb9d default
parent child Browse files
Show More
@@ -2765,12 +2765,14 b' def debugwireproto(ui, repo, path=None, '
2765 2765 syntax.
2766 2766
2767 2767 A frame is composed as a type, flags, and payload. These can be parsed
2768 from a string of the form ``<type> <flags> <payload>``. That is, 3
2769 space-delimited strings.
2768 from a string of the form ``<requestid> <type> <flags> <payload>``. That is,
2769 4 space-delimited strings.
2770 2770
2771 2771 ``payload`` is the simplest: it is evaluated as a Python byte string
2772 2772 literal.
2773 2773
2774 ``requestid`` is an integer defining the request identifier.
2775
2774 2776 ``type`` can be an integer value for the frame type or the string name
2775 2777 of the type. The strings are defined in ``wireprotoframing.py``. e.g.
2776 2778 ``command-name``.
@@ -469,22 +469,26 b' for sending data and another for receivi'
469 469
470 470 The protocol is request-response based: the client issues requests to
471 471 the server, which issues replies to those requests. Server-initiated
472 messaging is not supported.
472 messaging is not currently supported, but this specification carves
473 out room to implement it.
473 474
474 475 All data is read and written in atomic units called *frames*. These
475 476 are conceptually similar to TCP packets. Higher-level functionality
476 477 is built on the exchange and processing of frames.
477 478
478 Frames begin with a 4 octet header followed by a variable length
479 All frames are associated with a numbered request. Frames can thus
480 be logically grouped by their request ID.
481
482 Frames begin with a 6 octet header followed by a variable length
479 483 payload::
480 484
481 485 +-----------------------------------------------+
482 486 | Length (24) |
483 +-----------+-----------------------------------+
484 | Type (4) |
485 +-----------+
486 | Flags (4) |
487 +===========+===================================================|
487 +---------------------------------+-------------+
488 | Request ID (16) |
489 +----------+-----------+----------+
490 | Type (4) | Flags (4) |
491 +==========+===========+========================================|
488 492 | Frame Payload (0...) ...
489 493 +---------------------------------------------------------------+
490 494
@@ -494,6 +498,15 b' given permission by the server as part o'
494 498 during the handshake. The frame header is not part of the advertised
495 499 frame length.
496 500
501 The 16-bit ``Request ID`` field denotes the integer request identifier,
502 stored as an unsigned little endian integer. Odd numbered requests are
503 client-initiated. Even numbered requests are server-initiated. This
504 refers to where the *request* was initiated - not where the *frame* was
505 initiated, so servers will send frames with odd ``Request ID`` in
506 response to client-initiated requests. Implementations are advised to
507 start ordering request identifiers at ``1`` and ``0``, increment by
508 ``2``, and wrap around if all available numbers have been exhausted.
509
497 510 The 4-bit ``Type`` field denotes the type of message being sent.
498 511
499 512 The 4-bit ``Flags`` field defines special, per-type attributes for
@@ -633,6 +646,28 b' frames defining that command. This logic'
633 646 1 ``Command Request`` frame, 0 or more ``Command Argument`` frames,
634 647 and 0 or more ``Command Data`` frames.
635 648
649 All frames composing a single command request MUST be associated with
650 the same ``Request ID``.
651
652 Clients MAY send additional command requests without waiting on the
653 response to a previous command request. If they do so, they MUST ensure
654 that the ``Request ID`` field of outbound frames does not conflict
655 with that of an active ``Request ID`` whose response has not yet been
656 fully received.
657
658 Servers MAY respond to commands in a different order than they were
659 sent over the wire. Clients MUST be prepared to deal with this. Servers
660 also MAY start executing commands in a different order than they were
661 received, or MAY execute multiple commands concurrently.
662
663 If there is a dependency between commands or a race condition between
664 commands executing (e.g. a read-only command that depends on the results
665 of a command that mutates the repository), then clients MUST NOT send
666 frames issuing a command until a response to all dependent commands has
667 been received.
668 TODO think about whether we should express dependencies between commands
669 to avoid roundtrip latency.
670
636 671 Argument frames are the recommended mechanism for transferring fixed
637 672 sets of parameters to a command. Data frames are appropriate for
638 673 transferring variable data. A similar comparison would be to HTTP:
@@ -19,7 +19,7 b' from . import ('
19 19 util,
20 20 )
21 21
22 FRAME_HEADER_SIZE = 4
22 FRAME_HEADER_SIZE = 6
23 23 DEFAULT_MAX_FRAME_SIZE = 32768
24 24
25 25 FRAME_TYPE_COMMAND_NAME = 0x01
@@ -89,28 +89,43 b' FRAME_TYPE_FLAGS = {'
89 89
90 90 ARGUMENT_FRAME_HEADER = struct.Struct(r'<HH')
91 91
92 def makeframe(frametype, frameflags, payload):
92 def makeframe(requestid, frametype, frameflags, payload):
93 93 """Assemble a frame into a byte array."""
94 94 # TODO assert size of payload.
95 95 frame = bytearray(FRAME_HEADER_SIZE + len(payload))
96 96
97 # 24 bits length
98 # 16 bits request id
99 # 4 bits type
100 # 4 bits flags
101
97 102 l = struct.pack(r'<I', len(payload))
98 103 frame[0:3] = l[0:3]
99 frame[3] = (frametype << 4) | frameflags
100 frame[4:] = payload
104 struct.pack_into(r'<H', frame, 3, requestid)
105 frame[5] = (frametype << 4) | frameflags
106 frame[6:] = payload
101 107
102 108 return frame
103 109
104 110 def makeframefromhumanstring(s):
105 """Given a string of the form: <type> <flags> <payload>, creates a frame.
111 """Create a frame from a human readable string
112
113 Strings have the form:
114
115 <request-id> <type> <flags> <payload>
106 116
107 117 This can be used by user-facing applications and tests for creating
108 118 frames easily without having to type out a bunch of constants.
109 119
120 Request ID is an integer.
121
110 122 Frame type and flags can be specified by integer or named constant.
123
111 124 Flags can be delimited by `|` to bitwise OR them together.
112 125 """
113 frametype, frameflags, payload = s.split(b' ', 2)
126 requestid, frametype, frameflags, payload = s.split(b' ', 3)
127
128 requestid = int(requestid)
114 129
115 130 if frametype in FRAME_TYPES:
116 131 frametype = FRAME_TYPES[frametype]
@@ -127,7 +142,7 b' def makeframefromhumanstring(s):'
127 142
128 143 payload = util.unescapestr(payload)
129 144
130 return makeframe(frametype, finalflags, payload)
145 return makeframe(requestid, frametype, finalflags, payload)
131 146
132 147 def parseheader(data):
133 148 """Parse a unified framing protocol frame header from a buffer.
@@ -140,12 +155,13 b' def parseheader(data):'
140 155 # 4 bits frame flags
141 156 # ... payload
142 157 framelength = data[0] + 256 * data[1] + 16384 * data[2]
143 typeflags = data[3]
158 requestid = struct.unpack_from(r'<H', data, 3)[0]
159 typeflags = data[5]
144 160
145 161 frametype = (typeflags & 0xf0) >> 4
146 162 frameflags = typeflags & 0x0f
147 163
148 return frametype, frameflags, framelength
164 return requestid, frametype, frameflags, framelength
149 165
150 166 def readframe(fh):
151 167 """Read a unified framing protocol frame from a file object.
@@ -165,16 +181,16 b' def readframe(fh):'
165 181 raise error.Abort(_('received incomplete frame: got %d bytes: %s') %
166 182 (readcount, header))
167 183
168 frametype, frameflags, framelength = parseheader(header)
184 requestid, frametype, frameflags, framelength = parseheader(header)
169 185
170 186 payload = fh.read(framelength)
171 187 if len(payload) != framelength:
172 188 raise error.Abort(_('frame length error: expected %d; got %d') %
173 189 (framelength, len(payload)))
174 190
175 return frametype, frameflags, payload
191 return requestid, frametype, frameflags, payload
176 192
177 def createcommandframes(cmd, args, datafh=None):
193 def createcommandframes(requestid, cmd, args, datafh=None):
178 194 """Create frames necessary to transmit a request to run a command.
179 195
180 196 This is a generator of bytearrays. Each item represents a frame
@@ -189,7 +205,7 b' def createcommandframes(cmd, args, dataf'
189 205 if not flags:
190 206 flags |= FLAG_COMMAND_NAME_EOS
191 207
192 yield makeframe(FRAME_TYPE_COMMAND_NAME, flags, cmd)
208 yield makeframe(requestid, FRAME_TYPE_COMMAND_NAME, flags, cmd)
193 209
194 210 for i, k in enumerate(sorted(args)):
195 211 v = args[k]
@@ -205,7 +221,7 b' def createcommandframes(cmd, args, dataf'
205 221 payload[offset:offset + len(v)] = v
206 222
207 223 flags = FLAG_COMMAND_ARGUMENT_EOA if last else 0
208 yield makeframe(FRAME_TYPE_COMMAND_ARGUMENT, flags, payload)
224 yield makeframe(requestid, FRAME_TYPE_COMMAND_ARGUMENT, flags, payload)
209 225
210 226 if datafh:
211 227 while True:
@@ -219,12 +235,12 b' def createcommandframes(cmd, args, dataf'
219 235 assert datafh.read(1) == b''
220 236 done = True
221 237
222 yield makeframe(FRAME_TYPE_COMMAND_DATA, flags, data)
238 yield makeframe(requestid, FRAME_TYPE_COMMAND_DATA, flags, data)
223 239
224 240 if done:
225 241 break
226 242
227 def createbytesresponseframesfrombytes(data,
243 def createbytesresponseframesfrombytes(requestid, data,
228 244 maxframesize=DEFAULT_MAX_FRAME_SIZE):
229 245 """Create a raw frame to send a bytes response from static bytes input.
230 246
@@ -233,7 +249,7 b' def createbytesresponseframesfrombytes(d'
233 249
234 250 # Simple case of a single frame.
235 251 if len(data) <= maxframesize:
236 yield makeframe(FRAME_TYPE_BYTES_RESPONSE,
252 yield makeframe(requestid, FRAME_TYPE_BYTES_RESPONSE,
237 253 FLAG_BYTES_RESPONSE_EOS, data)
238 254 return
239 255
@@ -248,12 +264,12 b' def createbytesresponseframesfrombytes(d'
248 264 else:
249 265 flags = FLAG_BYTES_RESPONSE_CONTINUATION
250 266
251 yield makeframe(FRAME_TYPE_BYTES_RESPONSE, flags, chunk)
267 yield makeframe(requestid, FRAME_TYPE_BYTES_RESPONSE, flags, chunk)
252 268
253 269 if done:
254 270 break
255 271
256 def createerrorframe(msg, protocol=False, application=False):
272 def createerrorframe(requestid, msg, protocol=False, application=False):
257 273 # TODO properly handle frame size limits.
258 274 assert len(msg) <= DEFAULT_MAX_FRAME_SIZE
259 275
@@ -263,7 +279,7 b' def createerrorframe(msg, protocol=False'
263 279 if application:
264 280 flags |= FLAG_ERROR_RESPONSE_APPLICATION
265 281
266 yield makeframe(FRAME_TYPE_ERROR_RESPONSE, flags, msg)
282 yield makeframe(requestid, FRAME_TYPE_ERROR_RESPONSE, flags, msg)
267 283
268 284 class serverreactor(object):
269 285 """Holds state of a server handling frame-based protocol requests.
@@ -326,6 +342,7 b' class serverreactor(object):'
326 342 self._deferoutput = deferoutput
327 343 self._state = 'idle'
328 344 self._bufferedframegens = []
345 self._activerequestid = None
329 346 self._activecommand = None
330 347 self._activeargs = None
331 348 self._activedata = None
@@ -334,7 +351,7 b' class serverreactor(object):'
334 351 self._activeargname = None
335 352 self._activeargchunks = None
336 353
337 def onframerecv(self, frametype, frameflags, payload):
354 def onframerecv(self, requestid, frametype, frameflags, payload):
338 355 """Process a frame that has been received off the wire.
339 356
340 357 Returns a dict with an ``action`` key that details what action,
@@ -351,14 +368,14 b' class serverreactor(object):'
351 368 if not meth:
352 369 raise error.ProgrammingError('unhandled state: %s' % self._state)
353 370
354 return meth(frametype, frameflags, payload)
371 return meth(requestid, frametype, frameflags, payload)
355 372
356 def onbytesresponseready(self, data):
373 def onbytesresponseready(self, requestid, data):
357 374 """Signal that a bytes response is ready to be sent to the client.
358 375
359 376 The raw bytes response is passed as an argument.
360 377 """
361 framegen = createbytesresponseframesfrombytes(data)
378 framegen = createbytesresponseframesfrombytes(requestid, data)
362 379
363 380 if self._deferoutput:
364 381 self._bufferedframegens.append(framegen)
@@ -387,9 +404,9 b' class serverreactor(object):'
387 404 'framegen': makegen(),
388 405 }
389 406
390 def onapplicationerror(self, msg):
407 def onapplicationerror(self, requestid, msg):
391 408 return 'sendframes', {
392 'framegen': createerrorframe(msg, application=True),
409 'framegen': createerrorframe(requestid, msg, application=True),
393 410 }
394 411
395 412 def _makeerrorresult(self, msg):
@@ -399,6 +416,7 b' class serverreactor(object):'
399 416
400 417 def _makeruncommandresult(self):
401 418 return 'runcommand', {
419 'requestid': self._activerequestid,
402 420 'command': self._activecommand,
403 421 'args': self._activeargs,
404 422 'data': self._activedata.getvalue() if self._activedata else None,
@@ -409,7 +427,7 b' class serverreactor(object):'
409 427 'state': self._state,
410 428 }
411 429
412 def _onframeidle(self, frametype, frameflags, payload):
430 def _onframeidle(self, requestid, frametype, frameflags, payload):
413 431 # The only frame type that should be received in this state is a
414 432 # command request.
415 433 if frametype != FRAME_TYPE_COMMAND_NAME:
@@ -417,6 +435,7 b' class serverreactor(object):'
417 435 return self._makeerrorresult(
418 436 _('expected command frame; got %d') % frametype)
419 437
438 self._activerequestid = requestid
420 439 self._activecommand = payload
421 440 self._activeargs = {}
422 441 self._activedata = None
@@ -439,7 +458,7 b' class serverreactor(object):'
439 458 return self._makeerrorresult(_('missing frame flags on '
440 459 'command frame'))
441 460
442 def _onframereceivingargs(self, frametype, frameflags, payload):
461 def _onframereceivingargs(self, requestid, frametype, frameflags, payload):
443 462 if frametype != FRAME_TYPE_COMMAND_ARGUMENT:
444 463 self._state = 'errored'
445 464 return self._makeerrorresult(_('expected command argument '
@@ -492,7 +511,7 b' class serverreactor(object):'
492 511 else:
493 512 return self._makewantframeresult()
494 513
495 def _onframereceivingdata(self, frametype, frameflags, payload):
514 def _onframereceivingdata(self, requestid, frametype, frameflags, payload):
496 515 if frametype != FRAME_TYPE_COMMAND_DATA:
497 516 self._state = 'errored'
498 517 return self._makeerrorresult(_('expected command data frame; '
@@ -512,5 +531,5 b' class serverreactor(object):'
512 531 return self._makeerrorresult(_('command data frame without '
513 532 'flags'))
514 533
515 def _onframeerrored(self, frametype, frameflags, payload):
534 def _onframeerrored(self, requestid, frametype, frameflags, payload):
516 535 return self._makeerrorresult(_('server already errored'))
@@ -33,7 +33,7 b' HTTP_OK = 200'
33 33 HGTYPE = 'application/mercurial-0.1'
34 34 HGTYPE2 = 'application/mercurial-0.2'
35 35 HGERRTYPE = 'application/hg-error'
36 FRAMINGTYPE = b'application/mercurial-exp-framing-0001'
36 FRAMINGTYPE = b'application/mercurial-exp-framing-0002'
37 37
38 38 HTTPV2 = wireprototypes.HTTPV2
39 39 SSHV1 = wireprototypes.SSHV1
@@ -394,10 +394,12 b' def _processhttpv2reflectrequest(ui, rep'
394 394 states.append(b'received: <no frame>')
395 395 break
396 396
397 frametype, frameflags, payload = frame
398 states.append(b'received: %d %d %s' % (frametype, frameflags, payload))
397 requestid, frametype, frameflags, payload = frame
398 states.append(b'received: %d %d %d %s' % (frametype, frameflags,
399 requestid, payload))
399 400
400 action, meta = reactor.onframerecv(frametype, frameflags, payload)
401 action, meta = reactor.onframerecv(requestid, frametype, frameflags,
402 payload)
401 403 states.append(json.dumps((action, meta), sort_keys=True,
402 404 separators=(', ', ': ')))
403 405
@@ -517,7 +519,8 b' def _httpv2runcommand(ui, repo, req, res'
517 519 res.headers[b'Content-Type'] = FRAMINGTYPE
518 520
519 521 if isinstance(rsp, wireprototypes.bytesresponse):
520 action, meta = reactor.onbytesresponseready(rsp.data)
522 action, meta = reactor.onbytesresponseready(command['requestid'],
523 rsp.data)
521 524 else:
522 525 action, meta = reactor.onapplicationerror(
523 526 _('unhandled response type from wire proto command'))
@@ -1,5 +1,5 b''
1 1 $ HTTPV2=exp-http-v2-0001
2 $ MEDIATYPE=application/mercurial-exp-framing-0001
2 $ MEDIATYPE=application/mercurial-exp-framing-0002
3 3
4 4 $ send() {
5 5 > hg --verbose debugwireproto --peer raw http://$LOCALIP:$HGPORT/
@@ -122,7 +122,7 b' Missing Accept header results in 406'
122 122 s> Content-Type: text/plain\r\n
123 123 s> Content-Length: 85\r\n
124 124 s> \r\n
125 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0001\n
125 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0002\n
126 126
127 127 Bad Accept header results in 406
128 128
@@ -145,7 +145,7 b' Bad Accept header results in 406'
145 145 s> Content-Type: text/plain\r\n
146 146 s> Content-Length: 85\r\n
147 147 s> \r\n
148 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0001\n
148 s> client MUST specify Accept header with value: application/mercurial-exp-framing-0002\n
149 149
150 150 Bad Content-Type header results in 415
151 151
@@ -158,7 +158,7 b' Bad Content-Type header results in 415'
158 158 using raw connection to peer
159 159 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
160 160 s> Accept-Encoding: identity\r\n
161 s> accept: application/mercurial-exp-framing-0001\r\n
161 s> accept: application/mercurial-exp-framing-0002\r\n
162 162 s> content-type: badmedia\r\n
163 163 s> user-agent: test\r\n
164 164 s> host: $LOCALIP:$HGPORT\r\n (glob)
@@ -170,7 +170,7 b' Bad Content-Type header results in 415'
170 170 s> Content-Type: text/plain\r\n
171 171 s> Content-Length: 88\r\n
172 172 s> \r\n
173 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0001\n
173 s> client MUST send Content-Type header with value: application/mercurial-exp-framing-0002\n
174 174
175 175 Request to read-only command works out of the box
176 176
@@ -179,27 +179,27 b' Request to read-only command works out o'
179 179 > accept: $MEDIATYPE
180 180 > content-type: $MEDIATYPE
181 181 > user-agent: test
182 > frame command-name eos customreadonly
182 > frame 1 command-name eos customreadonly
183 183 > EOF
184 184 using raw connection to peer
185 185 s> POST /api/exp-http-v2-0001/ro/customreadonly HTTP/1.1\r\n
186 186 s> Accept-Encoding: identity\r\n
187 s> accept: application/mercurial-exp-framing-0001\r\n
188 s> content-type: application/mercurial-exp-framing-0001\r\n
187 s> accept: application/mercurial-exp-framing-0002\r\n
188 s> content-type: application/mercurial-exp-framing-0002\r\n
189 189 s> user-agent: test\r\n
190 s> content-length: 18\r\n
190 s> *\r\n (glob)
191 191 s> host: $LOCALIP:$HGPORT\r\n (glob)
192 192 s> \r\n
193 s> \x0e\x00\x00\x11customreadonly
193 s> \x0e\x00\x00\x01\x00\x11customreadonly
194 194 s> makefile('rb', None)
195 195 s> HTTP/1.1 200 OK\r\n
196 196 s> Server: testing stub value\r\n
197 197 s> Date: $HTTP_DATE$\r\n
198 s> Content-Type: application/mercurial-exp-framing-0001\r\n
198 s> Content-Type: application/mercurial-exp-framing-0002\r\n
199 199 s> Transfer-Encoding: chunked\r\n
200 200 s> \r\n
201 s> 21\r\n
202 s> \x1d\x00\x00Bcustomreadonly bytes response
201 s> 23\r\n
202 s> \x1d\x00\x00\x01\x00Bcustomreadonly bytes response
203 203 s> \r\n
204 204 s> 0\r\n
205 205 s> \r\n
@@ -290,27 +290,27 b' Authorized request for valid read-write '
290 290 > user-agent: test
291 291 > accept: $MEDIATYPE
292 292 > content-type: $MEDIATYPE
293 > frame command-name eos customreadonly
293 > frame 1 command-name eos customreadonly
294 294 > EOF
295 295 using raw connection to peer
296 296 s> POST /api/exp-http-v2-0001/rw/customreadonly HTTP/1.1\r\n
297 297 s> Accept-Encoding: identity\r\n
298 s> accept: application/mercurial-exp-framing-0001\r\n
299 s> content-type: application/mercurial-exp-framing-0001\r\n
298 s> accept: application/mercurial-exp-framing-0002\r\n
299 s> content-type: application/mercurial-exp-framing-0002\r\n
300 300 s> user-agent: test\r\n
301 s> content-length: 18\r\n
301 s> content-length: 20\r\n
302 302 s> host: $LOCALIP:$HGPORT\r\n (glob)
303 303 s> \r\n
304 s> \x0e\x00\x00\x11customreadonly
304 s> \x0e\x00\x00\x01\x00\x11customreadonly
305 305 s> makefile('rb', None)
306 306 s> HTTP/1.1 200 OK\r\n
307 307 s> Server: testing stub value\r\n
308 308 s> Date: $HTTP_DATE$\r\n
309 s> Content-Type: application/mercurial-exp-framing-0001\r\n
309 s> Content-Type: application/mercurial-exp-framing-0002\r\n
310 310 s> Transfer-Encoding: chunked\r\n
311 311 s> \r\n
312 s> 21\r\n
313 s> \x1d\x00\x00Bcustomreadonly bytes response
312 s> 23\r\n
313 s> \x1d\x00\x00\x01\x00Bcustomreadonly bytes response
314 314 s> \r\n
315 315 s> 0\r\n
316 316 s> \r\n
@@ -325,7 +325,7 b' Authorized request for unknown command i'
325 325 using raw connection to peer
326 326 s> POST /api/exp-http-v2-0001/rw/badcommand HTTP/1.1\r\n
327 327 s> Accept-Encoding: identity\r\n
328 s> accept: application/mercurial-exp-framing-0001\r\n
328 s> accept: application/mercurial-exp-framing-0002\r\n
329 329 s> user-agent: test\r\n
330 330 s> host: $LOCALIP:$HGPORT\r\n (glob)
331 331 s> \r\n
@@ -382,33 +382,33 b' Command frames can be reflected via debu'
382 382 > accept: $MEDIATYPE
383 383 > content-type: $MEDIATYPE
384 384 > user-agent: test
385 > frame command-name have-args command1
386 > frame command-argument 0 \x03\x00\x04\x00fooval1
387 > frame command-argument eoa \x04\x00\x03\x00bar1val
385 > frame 1 command-name have-args command1
386 > frame 1 command-argument 0 \x03\x00\x04\x00fooval1
387 > frame 1 command-argument eoa \x04\x00\x03\x00bar1val
388 388 > EOF
389 389 using raw connection to peer
390 390 s> POST /api/exp-http-v2-0001/ro/debugreflect HTTP/1.1\r\n
391 391 s> Accept-Encoding: identity\r\n
392 s> accept: application/mercurial-exp-framing-0001\r\n
393 s> content-type: application/mercurial-exp-framing-0001\r\n
392 s> accept: application/mercurial-exp-framing-0002\r\n
393 s> content-type: application/mercurial-exp-framing-0002\r\n
394 394 s> user-agent: test\r\n
395 s> content-length: 42\r\n
395 s> content-length: 48\r\n
396 396 s> host: $LOCALIP:$HGPORT\r\n (glob)
397 397 s> \r\n
398 s> \x08\x00\x00\x12command1\x0b\x00\x00 \x03\x00\x04\x00fooval1\x0b\x00\x00"\x04\x00\x03\x00bar1val
398 s> \x08\x00\x00\x01\x00\x12command1\x0b\x00\x00\x01\x00 \x03\x00\x04\x00fooval1\x0b\x00\x00\x01\x00"\x04\x00\x03\x00bar1val
399 399 s> makefile('rb', None)
400 400 s> HTTP/1.1 200 OK\r\n
401 401 s> Server: testing stub value\r\n
402 402 s> Date: $HTTP_DATE$\r\n
403 403 s> Content-Type: text/plain\r\n
404 s> Content-Length: 310\r\n
404 s> Content-Length: 332\r\n
405 405 s> \r\n
406 s> received: 1 2 command1\n
406 s> received: 1 2 1 command1\n
407 407 s> ["wantframe", {"state": "command-receiving-args"}]\n
408 s> received: 2 0 \x03\x00\x04\x00fooval1\n
408 s> received: 2 0 1 \x03\x00\x04\x00fooval1\n
409 409 s> ["wantframe", {"state": "command-receiving-args"}]\n
410 s> received: 2 2 \x04\x00\x03\x00bar1val\n
411 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null}]\n
410 s> received: 2 2 1 \x04\x00\x03\x00bar1val\n
411 s> ["runcommand", {"args": {"bar1": "val", "foo": "val1"}, "command": "command1", "data": null, "requestid": 1}]\n
412 412 s> received: <no frame>\n
413 413 s> {"action": "noop"}
414 414
@@ -18,52 +18,53 b' def sendframes(reactor, gen):'
18 18 Emits a generator of results from ``onframerecv()`` calls.
19 19 """
20 20 for frame in gen:
21 frametype, frameflags, framelength = framing.parseheader(frame)
21 rid, frametype, frameflags, framelength = framing.parseheader(frame)
22 22 payload = frame[framing.FRAME_HEADER_SIZE:]
23 23 assert len(payload) == framelength
24 24
25 yield reactor.onframerecv(frametype, frameflags, payload)
25 yield reactor.onframerecv(rid, frametype, frameflags, payload)
26 26
27 def sendcommandframes(reactor, cmd, args, datafh=None):
27 def sendcommandframes(reactor, rid, cmd, args, datafh=None):
28 28 """Generate frames to run a command and send them to a reactor."""
29 return sendframes(reactor, framing.createcommandframes(cmd, args, datafh))
29 return sendframes(reactor,
30 framing.createcommandframes(rid, cmd, args, datafh))
30 31
31 32 class FrameTests(unittest.TestCase):
32 33 def testdataexactframesize(self):
33 34 data = util.bytesio(b'x' * framing.DEFAULT_MAX_FRAME_SIZE)
34 35
35 frames = list(framing.createcommandframes(b'command', {}, data))
36 frames = list(framing.createcommandframes(1, b'command', {}, data))
36 37 self.assertEqual(frames, [
37 ffs(b'command-name have-data command'),
38 ffs(b'command-data continuation %s' % data.getvalue()),
39 ffs(b'command-data eos ')
38 ffs(b'1 command-name have-data command'),
39 ffs(b'1 command-data continuation %s' % data.getvalue()),
40 ffs(b'1 command-data eos ')
40 41 ])
41 42
42 43 def testdatamultipleframes(self):
43 44 data = util.bytesio(b'x' * (framing.DEFAULT_MAX_FRAME_SIZE + 1))
44 frames = list(framing.createcommandframes(b'command', {}, data))
45 frames = list(framing.createcommandframes(1, b'command', {}, data))
45 46 self.assertEqual(frames, [
46 ffs(b'command-name have-data command'),
47 ffs(b'command-data continuation %s' % (
47 ffs(b'1 command-name have-data command'),
48 ffs(b'1 command-data continuation %s' % (
48 49 b'x' * framing.DEFAULT_MAX_FRAME_SIZE)),
49 ffs(b'command-data eos x'),
50 ffs(b'1 command-data eos x'),
50 51 ])
51 52
52 53 def testargsanddata(self):
53 54 data = util.bytesio(b'x' * 100)
54 55
55 frames = list(framing.createcommandframes(b'command', {
56 frames = list(framing.createcommandframes(1, b'command', {
56 57 b'key1': b'key1value',
57 58 b'key2': b'key2value',
58 59 b'key3': b'key3value',
59 60 }, data))
60 61
61 62 self.assertEqual(frames, [
62 ffs(b'command-name have-args|have-data command'),
63 ffs(br'command-argument 0 \x04\x00\x09\x00key1key1value'),
64 ffs(br'command-argument 0 \x04\x00\x09\x00key2key2value'),
65 ffs(br'command-argument eoa \x04\x00\x09\x00key3key3value'),
66 ffs(b'command-data eos %s' % data.getvalue()),
63 ffs(b'1 command-name have-args|have-data command'),
64 ffs(br'1 command-argument 0 \x04\x00\x09\x00key1key1value'),
65 ffs(br'1 command-argument 0 \x04\x00\x09\x00key2key2value'),
66 ffs(br'1 command-argument eoa \x04\x00\x09\x00key3key3value'),
67 ffs(b'1 command-data eos %s' % data.getvalue()),
67 68 ])
68 69
69 70 class ServerReactorTests(unittest.TestCase):
@@ -86,10 +87,11 b' class ServerReactorTests(unittest.TestCa'
86 87 def test1framecommand(self):
87 88 """Receiving a command in a single frame yields request to run it."""
88 89 reactor = makereactor()
89 results = list(sendcommandframes(reactor, b'mycommand', {}))
90 results = list(sendcommandframes(reactor, 1, b'mycommand', {}))
90 91 self.assertEqual(len(results), 1)
91 92 self.assertaction(results[0], 'runcommand')
92 93 self.assertEqual(results[0][1], {
94 'requestid': 1,
93 95 'command': b'mycommand',
94 96 'args': {},
95 97 'data': None,
@@ -100,12 +102,13 b' class ServerReactorTests(unittest.TestCa'
100 102
101 103 def test1argument(self):
102 104 reactor = makereactor()
103 results = list(sendcommandframes(reactor, b'mycommand',
105 results = list(sendcommandframes(reactor, 41, b'mycommand',
104 106 {b'foo': b'bar'}))
105 107 self.assertEqual(len(results), 2)
106 108 self.assertaction(results[0], 'wantframe')
107 109 self.assertaction(results[1], 'runcommand')
108 110 self.assertEqual(results[1][1], {
111 'requestid': 41,
109 112 'command': b'mycommand',
110 113 'args': {b'foo': b'bar'},
111 114 'data': None,
@@ -113,13 +116,14 b' class ServerReactorTests(unittest.TestCa'
113 116
114 117 def testmultiarguments(self):
115 118 reactor = makereactor()
116 results = list(sendcommandframes(reactor, b'mycommand',
119 results = list(sendcommandframes(reactor, 1, b'mycommand',
117 120 {b'foo': b'bar', b'biz': b'baz'}))
118 121 self.assertEqual(len(results), 3)
119 122 self.assertaction(results[0], 'wantframe')
120 123 self.assertaction(results[1], 'wantframe')
121 124 self.assertaction(results[2], 'runcommand')
122 125 self.assertEqual(results[2][1], {
126 'requestid': 1,
123 127 'command': b'mycommand',
124 128 'args': {b'foo': b'bar', b'biz': b'baz'},
125 129 'data': None,
@@ -127,12 +131,13 b' class ServerReactorTests(unittest.TestCa'
127 131
128 132 def testsimplecommanddata(self):
129 133 reactor = makereactor()
130 results = list(sendcommandframes(reactor, b'mycommand', {},
134 results = list(sendcommandframes(reactor, 1, b'mycommand', {},
131 135 util.bytesio(b'data!')))
132 136 self.assertEqual(len(results), 2)
133 137 self.assertaction(results[0], 'wantframe')
134 138 self.assertaction(results[1], 'runcommand')
135 139 self.assertEqual(results[1][1], {
140 'requestid': 1,
136 141 'command': b'mycommand',
137 142 'args': {},
138 143 'data': b'data!',
@@ -140,10 +145,10 b' class ServerReactorTests(unittest.TestCa'
140 145
141 146 def testmultipledataframes(self):
142 147 frames = [
143 ffs(b'command-name have-data mycommand'),
144 ffs(b'command-data continuation data1'),
145 ffs(b'command-data continuation data2'),
146 ffs(b'command-data eos data3'),
148 ffs(b'1 command-name have-data mycommand'),
149 ffs(b'1 command-data continuation data1'),
150 ffs(b'1 command-data continuation data2'),
151 ffs(b'1 command-data eos data3'),
147 152 ]
148 153
149 154 reactor = makereactor()
@@ -153,6 +158,7 b' class ServerReactorTests(unittest.TestCa'
153 158 self.assertaction(results[i], 'wantframe')
154 159 self.assertaction(results[3], 'runcommand')
155 160 self.assertEqual(results[3][1], {
161 'requestid': 1,
156 162 'command': b'mycommand',
157 163 'args': {},
158 164 'data': b'data1data2data3',
@@ -160,11 +166,11 b' class ServerReactorTests(unittest.TestCa'
160 166
161 167 def testargumentanddata(self):
162 168 frames = [
163 ffs(b'command-name have-args|have-data command'),
164 ffs(br'command-argument 0 \x03\x00\x03\x00keyval'),
165 ffs(br'command-argument eoa \x03\x00\x03\x00foobar'),
166 ffs(b'command-data continuation value1'),
167 ffs(b'command-data eos value2'),
169 ffs(b'1 command-name have-args|have-data command'),
170 ffs(br'1 command-argument 0 \x03\x00\x03\x00keyval'),
171 ffs(br'1 command-argument eoa \x03\x00\x03\x00foobar'),
172 ffs(b'1 command-data continuation value1'),
173 ffs(b'1 command-data eos value2'),
168 174 ]
169 175
170 176 reactor = makereactor()
@@ -172,6 +178,7 b' class ServerReactorTests(unittest.TestCa'
172 178
173 179 self.assertaction(results[-1], 'runcommand')
174 180 self.assertEqual(results[-1][1], {
181 'requestid': 1,
175 182 'command': b'command',
176 183 'args': {
177 184 b'key': b'val',
@@ -183,7 +190,7 b' class ServerReactorTests(unittest.TestCa'
183 190 def testunexpectedcommandargument(self):
184 191 """Command argument frame when not running a command is an error."""
185 192 result = self._sendsingleframe(makereactor(),
186 b'command-argument 0 ignored')
193 b'1 command-argument 0 ignored')
187 194 self.assertaction(result, 'error')
188 195 self.assertEqual(result[1], {
189 196 'message': b'expected command frame; got 2',
@@ -192,7 +199,7 b' class ServerReactorTests(unittest.TestCa'
192 199 def testunexpectedcommanddata(self):
193 200 """Command argument frame when not running a command is an error."""
194 201 result = self._sendsingleframe(makereactor(),
195 b'command-data 0 ignored')
202 b'1 command-data 0 ignored')
196 203 self.assertaction(result, 'error')
197 204 self.assertEqual(result[1], {
198 205 'message': b'expected command frame; got 3',
@@ -201,7 +208,7 b' class ServerReactorTests(unittest.TestCa'
201 208 def testmissingcommandframeflags(self):
202 209 """Command name frame must have flags set."""
203 210 result = self._sendsingleframe(makereactor(),
204 b'command-name 0 command')
211 b'1 command-name 0 command')
205 212 self.assertaction(result, 'error')
206 213 self.assertEqual(result[1], {
207 214 'message': b'missing frame flags on command frame',
@@ -209,8 +216,8 b' class ServerReactorTests(unittest.TestCa'
209 216
210 217 def testmissingargumentframe(self):
211 218 frames = [
212 ffs(b'command-name have-args command'),
213 ffs(b'command-name 0 ignored'),
219 ffs(b'1 command-name have-args command'),
220 ffs(b'1 command-name 0 ignored'),
214 221 ]
215 222
216 223 results = list(sendframes(makereactor(), frames))
@@ -224,8 +231,8 b' class ServerReactorTests(unittest.TestCa'
224 231 def testincompleteargumentname(self):
225 232 """Argument frame with incomplete name."""
226 233 frames = [
227 ffs(b'command-name have-args command1'),
228 ffs(br'command-argument eoa \x04\x00\xde\xadfoo'),
234 ffs(b'1 command-name have-args command1'),
235 ffs(br'1 command-argument eoa \x04\x00\xde\xadfoo'),
229 236 ]
230 237
231 238 results = list(sendframes(makereactor(), frames))
@@ -239,8 +246,8 b' class ServerReactorTests(unittest.TestCa'
239 246 def testincompleteargumentvalue(self):
240 247 """Argument frame with incomplete value."""
241 248 frames = [
242 ffs(b'command-name have-args command'),
243 ffs(br'command-argument eoa \x03\x00\xaa\xaafoopartialvalue'),
249 ffs(b'1 command-name have-args command'),
250 ffs(br'1 command-argument eoa \x03\x00\xaa\xaafoopartialvalue'),
244 251 ]
245 252
246 253 results = list(sendframes(makereactor(), frames))
@@ -253,8 +260,8 b' class ServerReactorTests(unittest.TestCa'
253 260
254 261 def testmissingcommanddataframe(self):
255 262 frames = [
256 ffs(b'command-name have-data command1'),
257 ffs(b'command-name eos command2'),
263 ffs(b'1 command-name have-data command1'),
264 ffs(b'1 command-name eos command2'),
258 265 ]
259 266 results = list(sendframes(makereactor(), frames))
260 267 self.assertEqual(len(results), 2)
@@ -266,8 +273,8 b' class ServerReactorTests(unittest.TestCa'
266 273
267 274 def testmissingcommanddataframeflags(self):
268 275 frames = [
269 ffs(b'command-name have-data command1'),
270 ffs(b'command-data 0 data'),
276 ffs(b'1 command-name have-data command1'),
277 ffs(b'1 command-data 0 data'),
271 278 ]
272 279 results = list(sendframes(makereactor(), frames))
273 280 self.assertEqual(len(results), 2)
@@ -280,12 +287,12 b' class ServerReactorTests(unittest.TestCa'
280 287 def testsimpleresponse(self):
281 288 """Bytes response to command sends result frames."""
282 289 reactor = makereactor()
283 list(sendcommandframes(reactor, b'mycommand', {}))
290 list(sendcommandframes(reactor, 1, b'mycommand', {}))
284 291
285 result = reactor.onbytesresponseready(b'response')
292 result = reactor.onbytesresponseready(1, b'response')
286 293 self.assertaction(result, 'sendframes')
287 294 self.assertframesequal(result[1]['framegen'], [
288 b'bytes-response eos response',
295 b'1 bytes-response eos response',
289 296 ])
290 297
291 298 def testmultiframeresponse(self):
@@ -294,54 +301,73 b' class ServerReactorTests(unittest.TestCa'
294 301 second = b'y' * 100
295 302
296 303 reactor = makereactor()
297 list(sendcommandframes(reactor, b'mycommand', {}))
304 list(sendcommandframes(reactor, 1, b'mycommand', {}))
298 305
299 result = reactor.onbytesresponseready(first + second)
306 result = reactor.onbytesresponseready(1, first + second)
300 307 self.assertaction(result, 'sendframes')
301 308 self.assertframesequal(result[1]['framegen'], [
302 b'bytes-response continuation %s' % first,
303 b'bytes-response eos %s' % second,
309 b'1 bytes-response continuation %s' % first,
310 b'1 bytes-response eos %s' % second,
304 311 ])
305 312
306 313 def testapplicationerror(self):
307 314 reactor = makereactor()
308 list(sendcommandframes(reactor, b'mycommand', {}))
315 list(sendcommandframes(reactor, 1, b'mycommand', {}))
309 316
310 result = reactor.onapplicationerror(b'some message')
317 result = reactor.onapplicationerror(1, b'some message')
311 318 self.assertaction(result, 'sendframes')
312 319 self.assertframesequal(result[1]['framegen'], [
313 b'error-response application some message',
320 b'1 error-response application some message',
314 321 ])
315 322
316 323 def test1commanddeferresponse(self):
317 324 """Responses when in deferred output mode are delayed until EOF."""
318 325 reactor = makereactor(deferoutput=True)
319 results = list(sendcommandframes(reactor, b'mycommand', {}))
326 results = list(sendcommandframes(reactor, 1, b'mycommand', {}))
320 327 self.assertEqual(len(results), 1)
321 328 self.assertaction(results[0], 'runcommand')
322 329
323 result = reactor.onbytesresponseready(b'response')
330 result = reactor.onbytesresponseready(1, b'response')
324 331 self.assertaction(result, 'noop')
325 332 result = reactor.oninputeof()
326 333 self.assertaction(result, 'sendframes')
327 334 self.assertframesequal(result[1]['framegen'], [
328 b'bytes-response eos response',
335 b'1 bytes-response eos response',
329 336 ])
330 337
331 338 def testmultiplecommanddeferresponse(self):
332 339 reactor = makereactor(deferoutput=True)
333 list(sendcommandframes(reactor, b'command1', {}))
334 list(sendcommandframes(reactor, b'command2', {}))
340 list(sendcommandframes(reactor, 1, b'command1', {}))
341 list(sendcommandframes(reactor, 3, b'command2', {}))
335 342
336 result = reactor.onbytesresponseready(b'response1')
343 result = reactor.onbytesresponseready(1, b'response1')
337 344 self.assertaction(result, 'noop')
338 result = reactor.onbytesresponseready(b'response2')
345 result = reactor.onbytesresponseready(3, b'response2')
339 346 self.assertaction(result, 'noop')
340 347 result = reactor.oninputeof()
341 348 self.assertaction(result, 'sendframes')
342 349 self.assertframesequal(result[1]['framegen'], [
343 b'bytes-response eos response1',
344 b'bytes-response eos response2'
350 b'1 bytes-response eos response1',
351 b'3 bytes-response eos response2'
352 ])
353
354 def testrequestidtracking(self):
355 reactor = makereactor(deferoutput=True)
356 list(sendcommandframes(reactor, 1, b'command1', {}))
357 list(sendcommandframes(reactor, 3, b'command2', {}))
358 list(sendcommandframes(reactor, 5, b'command3', {}))
359
360 # Register results for commands out of order.
361 reactor.onbytesresponseready(3, b'response3')
362 reactor.onbytesresponseready(1, b'response1')
363 reactor.onbytesresponseready(5, b'response5')
364
365 result = reactor.oninputeof()
366 self.assertaction(result, 'sendframes')
367 self.assertframesequal(result[1]['framegen'], [
368 b'3 bytes-response eos response3',
369 b'1 bytes-response eos response1',
370 b'5 bytes-response eos response5',
345 371 ])
346 372
347 373 if __name__ == '__main__':
General Comments 0
You need to be logged in to leave comments. Login now