comparison mercurial/debugcommands.py @ 37051:40206e227412

wireproto: define and implement protocol for issuing requests The existing HTTP and SSH wire protocols suffer from a host of flaws and shortcomings. I've been wanting to rewrite the protocol for a while now. Supporting partial clone - which will require new wire protocol commands and capabilities - and other advanced server functionality will be much easier if we start from a clean slate and don't have to be constrained by limitations of the existing wire protocol. This commit starts to introduce a new data exchange format for use over the wire protocol. The new protocol is built on top of "frames," which are atomic units of metadata + data. Frames will make it easier to implement proxies and other mechanisms that want to inspect data without having to maintain state. The existing frame metadata is very minimal and it will evolve heavily. (We will eventually support things like concurrent requests, out-of-order responses, compression, side-channels for status updates, etc. Some of these will require additions to the frame header.) Another benefit of frames is that all reads are of a fixed size. A reader works by consuming a frame header, extracting the payload length, then reading that many bytes. No lookahead, buffering, or memory reallocations are needed. The new protocol attempts to be transport agnostic. I want all that's required to use the new protocol to be a pair of unidirectional, half-duplex pipes. (Yes, we will eventually make use of full-duplex pipes, but that's for another commit.) Notably, when the SSH transport switches to this new protocol, stderr will be unused. This is by design: the lack of stderr on HTTP harms protocol behavior there. By shoehorning everything into a pair of pipes, we can have more consistent behavior across transports. We currently only define the client side parts of the new protocol, specifically the bits for requesting that a command run. This keeps the new code and feature small and somewhat easy to review. We add support to `hg debugwireproto` for writing frames into HTTP request bodies. Our tests that issue commands to the new HTTP endpoint have been updated to transmit frames. The server bits haven't been touched to consume the frames yet. This will occur in the next commit... Astute readers may notice that the command name is transmitted in both the HTTP request URL and the command request frame. This is partially a kludge from me initially implementing the frame-based protocol for SSH first. But it is also a feature: I intend to eventually support issuing multiple commands per HTTP request. This will allow us to replace the abomination that is the "batch" wire protocol command with a protocol-level mechanism for performing multi-dispatch. Because I want the frame-based protocol to be as similar as possible across transports, I'd rather we (redundantly) include the command name in the frame than differ behavior between transports that have out-of-band routing information (like HTTP) readily available. Differential Revision: https://phab.mercurial-scm.org/D2851
author Gregory Szorc <gregory.szorc@gmail.com>
date Mon, 19 Mar 2018 16:49:53 -0700
parents fddcb51b5084
children 2ec1fb9de638
comparison
equal deleted inserted replaced
37050:37d7a1d18b97 37051:40206e227412
76 treediscovery, 76 treediscovery,
77 upgrade, 77 upgrade,
78 url as urlmod, 78 url as urlmod,
79 util, 79 util,
80 vfs as vfsmod, 80 vfs as vfsmod,
81 wireprotoframing,
81 wireprotoserver, 82 wireprotoserver,
82 ) 83 )
83 from .utils import dateutil 84 from .utils import dateutil
84 85
85 release = lockmod.release 86 release = lockmod.release
2709 2710
2710 ``BODYFILE`` 2711 ``BODYFILE``
2711 The content of the file defined as the value to this argument will be 2712 The content of the file defined as the value to this argument will be
2712 transferred verbatim as the HTTP request body. 2713 transferred verbatim as the HTTP request body.
2713 2714
2715 ``frame <type> <flags> <payload>``
2716 Send a unified protocol frame as part of the request body.
2717
2718 All frames will be collected and sent as the body to the HTTP
2719 request.
2720
2714 close 2721 close
2715 ----- 2722 -----
2716 2723
2717 Close the connection to the server. 2724 Close the connection to the server.
2718 2725
2748 2755
2749 eread <X> 2756 eread <X>
2750 --------- 2757 ---------
2751 2758
2752 ``read()`` N bytes from the server's stderr pipe, if available. 2759 ``read()`` N bytes from the server's stderr pipe, if available.
2760
2761 Specifying Unified Frame-Based Protocol Frames
2762 ----------------------------------------------
2763
2764 It is possible to emit a *Unified Frame-Based Protocol* by using special
2765 syntax.
2766
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.
2770
2771 ``payload`` is the simplest: it is evaluated as a Python byte string
2772 literal.
2773
2774 ``type`` can be an integer value for the frame type or the string name
2775 of the type. The strings are defined in ``wireprotoframing.py``. e.g.
2776 ``command-name``.
2777
2778 ``flags`` is a ``|`` delimited list of flag components. Each component
2779 (and there can be just one) can be an integer or a flag name for the
2780 specified frame type. Values are resolved to integers and then bitwise
2781 OR'd together.
2753 """ 2782 """
2754 opts = pycompat.byteskwargs(opts) 2783 opts = pycompat.byteskwargs(opts)
2755 2784
2756 if opts['localssh'] and not repo: 2785 if opts['localssh'] and not repo:
2757 raise error.Abort(_('--localssh requires a repository')) 2786 raise error.Abort(_('--localssh requires a repository'))
2951 '"httprequest <method> <path>')) 2980 '"httprequest <method> <path>'))
2952 2981
2953 method, httppath = request[1:] 2982 method, httppath = request[1:]
2954 headers = {} 2983 headers = {}
2955 body = None 2984 body = None
2985 frames = []
2956 for line in lines: 2986 for line in lines:
2957 line = line.lstrip() 2987 line = line.lstrip()
2958 m = re.match(b'^([a-zA-Z0-9_-]+): (.*)$', line) 2988 m = re.match(b'^([a-zA-Z0-9_-]+): (.*)$', line)
2959 if m: 2989 if m:
2960 headers[m.group(1)] = m.group(2) 2990 headers[m.group(1)] = m.group(2)
2961 continue 2991 continue
2962 2992
2963 if line.startswith(b'BODYFILE '): 2993 if line.startswith(b'BODYFILE '):
2964 with open(line.split(b' ', 1), 'rb') as fh: 2994 with open(line.split(b' ', 1), 'rb') as fh:
2965 body = fh.read() 2995 body = fh.read()
2996 elif line.startswith(b'frame '):
2997 frame = wireprotoframing.makeframefromhumanstring(
2998 line[len(b'frame '):])
2999
3000 frames.append(frame)
2966 else: 3001 else:
2967 raise error.Abort(_('unknown argument to httprequest: %s') % 3002 raise error.Abort(_('unknown argument to httprequest: %s') %
2968 line) 3003 line)
2969 3004
2970 url = path + httppath 3005 url = path + httppath
3006
3007 if frames:
3008 body = b''.join(bytes(f) for f in frames)
3009
2971 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers) 3010 req = urlmod.urlreq.request(pycompat.strurl(url), body, headers)
2972 3011
2973 # urllib.Request insists on using has_data() as a proxy for 3012 # urllib.Request insists on using has_data() as a proxy for
2974 # determining the request method. Override that to use our 3013 # determining the request method. Override that to use our
2975 # explicitly requested method. 3014 # explicitly requested method.