comparison mercurial/wireprotoserver.py @ 37052:8c3c47362934

wireproto: implement basic frame reading and processing We just implemented support for writing frames. Now let's implement support for reading them. The bulk of the new code is for a class that maintains the state of a server. Essentially, you construct an instance, feed frames to it, and it tells you what you should do next. The design is inspired by the "sans I/O" movement and the reactor pattern. We don't want to perform I/O or any major blocking event during frame ingestion because this arbitrarily limits ways that server pieces can be implemented. For example, it makes it much harder to swap in an alternate implementation based on asyncio or do crazy things like have requests dispatch to other processes. We do still implement readframe() which does I/O. But it is decoupled from the server reactor. And important parsing of frame headers is a standalone function. So I/O is only needed to obtain frame data. Because testing server-side ingest is useful and difficult on running servers, we create a new "debugreflect" endpoint that will echo back to the client what was received and how it was interpreted. This could be useful for a server admin, someone implementing a client. But immediately, it is useful for testing: we're able to demonstrate that frames are parsed correctly and turned into requests to run commands without having to implement command dispatch on the server! In addition, we implement Python level unit tests for the reactor. This is vastly more efficient than sending requests to the "debugreflect" endpoint and vastly more powerful for advanced testing. Differential Revision: https://phab.mercurial-scm.org/D2852
author Gregory Szorc <gregory.szorc@gmail.com>
date Wed, 14 Mar 2018 15:25:06 -0700
parents 40206e227412
children cd0ca979a8b8
comparison
equal deleted inserted replaced
37051:40206e227412 37052:8c3c47362934
17 error, 17 error,
18 hook, 18 hook,
19 pycompat, 19 pycompat,
20 util, 20 util,
21 wireproto, 21 wireproto,
22 wireprotoframing,
22 wireprototypes, 23 wireprototypes,
23 ) 24 )
24 25
25 stringio = util.stringio 26 stringio = util.stringio
26 27
317 for k, v in e.headers: 318 for k, v in e.headers:
318 res.headers[k] = v 319 res.headers[k] = v
319 res.setbodybytes('permission denied') 320 res.setbodybytes('permission denied')
320 return 321 return
321 322
323 # We have a special endpoint to reflect the request back at the client.
324 if command == b'debugreflect':
325 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
326 return
327
322 if command not in wireproto.commands: 328 if command not in wireproto.commands:
323 res.status = b'404 Not Found' 329 res.status = b'404 Not Found'
324 res.headers[b'Content-Type'] = b'text/plain' 330 res.headers[b'Content-Type'] = b'text/plain'
325 res.setbodybytes(_('unknown wire protocol command: %s\n') % command) 331 res.setbodybytes(_('unknown wire protocol command: %s\n') % command)
326 return 332 return
341 res.headers[b'Content-Type'] = b'text/plain' 347 res.headers[b'Content-Type'] = b'text/plain'
342 res.setbodybytes(_('client MUST specify Accept header with value: %s\n') 348 res.setbodybytes(_('client MUST specify Accept header with value: %s\n')
343 % FRAMINGTYPE) 349 % FRAMINGTYPE)
344 return 350 return
345 351
346 if (b'Content-Type' in req.headers 352 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
347 and req.headers[b'Content-Type'] != FRAMINGTYPE):
348 res.status = b'415 Unsupported Media Type' 353 res.status = b'415 Unsupported Media Type'
349 # TODO we should send a response with appropriate media type, 354 # TODO we should send a response with appropriate media type,
350 # since client does Accept it. 355 # since client does Accept it.
351 res.headers[b'Content-Type'] = b'text/plain' 356 res.headers[b'Content-Type'] = b'text/plain'
352 res.setbodybytes(_('client MUST send Content-Type header with ' 357 res.setbodybytes(_('client MUST send Content-Type header with '
355 360
356 # We don't do anything meaningful yet. 361 # We don't do anything meaningful yet.
357 res.status = b'200 OK' 362 res.status = b'200 OK'
358 res.headers[b'Content-Type'] = b'text/plain' 363 res.headers[b'Content-Type'] = b'text/plain'
359 res.setbodybytes(b'/'.join(urlparts) + b'\n') 364 res.setbodybytes(b'/'.join(urlparts) + b'\n')
365
366 def _processhttpv2reflectrequest(ui, repo, req, res):
367 """Reads unified frame protocol request and dumps out state to client.
368
369 This special endpoint can be used to help debug the wire protocol.
370
371 Instead of routing the request through the normal dispatch mechanism,
372 we instead read all frames, decode them, and feed them into our state
373 tracker. We then dump the log of all that activity back out to the
374 client.
375 """
376 import json
377
378 # Reflection APIs have a history of being abused, accidentally disclosing
379 # sensitive data, etc. So we have a config knob.
380 if not ui.configbool('experimental', 'web.api.debugreflect'):
381 res.status = b'404 Not Found'
382 res.headers[b'Content-Type'] = b'text/plain'
383 res.setbodybytes(_('debugreflect service not available'))
384 return
385
386 # We assume we have a unified framing protocol request body.
387
388 reactor = wireprotoframing.serverreactor()
389 states = []
390
391 while True:
392 frame = wireprotoframing.readframe(req.bodyfh)
393
394 if not frame:
395 states.append(b'received: <no frame>')
396 break
397
398 frametype, frameflags, payload = frame
399 states.append(b'received: %d %d %s' % (frametype, frameflags, payload))
400
401 action, meta = reactor.onframerecv(frametype, frameflags, payload)
402 states.append(json.dumps((action, meta), sort_keys=True,
403 separators=(', ', ': ')))
404
405 res.status = b'200 OK'
406 res.headers[b'Content-Type'] = b'text/plain'
407 res.setbodybytes(b'\n'.join(states))
360 408
361 # Maps API name to metadata so custom API can be registered. 409 # Maps API name to metadata so custom API can be registered.
362 API_HANDLERS = { 410 API_HANDLERS = {
363 HTTPV2: { 411 HTTPV2: {
364 'config': ('experimental', 'web.api.http-v2'), 412 'config': ('experimental', 'web.api.http-v2'),