Mercurial > hg
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. |