##// END OF EJS Templates
wireprotov2: move response handling out of httppeer...
Gregory Szorc -
r37737:a656cba0 default
parent child Browse files
Show More
@@ -0,0 +1,135
1 # wireprotov2peer.py - client side code for wire protocol version 2
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 from __future__ import absolute_import
9
10 from .i18n import _
11 from .thirdparty import (
12 cbor,
13 )
14 from . import (
15 error,
16 util,
17 wireprotoframing,
18 )
19
20 class clienthandler(object):
21 """Object to handle higher-level client activities.
22
23 The ``clientreactor`` is used to hold low-level state about the frame-based
24 protocol, such as which requests and streams are active. This type is used
25 for higher-level operations, such as reading frames from a socket, exposing
26 and managing a higher-level primitive for representing command responses,
27 etc. This class is what peers should probably use to bridge wire activity
28 with the higher-level peer API.
29 """
30
31 def __init__(self, ui, clientreactor):
32 self._ui = ui
33 self._reactor = clientreactor
34 self._requests = {}
35 self._futures = {}
36 self._responses = {}
37
38 def callcommand(self, command, args, f):
39 """Register a request to call a command.
40
41 Returns an iterable of frames that should be sent over the wire.
42 """
43 request, action, meta = self._reactor.callcommand(command, args)
44
45 if action != 'noop':
46 raise error.ProgrammingError('%s not yet supported' % action)
47
48 rid = request.requestid
49 self._requests[rid] = request
50 self._futures[rid] = f
51 self._responses[rid] = {
52 'cbor': False,
53 'b': util.bytesio(),
54 }
55
56 return iter(())
57
58 def flushcommands(self):
59 """Flush all queued commands.
60
61 Returns an iterable of frames that should be sent over the wire.
62 """
63 action, meta = self._reactor.flushcommands()
64
65 if action != 'sendframes':
66 raise error.ProgrammingError('%s not yet supported' % action)
67
68 return meta['framegen']
69
70 def readframe(self, fh):
71 """Attempt to read and process a frame.
72
73 Returns None if no frame was read. Presumably this means EOF.
74 """
75 frame = wireprotoframing.readframe(fh)
76 if frame is None:
77 # TODO tell reactor?
78 return
79
80 self._ui.note(_('received %r\n') % frame)
81 self._processframe(frame)
82
83 return True
84
85 def _processframe(self, frame):
86 """Process a single read frame."""
87
88 action, meta = self._reactor.onframerecv(frame)
89
90 if action == 'error':
91 e = error.RepoError(meta['message'])
92
93 if frame.requestid in self._futures:
94 self._futures[frame.requestid].set_exception(e)
95 else:
96 raise e
97
98 if frame.requestid not in self._requests:
99 raise error.ProgrammingError(
100 'received frame for unknown request; this is either a bug in '
101 'the clientreactor not screening for this or this instance was '
102 'never told about this request: %r' % frame)
103
104 response = self._responses[frame.requestid]
105
106 if action == 'responsedata':
107 response['b'].write(meta['data'])
108
109 if meta['cbor']:
110 response['cbor'] = True
111
112 if meta['eos']:
113 if meta['cbor']:
114 # If CBOR, decode every object.
115 b = response['b']
116
117 size = b.tell()
118 b.seek(0)
119
120 decoder = cbor.CBORDecoder(b)
121
122 result = []
123 while b.tell() < size:
124 result.append(decoder.decode())
125 else:
126 result = [response['b'].getvalue()]
127
128 self._futures[frame.requestid].set_result(result)
129
130 del self._requests[frame.requestid]
131 del self._futures[frame.requestid]
132
133 else:
134 raise error.ProgrammingError(
135 'unhandled action from clientreactor: %s' % action)
@@ -1,1010 +1,950
1 # httppeer.py - HTTP repository proxy classes for mercurial
1 # httppeer.py - HTTP repository proxy classes for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import io
12 import io
13 import os
13 import os
14 import socket
14 import socket
15 import struct
15 import struct
16 import sys
17 import tempfile
16 import tempfile
18 import weakref
17 import weakref
19
18
20 from .i18n import _
19 from .i18n import _
21 from .thirdparty import (
20 from .thirdparty import (
22 cbor,
21 cbor,
23 )
22 )
24 from .thirdparty.zope import (
23 from .thirdparty.zope import (
25 interface as zi,
24 interface as zi,
26 )
25 )
27 from . import (
26 from . import (
28 bundle2,
27 bundle2,
29 error,
28 error,
30 httpconnection,
29 httpconnection,
31 pycompat,
30 pycompat,
32 repository,
31 repository,
33 statichttprepo,
32 statichttprepo,
34 url as urlmod,
33 url as urlmod,
35 util,
34 util,
36 wireprotoframing,
35 wireprotoframing,
37 wireprototypes,
36 wireprototypes,
38 wireprotov1peer,
37 wireprotov1peer,
38 wireprotov2peer,
39 wireprotov2server,
39 wireprotov2server,
40 )
40 )
41
41
42 httplib = util.httplib
42 httplib = util.httplib
43 urlerr = util.urlerr
43 urlerr = util.urlerr
44 urlreq = util.urlreq
44 urlreq = util.urlreq
45
45
46 def encodevalueinheaders(value, header, limit):
46 def encodevalueinheaders(value, header, limit):
47 """Encode a string value into multiple HTTP headers.
47 """Encode a string value into multiple HTTP headers.
48
48
49 ``value`` will be encoded into 1 or more HTTP headers with the names
49 ``value`` will be encoded into 1 or more HTTP headers with the names
50 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
50 ``header-<N>`` where ``<N>`` is an integer starting at 1. Each header
51 name + value will be at most ``limit`` bytes long.
51 name + value will be at most ``limit`` bytes long.
52
52
53 Returns an iterable of 2-tuples consisting of header names and
53 Returns an iterable of 2-tuples consisting of header names and
54 values as native strings.
54 values as native strings.
55 """
55 """
56 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
56 # HTTP Headers are ASCII. Python 3 requires them to be unicodes,
57 # not bytes. This function always takes bytes in as arguments.
57 # not bytes. This function always takes bytes in as arguments.
58 fmt = pycompat.strurl(header) + r'-%s'
58 fmt = pycompat.strurl(header) + r'-%s'
59 # Note: it is *NOT* a bug that the last bit here is a bytestring
59 # Note: it is *NOT* a bug that the last bit here is a bytestring
60 # and not a unicode: we're just getting the encoded length anyway,
60 # and not a unicode: we're just getting the encoded length anyway,
61 # and using an r-string to make it portable between Python 2 and 3
61 # and using an r-string to make it portable between Python 2 and 3
62 # doesn't work because then the \r is a literal backslash-r
62 # doesn't work because then the \r is a literal backslash-r
63 # instead of a carriage return.
63 # instead of a carriage return.
64 valuelen = limit - len(fmt % r'000') - len(': \r\n')
64 valuelen = limit - len(fmt % r'000') - len(': \r\n')
65 result = []
65 result = []
66
66
67 n = 0
67 n = 0
68 for i in xrange(0, len(value), valuelen):
68 for i in xrange(0, len(value), valuelen):
69 n += 1
69 n += 1
70 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
70 result.append((fmt % str(n), pycompat.strurl(value[i:i + valuelen])))
71
71
72 return result
72 return result
73
73
74 def _wraphttpresponse(resp):
74 def _wraphttpresponse(resp):
75 """Wrap an HTTPResponse with common error handlers.
75 """Wrap an HTTPResponse with common error handlers.
76
76
77 This ensures that any I/O from any consumer raises the appropriate
77 This ensures that any I/O from any consumer raises the appropriate
78 error and messaging.
78 error and messaging.
79 """
79 """
80 origread = resp.read
80 origread = resp.read
81
81
82 class readerproxy(resp.__class__):
82 class readerproxy(resp.__class__):
83 def read(self, size=None):
83 def read(self, size=None):
84 try:
84 try:
85 return origread(size)
85 return origread(size)
86 except httplib.IncompleteRead as e:
86 except httplib.IncompleteRead as e:
87 # e.expected is an integer if length known or None otherwise.
87 # e.expected is an integer if length known or None otherwise.
88 if e.expected:
88 if e.expected:
89 msg = _('HTTP request error (incomplete response; '
89 msg = _('HTTP request error (incomplete response; '
90 'expected %d bytes got %d)') % (e.expected,
90 'expected %d bytes got %d)') % (e.expected,
91 len(e.partial))
91 len(e.partial))
92 else:
92 else:
93 msg = _('HTTP request error (incomplete response)')
93 msg = _('HTTP request error (incomplete response)')
94
94
95 raise error.PeerTransportError(
95 raise error.PeerTransportError(
96 msg,
96 msg,
97 hint=_('this may be an intermittent network failure; '
97 hint=_('this may be an intermittent network failure; '
98 'if the error persists, consider contacting the '
98 'if the error persists, consider contacting the '
99 'network or server operator'))
99 'network or server operator'))
100 except httplib.HTTPException as e:
100 except httplib.HTTPException as e:
101 raise error.PeerTransportError(
101 raise error.PeerTransportError(
102 _('HTTP request error (%s)') % e,
102 _('HTTP request error (%s)') % e,
103 hint=_('this may be an intermittent network failure; '
103 hint=_('this may be an intermittent network failure; '
104 'if the error persists, consider contacting the '
104 'if the error persists, consider contacting the '
105 'network or server operator'))
105 'network or server operator'))
106
106
107 resp.__class__ = readerproxy
107 resp.__class__ = readerproxy
108
108
109 class _multifile(object):
109 class _multifile(object):
110 def __init__(self, *fileobjs):
110 def __init__(self, *fileobjs):
111 for f in fileobjs:
111 for f in fileobjs:
112 if not util.safehasattr(f, 'length'):
112 if not util.safehasattr(f, 'length'):
113 raise ValueError(
113 raise ValueError(
114 '_multifile only supports file objects that '
114 '_multifile only supports file objects that '
115 'have a length but this one does not:', type(f), f)
115 'have a length but this one does not:', type(f), f)
116 self._fileobjs = fileobjs
116 self._fileobjs = fileobjs
117 self._index = 0
117 self._index = 0
118
118
119 @property
119 @property
120 def length(self):
120 def length(self):
121 return sum(f.length for f in self._fileobjs)
121 return sum(f.length for f in self._fileobjs)
122
122
123 def read(self, amt=None):
123 def read(self, amt=None):
124 if amt <= 0:
124 if amt <= 0:
125 return ''.join(f.read() for f in self._fileobjs)
125 return ''.join(f.read() for f in self._fileobjs)
126 parts = []
126 parts = []
127 while amt and self._index < len(self._fileobjs):
127 while amt and self._index < len(self._fileobjs):
128 parts.append(self._fileobjs[self._index].read(amt))
128 parts.append(self._fileobjs[self._index].read(amt))
129 got = len(parts[-1])
129 got = len(parts[-1])
130 if got < amt:
130 if got < amt:
131 self._index += 1
131 self._index += 1
132 amt -= got
132 amt -= got
133 return ''.join(parts)
133 return ''.join(parts)
134
134
135 def seek(self, offset, whence=os.SEEK_SET):
135 def seek(self, offset, whence=os.SEEK_SET):
136 if whence != os.SEEK_SET:
136 if whence != os.SEEK_SET:
137 raise NotImplementedError(
137 raise NotImplementedError(
138 '_multifile does not support anything other'
138 '_multifile does not support anything other'
139 ' than os.SEEK_SET for whence on seek()')
139 ' than os.SEEK_SET for whence on seek()')
140 if offset != 0:
140 if offset != 0:
141 raise NotImplementedError(
141 raise NotImplementedError(
142 '_multifile only supports seeking to start, but that '
142 '_multifile only supports seeking to start, but that '
143 'could be fixed if you need it')
143 'could be fixed if you need it')
144 for f in self._fileobjs:
144 for f in self._fileobjs:
145 f.seek(0)
145 f.seek(0)
146 self._index = 0
146 self._index = 0
147
147
148 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
148 def makev1commandrequest(ui, requestbuilder, caps, capablefn,
149 repobaseurl, cmd, args):
149 repobaseurl, cmd, args):
150 """Make an HTTP request to run a command for a version 1 client.
150 """Make an HTTP request to run a command for a version 1 client.
151
151
152 ``caps`` is a set of known server capabilities. The value may be
152 ``caps`` is a set of known server capabilities. The value may be
153 None if capabilities are not yet known.
153 None if capabilities are not yet known.
154
154
155 ``capablefn`` is a function to evaluate a capability.
155 ``capablefn`` is a function to evaluate a capability.
156
156
157 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
157 ``cmd``, ``args``, and ``data`` define the command, its arguments, and
158 raw data to pass to it.
158 raw data to pass to it.
159 """
159 """
160 if cmd == 'pushkey':
160 if cmd == 'pushkey':
161 args['data'] = ''
161 args['data'] = ''
162 data = args.pop('data', None)
162 data = args.pop('data', None)
163 headers = args.pop('headers', {})
163 headers = args.pop('headers', {})
164
164
165 ui.debug("sending %s command\n" % cmd)
165 ui.debug("sending %s command\n" % cmd)
166 q = [('cmd', cmd)]
166 q = [('cmd', cmd)]
167 headersize = 0
167 headersize = 0
168 # Important: don't use self.capable() here or else you end up
168 # Important: don't use self.capable() here or else you end up
169 # with infinite recursion when trying to look up capabilities
169 # with infinite recursion when trying to look up capabilities
170 # for the first time.
170 # for the first time.
171 postargsok = caps is not None and 'httppostargs' in caps
171 postargsok = caps is not None and 'httppostargs' in caps
172
172
173 # Send arguments via POST.
173 # Send arguments via POST.
174 if postargsok and args:
174 if postargsok and args:
175 strargs = urlreq.urlencode(sorted(args.items()))
175 strargs = urlreq.urlencode(sorted(args.items()))
176 if not data:
176 if not data:
177 data = strargs
177 data = strargs
178 else:
178 else:
179 if isinstance(data, bytes):
179 if isinstance(data, bytes):
180 i = io.BytesIO(data)
180 i = io.BytesIO(data)
181 i.length = len(data)
181 i.length = len(data)
182 data = i
182 data = i
183 argsio = io.BytesIO(strargs)
183 argsio = io.BytesIO(strargs)
184 argsio.length = len(strargs)
184 argsio.length = len(strargs)
185 data = _multifile(argsio, data)
185 data = _multifile(argsio, data)
186 headers[r'X-HgArgs-Post'] = len(strargs)
186 headers[r'X-HgArgs-Post'] = len(strargs)
187 elif args:
187 elif args:
188 # Calling self.capable() can infinite loop if we are calling
188 # Calling self.capable() can infinite loop if we are calling
189 # "capabilities". But that command should never accept wire
189 # "capabilities". But that command should never accept wire
190 # protocol arguments. So this should never happen.
190 # protocol arguments. So this should never happen.
191 assert cmd != 'capabilities'
191 assert cmd != 'capabilities'
192 httpheader = capablefn('httpheader')
192 httpheader = capablefn('httpheader')
193 if httpheader:
193 if httpheader:
194 headersize = int(httpheader.split(',', 1)[0])
194 headersize = int(httpheader.split(',', 1)[0])
195
195
196 # Send arguments via HTTP headers.
196 # Send arguments via HTTP headers.
197 if headersize > 0:
197 if headersize > 0:
198 # The headers can typically carry more data than the URL.
198 # The headers can typically carry more data than the URL.
199 encargs = urlreq.urlencode(sorted(args.items()))
199 encargs = urlreq.urlencode(sorted(args.items()))
200 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
200 for header, value in encodevalueinheaders(encargs, 'X-HgArg',
201 headersize):
201 headersize):
202 headers[header] = value
202 headers[header] = value
203 # Send arguments via query string (Mercurial <1.9).
203 # Send arguments via query string (Mercurial <1.9).
204 else:
204 else:
205 q += sorted(args.items())
205 q += sorted(args.items())
206
206
207 qs = '?%s' % urlreq.urlencode(q)
207 qs = '?%s' % urlreq.urlencode(q)
208 cu = "%s%s" % (repobaseurl, qs)
208 cu = "%s%s" % (repobaseurl, qs)
209 size = 0
209 size = 0
210 if util.safehasattr(data, 'length'):
210 if util.safehasattr(data, 'length'):
211 size = data.length
211 size = data.length
212 elif data is not None:
212 elif data is not None:
213 size = len(data)
213 size = len(data)
214 if data is not None and r'Content-Type' not in headers:
214 if data is not None and r'Content-Type' not in headers:
215 headers[r'Content-Type'] = r'application/mercurial-0.1'
215 headers[r'Content-Type'] = r'application/mercurial-0.1'
216
216
217 # Tell the server we accept application/mercurial-0.2 and multiple
217 # Tell the server we accept application/mercurial-0.2 and multiple
218 # compression formats if the server is capable of emitting those
218 # compression formats if the server is capable of emitting those
219 # payloads.
219 # payloads.
220 # Note: Keep this set empty by default, as client advertisement of
220 # Note: Keep this set empty by default, as client advertisement of
221 # protocol parameters should only occur after the handshake.
221 # protocol parameters should only occur after the handshake.
222 protoparams = set()
222 protoparams = set()
223
223
224 mediatypes = set()
224 mediatypes = set()
225 if caps is not None:
225 if caps is not None:
226 mt = capablefn('httpmediatype')
226 mt = capablefn('httpmediatype')
227 if mt:
227 if mt:
228 protoparams.add('0.1')
228 protoparams.add('0.1')
229 mediatypes = set(mt.split(','))
229 mediatypes = set(mt.split(','))
230
230
231 protoparams.add('partial-pull')
231 protoparams.add('partial-pull')
232
232
233 if '0.2tx' in mediatypes:
233 if '0.2tx' in mediatypes:
234 protoparams.add('0.2')
234 protoparams.add('0.2')
235
235
236 if '0.2tx' in mediatypes and capablefn('compression'):
236 if '0.2tx' in mediatypes and capablefn('compression'):
237 # We /could/ compare supported compression formats and prune
237 # We /could/ compare supported compression formats and prune
238 # non-mutually supported or error if nothing is mutually supported.
238 # non-mutually supported or error if nothing is mutually supported.
239 # For now, send the full list to the server and have it error.
239 # For now, send the full list to the server and have it error.
240 comps = [e.wireprotosupport().name for e in
240 comps = [e.wireprotosupport().name for e in
241 util.compengines.supportedwireengines(util.CLIENTROLE)]
241 util.compengines.supportedwireengines(util.CLIENTROLE)]
242 protoparams.add('comp=%s' % ','.join(comps))
242 protoparams.add('comp=%s' % ','.join(comps))
243
243
244 if protoparams:
244 if protoparams:
245 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
245 protoheaders = encodevalueinheaders(' '.join(sorted(protoparams)),
246 'X-HgProto',
246 'X-HgProto',
247 headersize or 1024)
247 headersize or 1024)
248 for header, value in protoheaders:
248 for header, value in protoheaders:
249 headers[header] = value
249 headers[header] = value
250
250
251 varyheaders = []
251 varyheaders = []
252 for header in headers:
252 for header in headers:
253 if header.lower().startswith(r'x-hg'):
253 if header.lower().startswith(r'x-hg'):
254 varyheaders.append(header)
254 varyheaders.append(header)
255
255
256 if varyheaders:
256 if varyheaders:
257 headers[r'Vary'] = r','.join(sorted(varyheaders))
257 headers[r'Vary'] = r','.join(sorted(varyheaders))
258
258
259 req = requestbuilder(pycompat.strurl(cu), data, headers)
259 req = requestbuilder(pycompat.strurl(cu), data, headers)
260
260
261 if data is not None:
261 if data is not None:
262 ui.debug("sending %d bytes\n" % size)
262 ui.debug("sending %d bytes\n" % size)
263 req.add_unredirected_header(r'Content-Length', r'%d' % size)
263 req.add_unredirected_header(r'Content-Length', r'%d' % size)
264
264
265 return req, cu, qs
265 return req, cu, qs
266
266
267 def sendrequest(ui, opener, req):
267 def sendrequest(ui, opener, req):
268 """Send a prepared HTTP request.
268 """Send a prepared HTTP request.
269
269
270 Returns the response object.
270 Returns the response object.
271 """
271 """
272 if (ui.debugflag
272 if (ui.debugflag
273 and ui.configbool('devel', 'debug.peer-request')):
273 and ui.configbool('devel', 'debug.peer-request')):
274 dbg = ui.debug
274 dbg = ui.debug
275 line = 'devel-peer-request: %s\n'
275 line = 'devel-peer-request: %s\n'
276 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
276 dbg(line % '%s %s' % (req.get_method(), req.get_full_url()))
277 hgargssize = None
277 hgargssize = None
278
278
279 for header, value in sorted(req.header_items()):
279 for header, value in sorted(req.header_items()):
280 if header.startswith('X-hgarg-'):
280 if header.startswith('X-hgarg-'):
281 if hgargssize is None:
281 if hgargssize is None:
282 hgargssize = 0
282 hgargssize = 0
283 hgargssize += len(value)
283 hgargssize += len(value)
284 else:
284 else:
285 dbg(line % ' %s %s' % (header, value))
285 dbg(line % ' %s %s' % (header, value))
286
286
287 if hgargssize is not None:
287 if hgargssize is not None:
288 dbg(line % ' %d bytes of commands arguments in headers'
288 dbg(line % ' %d bytes of commands arguments in headers'
289 % hgargssize)
289 % hgargssize)
290
290
291 if req.has_data():
291 if req.has_data():
292 data = req.get_data()
292 data = req.get_data()
293 length = getattr(data, 'length', None)
293 length = getattr(data, 'length', None)
294 if length is None:
294 if length is None:
295 length = len(data)
295 length = len(data)
296 dbg(line % ' %d bytes of data' % length)
296 dbg(line % ' %d bytes of data' % length)
297
297
298 start = util.timer()
298 start = util.timer()
299
299
300 try:
300 try:
301 res = opener.open(req)
301 res = opener.open(req)
302 except urlerr.httperror as inst:
302 except urlerr.httperror as inst:
303 if inst.code == 401:
303 if inst.code == 401:
304 raise error.Abort(_('authorization failed'))
304 raise error.Abort(_('authorization failed'))
305 raise
305 raise
306 except httplib.HTTPException as inst:
306 except httplib.HTTPException as inst:
307 ui.debug('http error requesting %s\n' %
307 ui.debug('http error requesting %s\n' %
308 util.hidepassword(req.get_full_url()))
308 util.hidepassword(req.get_full_url()))
309 ui.traceback()
309 ui.traceback()
310 raise IOError(None, inst)
310 raise IOError(None, inst)
311 finally:
311 finally:
312 if ui.configbool('devel', 'debug.peer-request'):
312 if ui.configbool('devel', 'debug.peer-request'):
313 dbg(line % ' finished in %.4f seconds (%s)'
313 dbg(line % ' finished in %.4f seconds (%s)'
314 % (util.timer() - start, res.code))
314 % (util.timer() - start, res.code))
315
315
316 # Insert error handlers for common I/O failures.
316 # Insert error handlers for common I/O failures.
317 _wraphttpresponse(res)
317 _wraphttpresponse(res)
318
318
319 return res
319 return res
320
320
321 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
321 def parsev1commandresponse(ui, baseurl, requrl, qs, resp, compressible,
322 allowcbor=False):
322 allowcbor=False):
323 # record the url we got redirected to
323 # record the url we got redirected to
324 respurl = pycompat.bytesurl(resp.geturl())
324 respurl = pycompat.bytesurl(resp.geturl())
325 if respurl.endswith(qs):
325 if respurl.endswith(qs):
326 respurl = respurl[:-len(qs)]
326 respurl = respurl[:-len(qs)]
327 if baseurl.rstrip('/') != respurl.rstrip('/'):
327 if baseurl.rstrip('/') != respurl.rstrip('/'):
328 if not ui.quiet:
328 if not ui.quiet:
329 ui.warn(_('real URL is %s\n') % respurl)
329 ui.warn(_('real URL is %s\n') % respurl)
330
330
331 try:
331 try:
332 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
332 proto = pycompat.bytesurl(resp.getheader(r'content-type', r''))
333 except AttributeError:
333 except AttributeError:
334 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
334 proto = pycompat.bytesurl(resp.headers.get(r'content-type', r''))
335
335
336 safeurl = util.hidepassword(baseurl)
336 safeurl = util.hidepassword(baseurl)
337 if proto.startswith('application/hg-error'):
337 if proto.startswith('application/hg-error'):
338 raise error.OutOfBandError(resp.read())
338 raise error.OutOfBandError(resp.read())
339
339
340 # Pre 1.0 versions of Mercurial used text/plain and
340 # Pre 1.0 versions of Mercurial used text/plain and
341 # application/hg-changegroup. We don't support such old servers.
341 # application/hg-changegroup. We don't support such old servers.
342 if not proto.startswith('application/mercurial-'):
342 if not proto.startswith('application/mercurial-'):
343 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
343 ui.debug("requested URL: '%s'\n" % util.hidepassword(requrl))
344 raise error.RepoError(
344 raise error.RepoError(
345 _("'%s' does not appear to be an hg repository:\n"
345 _("'%s' does not appear to be an hg repository:\n"
346 "---%%<--- (%s)\n%s\n---%%<---\n")
346 "---%%<--- (%s)\n%s\n---%%<---\n")
347 % (safeurl, proto or 'no content-type', resp.read(1024)))
347 % (safeurl, proto or 'no content-type', resp.read(1024)))
348
348
349 try:
349 try:
350 subtype = proto.split('-', 1)[1]
350 subtype = proto.split('-', 1)[1]
351
351
352 # Unless we end up supporting CBOR in the legacy wire protocol,
352 # Unless we end up supporting CBOR in the legacy wire protocol,
353 # this should ONLY be encountered for the initial capabilities
353 # this should ONLY be encountered for the initial capabilities
354 # request during handshake.
354 # request during handshake.
355 if subtype == 'cbor':
355 if subtype == 'cbor':
356 if allowcbor:
356 if allowcbor:
357 return respurl, proto, resp
357 return respurl, proto, resp
358 else:
358 else:
359 raise error.RepoError(_('unexpected CBOR response from '
359 raise error.RepoError(_('unexpected CBOR response from '
360 'server'))
360 'server'))
361
361
362 version_info = tuple([int(n) for n in subtype.split('.')])
362 version_info = tuple([int(n) for n in subtype.split('.')])
363 except ValueError:
363 except ValueError:
364 raise error.RepoError(_("'%s' sent a broken Content-Type "
364 raise error.RepoError(_("'%s' sent a broken Content-Type "
365 "header (%s)") % (safeurl, proto))
365 "header (%s)") % (safeurl, proto))
366
366
367 # TODO consider switching to a decompression reader that uses
367 # TODO consider switching to a decompression reader that uses
368 # generators.
368 # generators.
369 if version_info == (0, 1):
369 if version_info == (0, 1):
370 if compressible:
370 if compressible:
371 resp = util.compengines['zlib'].decompressorreader(resp)
371 resp = util.compengines['zlib'].decompressorreader(resp)
372
372
373 elif version_info == (0, 2):
373 elif version_info == (0, 2):
374 # application/mercurial-0.2 always identifies the compression
374 # application/mercurial-0.2 always identifies the compression
375 # engine in the payload header.
375 # engine in the payload header.
376 elen = struct.unpack('B', resp.read(1))[0]
376 elen = struct.unpack('B', resp.read(1))[0]
377 ename = resp.read(elen)
377 ename = resp.read(elen)
378 engine = util.compengines.forwiretype(ename)
378 engine = util.compengines.forwiretype(ename)
379
379
380 resp = engine.decompressorreader(resp)
380 resp = engine.decompressorreader(resp)
381 else:
381 else:
382 raise error.RepoError(_("'%s' uses newer protocol %s") %
382 raise error.RepoError(_("'%s' uses newer protocol %s") %
383 (safeurl, subtype))
383 (safeurl, subtype))
384
384
385 return respurl, proto, resp
385 return respurl, proto, resp
386
386
387 class httppeer(wireprotov1peer.wirepeer):
387 class httppeer(wireprotov1peer.wirepeer):
388 def __init__(self, ui, path, url, opener, requestbuilder, caps):
388 def __init__(self, ui, path, url, opener, requestbuilder, caps):
389 self.ui = ui
389 self.ui = ui
390 self._path = path
390 self._path = path
391 self._url = url
391 self._url = url
392 self._caps = caps
392 self._caps = caps
393 self._urlopener = opener
393 self._urlopener = opener
394 self._requestbuilder = requestbuilder
394 self._requestbuilder = requestbuilder
395
395
396 def __del__(self):
396 def __del__(self):
397 for h in self._urlopener.handlers:
397 for h in self._urlopener.handlers:
398 h.close()
398 h.close()
399 getattr(h, "close_all", lambda: None)()
399 getattr(h, "close_all", lambda: None)()
400
400
401 # Begin of ipeerconnection interface.
401 # Begin of ipeerconnection interface.
402
402
403 def url(self):
403 def url(self):
404 return self._path
404 return self._path
405
405
406 def local(self):
406 def local(self):
407 return None
407 return None
408
408
409 def peer(self):
409 def peer(self):
410 return self
410 return self
411
411
412 def canpush(self):
412 def canpush(self):
413 return True
413 return True
414
414
415 def close(self):
415 def close(self):
416 pass
416 pass
417
417
418 # End of ipeerconnection interface.
418 # End of ipeerconnection interface.
419
419
420 # Begin of ipeercommands interface.
420 # Begin of ipeercommands interface.
421
421
422 def capabilities(self):
422 def capabilities(self):
423 return self._caps
423 return self._caps
424
424
425 # End of ipeercommands interface.
425 # End of ipeercommands interface.
426
426
427 # look up capabilities only when needed
427 # look up capabilities only when needed
428
428
429 def _callstream(self, cmd, _compressible=False, **args):
429 def _callstream(self, cmd, _compressible=False, **args):
430 args = pycompat.byteskwargs(args)
430 args = pycompat.byteskwargs(args)
431
431
432 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
432 req, cu, qs = makev1commandrequest(self.ui, self._requestbuilder,
433 self._caps, self.capable,
433 self._caps, self.capable,
434 self._url, cmd, args)
434 self._url, cmd, args)
435
435
436 resp = sendrequest(self.ui, self._urlopener, req)
436 resp = sendrequest(self.ui, self._urlopener, req)
437
437
438 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
438 self._url, ct, resp = parsev1commandresponse(self.ui, self._url, cu, qs,
439 resp, _compressible)
439 resp, _compressible)
440
440
441 return resp
441 return resp
442
442
443 def _call(self, cmd, **args):
443 def _call(self, cmd, **args):
444 fp = self._callstream(cmd, **args)
444 fp = self._callstream(cmd, **args)
445 try:
445 try:
446 return fp.read()
446 return fp.read()
447 finally:
447 finally:
448 # if using keepalive, allow connection to be reused
448 # if using keepalive, allow connection to be reused
449 fp.close()
449 fp.close()
450
450
451 def _callpush(self, cmd, cg, **args):
451 def _callpush(self, cmd, cg, **args):
452 # have to stream bundle to a temp file because we do not have
452 # have to stream bundle to a temp file because we do not have
453 # http 1.1 chunked transfer.
453 # http 1.1 chunked transfer.
454
454
455 types = self.capable('unbundle')
455 types = self.capable('unbundle')
456 try:
456 try:
457 types = types.split(',')
457 types = types.split(',')
458 except AttributeError:
458 except AttributeError:
459 # servers older than d1b16a746db6 will send 'unbundle' as a
459 # servers older than d1b16a746db6 will send 'unbundle' as a
460 # boolean capability. They only support headerless/uncompressed
460 # boolean capability. They only support headerless/uncompressed
461 # bundles.
461 # bundles.
462 types = [""]
462 types = [""]
463 for x in types:
463 for x in types:
464 if x in bundle2.bundletypes:
464 if x in bundle2.bundletypes:
465 type = x
465 type = x
466 break
466 break
467
467
468 tempname = bundle2.writebundle(self.ui, cg, None, type)
468 tempname = bundle2.writebundle(self.ui, cg, None, type)
469 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
469 fp = httpconnection.httpsendfile(self.ui, tempname, "rb")
470 headers = {r'Content-Type': r'application/mercurial-0.1'}
470 headers = {r'Content-Type': r'application/mercurial-0.1'}
471
471
472 try:
472 try:
473 r = self._call(cmd, data=fp, headers=headers, **args)
473 r = self._call(cmd, data=fp, headers=headers, **args)
474 vals = r.split('\n', 1)
474 vals = r.split('\n', 1)
475 if len(vals) < 2:
475 if len(vals) < 2:
476 raise error.ResponseError(_("unexpected response:"), r)
476 raise error.ResponseError(_("unexpected response:"), r)
477 return vals
477 return vals
478 except urlerr.httperror:
478 except urlerr.httperror:
479 # Catch and re-raise these so we don't try and treat them
479 # Catch and re-raise these so we don't try and treat them
480 # like generic socket errors. They lack any values in
480 # like generic socket errors. They lack any values in
481 # .args on Python 3 which breaks our socket.error block.
481 # .args on Python 3 which breaks our socket.error block.
482 raise
482 raise
483 except socket.error as err:
483 except socket.error as err:
484 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
484 if err.args[0] in (errno.ECONNRESET, errno.EPIPE):
485 raise error.Abort(_('push failed: %s') % err.args[1])
485 raise error.Abort(_('push failed: %s') % err.args[1])
486 raise error.Abort(err.args[1])
486 raise error.Abort(err.args[1])
487 finally:
487 finally:
488 fp.close()
488 fp.close()
489 os.unlink(tempname)
489 os.unlink(tempname)
490
490
491 def _calltwowaystream(self, cmd, fp, **args):
491 def _calltwowaystream(self, cmd, fp, **args):
492 fh = None
492 fh = None
493 fp_ = None
493 fp_ = None
494 filename = None
494 filename = None
495 try:
495 try:
496 # dump bundle to disk
496 # dump bundle to disk
497 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
497 fd, filename = tempfile.mkstemp(prefix="hg-bundle-", suffix=".hg")
498 fh = os.fdopen(fd, r"wb")
498 fh = os.fdopen(fd, r"wb")
499 d = fp.read(4096)
499 d = fp.read(4096)
500 while d:
500 while d:
501 fh.write(d)
501 fh.write(d)
502 d = fp.read(4096)
502 d = fp.read(4096)
503 fh.close()
503 fh.close()
504 # start http push
504 # start http push
505 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
505 fp_ = httpconnection.httpsendfile(self.ui, filename, "rb")
506 headers = {r'Content-Type': r'application/mercurial-0.1'}
506 headers = {r'Content-Type': r'application/mercurial-0.1'}
507 return self._callstream(cmd, data=fp_, headers=headers, **args)
507 return self._callstream(cmd, data=fp_, headers=headers, **args)
508 finally:
508 finally:
509 if fp_ is not None:
509 if fp_ is not None:
510 fp_.close()
510 fp_.close()
511 if fh is not None:
511 if fh is not None:
512 fh.close()
512 fh.close()
513 os.unlink(filename)
513 os.unlink(filename)
514
514
515 def _callcompressable(self, cmd, **args):
515 def _callcompressable(self, cmd, **args):
516 return self._callstream(cmd, _compressible=True, **args)
516 return self._callstream(cmd, _compressible=True, **args)
517
517
518 def _abort(self, exception):
518 def _abort(self, exception):
519 raise exception
519 raise exception
520
520
521 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
521 def sendv2request(ui, opener, requestbuilder, apiurl, permission, requests):
522 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
522 reactor = wireprotoframing.clientreactor(hasmultiplesend=False,
523 buffersends=True)
523 buffersends=True)
524
524
525 handler = wireprotov2peer.clienthandler(ui, reactor)
526
525 url = '%s/%s' % (apiurl, permission)
527 url = '%s/%s' % (apiurl, permission)
526
528
527 if len(requests) > 1:
529 if len(requests) > 1:
528 url += '/multirequest'
530 url += '/multirequest'
529 else:
531 else:
530 url += '/%s' % requests[0][0]
532 url += '/%s' % requests[0][0]
531
533
532 # Request ID to (request, future)
533 requestmap = {}
534
535 for command, args, f in requests:
534 for command, args, f in requests:
536 request, action, meta = reactor.callcommand(command, args)
535 assert not list(handler.callcommand(command, args, f))
537 assert action == 'noop'
538
539 requestmap[request.requestid] = (request, f)
540
541 action, meta = reactor.flushcommands()
542 assert action == 'sendframes'
543
536
544 # TODO stream this.
537 # TODO stream this.
545 body = b''.join(map(bytes, meta['framegen']))
538 body = b''.join(map(bytes, handler.flushcommands()))
546
539
547 # TODO modify user-agent to reflect v2
540 # TODO modify user-agent to reflect v2
548 headers = {
541 headers = {
549 r'Accept': wireprotov2server.FRAMINGTYPE,
542 r'Accept': wireprotov2server.FRAMINGTYPE,
550 r'Content-Type': wireprotov2server.FRAMINGTYPE,
543 r'Content-Type': wireprotov2server.FRAMINGTYPE,
551 }
544 }
552
545
553 req = requestbuilder(pycompat.strurl(url), body, headers)
546 req = requestbuilder(pycompat.strurl(url), body, headers)
554 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
547 req.add_unredirected_header(r'Content-Length', r'%d' % len(body))
555
548
556 try:
549 try:
557 res = opener.open(req)
550 res = opener.open(req)
558 except urlerr.httperror as e:
551 except urlerr.httperror as e:
559 if e.code == 401:
552 if e.code == 401:
560 raise error.Abort(_('authorization failed'))
553 raise error.Abort(_('authorization failed'))
561
554
562 raise
555 raise
563 except httplib.HTTPException as e:
556 except httplib.HTTPException as e:
564 ui.traceback()
557 ui.traceback()
565 raise IOError(None, e)
558 raise IOError(None, e)
566
559
567 return reactor, requestmap, res
560 return handler, res
568
561
569 class queuedcommandfuture(pycompat.futures.Future):
562 class queuedcommandfuture(pycompat.futures.Future):
570 """Wraps result() on command futures to trigger submission on call."""
563 """Wraps result() on command futures to trigger submission on call."""
571
564
572 def result(self, timeout=None):
565 def result(self, timeout=None):
573 if self.done():
566 if self.done():
574 return pycompat.futures.Future.result(self, timeout)
567 return pycompat.futures.Future.result(self, timeout)
575
568
576 self._peerexecutor.sendcommands()
569 self._peerexecutor.sendcommands()
577
570
578 # sendcommands() will restore the original __class__ and self.result
571 # sendcommands() will restore the original __class__ and self.result
579 # will resolve to Future.result.
572 # will resolve to Future.result.
580 return self.result(timeout)
573 return self.result(timeout)
581
574
582 @zi.implementer(repository.ipeercommandexecutor)
575 @zi.implementer(repository.ipeercommandexecutor)
583 class httpv2executor(object):
576 class httpv2executor(object):
584 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
577 def __init__(self, ui, opener, requestbuilder, apiurl, descriptor):
585 self._ui = ui
578 self._ui = ui
586 self._opener = opener
579 self._opener = opener
587 self._requestbuilder = requestbuilder
580 self._requestbuilder = requestbuilder
588 self._apiurl = apiurl
581 self._apiurl = apiurl
589 self._descriptor = descriptor
582 self._descriptor = descriptor
590 self._sent = False
583 self._sent = False
591 self._closed = False
584 self._closed = False
592 self._neededpermissions = set()
585 self._neededpermissions = set()
593 self._calls = []
586 self._calls = []
594 self._futures = weakref.WeakSet()
587 self._futures = weakref.WeakSet()
595 self._responseexecutor = None
588 self._responseexecutor = None
596 self._responsef = None
589 self._responsef = None
597
590
598 def __enter__(self):
591 def __enter__(self):
599 return self
592 return self
600
593
601 def __exit__(self, exctype, excvalue, exctb):
594 def __exit__(self, exctype, excvalue, exctb):
602 self.close()
595 self.close()
603
596
604 def callcommand(self, command, args):
597 def callcommand(self, command, args):
605 if self._sent:
598 if self._sent:
606 raise error.ProgrammingError('callcommand() cannot be used after '
599 raise error.ProgrammingError('callcommand() cannot be used after '
607 'commands are sent')
600 'commands are sent')
608
601
609 if self._closed:
602 if self._closed:
610 raise error.ProgrammingError('callcommand() cannot be used after '
603 raise error.ProgrammingError('callcommand() cannot be used after '
611 'close()')
604 'close()')
612
605
613 # The service advertises which commands are available. So if we attempt
606 # The service advertises which commands are available. So if we attempt
614 # to call an unknown command or pass an unknown argument, we can screen
607 # to call an unknown command or pass an unknown argument, we can screen
615 # for this.
608 # for this.
616 if command not in self._descriptor['commands']:
609 if command not in self._descriptor['commands']:
617 raise error.ProgrammingError(
610 raise error.ProgrammingError(
618 'wire protocol command %s is not available' % command)
611 'wire protocol command %s is not available' % command)
619
612
620 cmdinfo = self._descriptor['commands'][command]
613 cmdinfo = self._descriptor['commands'][command]
621 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
614 unknownargs = set(args.keys()) - set(cmdinfo.get('args', {}))
622
615
623 if unknownargs:
616 if unknownargs:
624 raise error.ProgrammingError(
617 raise error.ProgrammingError(
625 'wire protocol command %s does not accept argument: %s' % (
618 'wire protocol command %s does not accept argument: %s' % (
626 command, ', '.join(sorted(unknownargs))))
619 command, ', '.join(sorted(unknownargs))))
627
620
628 self._neededpermissions |= set(cmdinfo['permissions'])
621 self._neededpermissions |= set(cmdinfo['permissions'])
629
622
630 # TODO we /could/ also validate types here, since the API descriptor
623 # TODO we /could/ also validate types here, since the API descriptor
631 # includes types...
624 # includes types...
632
625
633 f = pycompat.futures.Future()
626 f = pycompat.futures.Future()
634
627
635 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
628 # Monkeypatch it so result() triggers sendcommands(), otherwise result()
636 # could deadlock.
629 # could deadlock.
637 f.__class__ = queuedcommandfuture
630 f.__class__ = queuedcommandfuture
638 f._peerexecutor = self
631 f._peerexecutor = self
639
632
640 self._futures.add(f)
633 self._futures.add(f)
641 self._calls.append((command, args, f))
634 self._calls.append((command, args, f))
642
635
643 return f
636 return f
644
637
645 def sendcommands(self):
638 def sendcommands(self):
646 if self._sent:
639 if self._sent:
647 return
640 return
648
641
649 if not self._calls:
642 if not self._calls:
650 return
643 return
651
644
652 self._sent = True
645 self._sent = True
653
646
654 # Unhack any future types so caller sees a clean type and so we
647 # Unhack any future types so caller sees a clean type and so we
655 # break reference cycle.
648 # break reference cycle.
656 for f in self._futures:
649 for f in self._futures:
657 if isinstance(f, queuedcommandfuture):
650 if isinstance(f, queuedcommandfuture):
658 f.__class__ = pycompat.futures.Future
651 f.__class__ = pycompat.futures.Future
659 f._peerexecutor = None
652 f._peerexecutor = None
660
653
661 # Mark the future as running and filter out cancelled futures.
654 # Mark the future as running and filter out cancelled futures.
662 calls = [(command, args, f)
655 calls = [(command, args, f)
663 for command, args, f in self._calls
656 for command, args, f in self._calls
664 if f.set_running_or_notify_cancel()]
657 if f.set_running_or_notify_cancel()]
665
658
666 # Clear out references, prevent improper object usage.
659 # Clear out references, prevent improper object usage.
667 self._calls = None
660 self._calls = None
668
661
669 if not calls:
662 if not calls:
670 return
663 return
671
664
672 permissions = set(self._neededpermissions)
665 permissions = set(self._neededpermissions)
673
666
674 if 'push' in permissions and 'pull' in permissions:
667 if 'push' in permissions and 'pull' in permissions:
675 permissions.remove('pull')
668 permissions.remove('pull')
676
669
677 if len(permissions) > 1:
670 if len(permissions) > 1:
678 raise error.RepoError(_('cannot make request requiring multiple '
671 raise error.RepoError(_('cannot make request requiring multiple '
679 'permissions: %s') %
672 'permissions: %s') %
680 _(', ').join(sorted(permissions)))
673 _(', ').join(sorted(permissions)))
681
674
682 permission = {
675 permission = {
683 'push': 'rw',
676 'push': 'rw',
684 'pull': 'ro',
677 'pull': 'ro',
685 }[permissions.pop()]
678 }[permissions.pop()]
686
679
687 reactor, requests, resp = sendv2request(
680 handler, resp = sendv2request(
688 self._ui, self._opener, self._requestbuilder, self._apiurl,
681 self._ui, self._opener, self._requestbuilder, self._apiurl,
689 permission, calls)
682 permission, calls)
690
683
691 # TODO we probably want to validate the HTTP code, media type, etc.
684 # TODO we probably want to validate the HTTP code, media type, etc.
692
685
693 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
686 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
694 self._responsef = self._responseexecutor.submit(self._handleresponse,
687 self._responsef = self._responseexecutor.submit(self._handleresponse,
695 reactor,
688 handler, resp)
696 requests,
697 resp)
698
689
699 def close(self):
690 def close(self):
700 if self._closed:
691 if self._closed:
701 return
692 return
702
693
703 self.sendcommands()
694 self.sendcommands()
704
695
705 self._closed = True
696 self._closed = True
706
697
707 if not self._responsef:
698 if not self._responsef:
708 return
699 return
709
700
710 try:
701 try:
711 self._responsef.result()
702 self._responsef.result()
712 finally:
703 finally:
713 self._responseexecutor.shutdown(wait=True)
704 self._responseexecutor.shutdown(wait=True)
714 self._responsef = None
705 self._responsef = None
715 self._responseexecutor = None
706 self._responseexecutor = None
716
707
717 # If any of our futures are still in progress, mark them as
708 # If any of our futures are still in progress, mark them as
718 # errored, otherwise a result() could wait indefinitely.
709 # errored, otherwise a result() could wait indefinitely.
719 for f in self._futures:
710 for f in self._futures:
720 if not f.done():
711 if not f.done():
721 f.set_exception(error.ResponseError(
712 f.set_exception(error.ResponseError(
722 _('unfulfilled command response')))
713 _('unfulfilled command response')))
723
714
724 self._futures = None
715 self._futures = None
725
716
726 def _handleresponse(self, reactor, requests, resp):
717 def _handleresponse(self, handler, resp):
727 # Called in a thread to read the response.
718 # Called in a thread to read the response.
728
719
729 results = {k: [] for k in requests}
720 while handler.readframe(resp):
730
721 pass
731 while True:
732 frame = wireprotoframing.readframe(resp)
733 if frame is None:
734 break
735
736 self._ui.note(_('received %r\n') % frame)
737
738 # Guard against receiving a frame with a request ID that we
739 # didn't issue. This should never happen.
740 request, f = requests.get(frame.requestid, [None, None])
741
742 action, meta = reactor.onframerecv(frame)
743
744 if action == 'responsedata':
745 assert request.requestid == meta['request'].requestid
746
747 result = results[request.requestid]
748
749 if meta['cbor']:
750 payload = util.bytesio(meta['data'])
751
752 decoder = cbor.CBORDecoder(payload)
753 while payload.tell() + 1 < len(meta['data']):
754 try:
755 result.append(decoder.decode())
756 except Exception:
757 pycompat.future_set_exception_info(
758 f, sys.exc_info()[1:])
759 continue
760 else:
761 result.append(meta['data'])
762
763 if meta['eos']:
764 f.set_result(result)
765 del results[request.requestid]
766
767 elif action == 'error':
768 e = error.RepoError(meta['message'])
769
770 if f:
771 f.set_exception(e)
772 else:
773 raise e
774
775 else:
776 e = error.ProgrammingError('unhandled action: %s' % action)
777
778 if f:
779 f.set_exception(e)
780 else:
781 raise e
782
722
783 # TODO implement interface for version 2 peers
723 # TODO implement interface for version 2 peers
784 @zi.implementer(repository.ipeerconnection, repository.ipeercapabilities,
724 @zi.implementer(repository.ipeerconnection, repository.ipeercapabilities,
785 repository.ipeerrequests)
725 repository.ipeerrequests)
786 class httpv2peer(object):
726 class httpv2peer(object):
787 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
727 def __init__(self, ui, repourl, apipath, opener, requestbuilder,
788 apidescriptor):
728 apidescriptor):
789 self.ui = ui
729 self.ui = ui
790
730
791 if repourl.endswith('/'):
731 if repourl.endswith('/'):
792 repourl = repourl[:-1]
732 repourl = repourl[:-1]
793
733
794 self._url = repourl
734 self._url = repourl
795 self._apipath = apipath
735 self._apipath = apipath
796 self._apiurl = '%s/%s' % (repourl, apipath)
736 self._apiurl = '%s/%s' % (repourl, apipath)
797 self._opener = opener
737 self._opener = opener
798 self._requestbuilder = requestbuilder
738 self._requestbuilder = requestbuilder
799 self._descriptor = apidescriptor
739 self._descriptor = apidescriptor
800
740
801 # Start of ipeerconnection.
741 # Start of ipeerconnection.
802
742
803 def url(self):
743 def url(self):
804 return self._url
744 return self._url
805
745
806 def local(self):
746 def local(self):
807 return None
747 return None
808
748
809 def peer(self):
749 def peer(self):
810 return self
750 return self
811
751
812 def canpush(self):
752 def canpush(self):
813 # TODO change once implemented.
753 # TODO change once implemented.
814 return False
754 return False
815
755
816 def close(self):
756 def close(self):
817 pass
757 pass
818
758
819 # End of ipeerconnection.
759 # End of ipeerconnection.
820
760
821 # Start of ipeercapabilities.
761 # Start of ipeercapabilities.
822
762
823 def capable(self, name):
763 def capable(self, name):
824 # The capabilities used internally historically map to capabilities
764 # The capabilities used internally historically map to capabilities
825 # advertised from the "capabilities" wire protocol command. However,
765 # advertised from the "capabilities" wire protocol command. However,
826 # version 2 of that command works differently.
766 # version 2 of that command works differently.
827
767
828 # Maps to commands that are available.
768 # Maps to commands that are available.
829 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
769 if name in ('branchmap', 'getbundle', 'known', 'lookup', 'pushkey'):
830 return True
770 return True
831
771
832 # Other concepts.
772 # Other concepts.
833 if name in ('bundle2',):
773 if name in ('bundle2',):
834 return True
774 return True
835
775
836 return False
776 return False
837
777
838 def requirecap(self, name, purpose):
778 def requirecap(self, name, purpose):
839 if self.capable(name):
779 if self.capable(name):
840 return
780 return
841
781
842 raise error.CapabilityError(
782 raise error.CapabilityError(
843 _('cannot %s; client or remote repository does not support the %r '
783 _('cannot %s; client or remote repository does not support the %r '
844 'capability') % (purpose, name))
784 'capability') % (purpose, name))
845
785
846 # End of ipeercapabilities.
786 # End of ipeercapabilities.
847
787
848 def _call(self, name, **args):
788 def _call(self, name, **args):
849 with self.commandexecutor() as e:
789 with self.commandexecutor() as e:
850 return e.callcommand(name, args).result()
790 return e.callcommand(name, args).result()
851
791
852 def commandexecutor(self):
792 def commandexecutor(self):
853 return httpv2executor(self.ui, self._opener, self._requestbuilder,
793 return httpv2executor(self.ui, self._opener, self._requestbuilder,
854 self._apiurl, self._descriptor)
794 self._apiurl, self._descriptor)
855
795
856 # Registry of API service names to metadata about peers that handle it.
796 # Registry of API service names to metadata about peers that handle it.
857 #
797 #
858 # The following keys are meaningful:
798 # The following keys are meaningful:
859 #
799 #
860 # init
800 # init
861 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
801 # Callable receiving (ui, repourl, servicepath, opener, requestbuilder,
862 # apidescriptor) to create a peer.
802 # apidescriptor) to create a peer.
863 #
803 #
864 # priority
804 # priority
865 # Integer priority for the service. If we could choose from multiple
805 # Integer priority for the service. If we could choose from multiple
866 # services, we choose the one with the highest priority.
806 # services, we choose the one with the highest priority.
867 API_PEERS = {
807 API_PEERS = {
868 wireprototypes.HTTP_WIREPROTO_V2: {
808 wireprototypes.HTTP_WIREPROTO_V2: {
869 'init': httpv2peer,
809 'init': httpv2peer,
870 'priority': 50,
810 'priority': 50,
871 },
811 },
872 }
812 }
873
813
874 def performhandshake(ui, url, opener, requestbuilder):
814 def performhandshake(ui, url, opener, requestbuilder):
875 # The handshake is a request to the capabilities command.
815 # The handshake is a request to the capabilities command.
876
816
877 caps = None
817 caps = None
878 def capable(x):
818 def capable(x):
879 raise error.ProgrammingError('should not be called')
819 raise error.ProgrammingError('should not be called')
880
820
881 args = {}
821 args = {}
882
822
883 # The client advertises support for newer protocols by adding an
823 # The client advertises support for newer protocols by adding an
884 # X-HgUpgrade-* header with a list of supported APIs and an
824 # X-HgUpgrade-* header with a list of supported APIs and an
885 # X-HgProto-* header advertising which serializing formats it supports.
825 # X-HgProto-* header advertising which serializing formats it supports.
886 # We only support the HTTP version 2 transport and CBOR responses for
826 # We only support the HTTP version 2 transport and CBOR responses for
887 # now.
827 # now.
888 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
828 advertisev2 = ui.configbool('experimental', 'httppeer.advertise-v2')
889
829
890 if advertisev2:
830 if advertisev2:
891 args['headers'] = {
831 args['headers'] = {
892 r'X-HgProto-1': r'cbor',
832 r'X-HgProto-1': r'cbor',
893 }
833 }
894
834
895 args['headers'].update(
835 args['headers'].update(
896 encodevalueinheaders(' '.join(sorted(API_PEERS)),
836 encodevalueinheaders(' '.join(sorted(API_PEERS)),
897 'X-HgUpgrade',
837 'X-HgUpgrade',
898 # We don't know the header limit this early.
838 # We don't know the header limit this early.
899 # So make it small.
839 # So make it small.
900 1024))
840 1024))
901
841
902 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
842 req, requrl, qs = makev1commandrequest(ui, requestbuilder, caps,
903 capable, url, 'capabilities',
843 capable, url, 'capabilities',
904 args)
844 args)
905
845
906 resp = sendrequest(ui, opener, req)
846 resp = sendrequest(ui, opener, req)
907
847
908 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
848 respurl, ct, resp = parsev1commandresponse(ui, url, requrl, qs, resp,
909 compressible=False,
849 compressible=False,
910 allowcbor=advertisev2)
850 allowcbor=advertisev2)
911
851
912 try:
852 try:
913 rawdata = resp.read()
853 rawdata = resp.read()
914 finally:
854 finally:
915 resp.close()
855 resp.close()
916
856
917 if not ct.startswith('application/mercurial-'):
857 if not ct.startswith('application/mercurial-'):
918 raise error.ProgrammingError('unexpected content-type: %s' % ct)
858 raise error.ProgrammingError('unexpected content-type: %s' % ct)
919
859
920 if advertisev2:
860 if advertisev2:
921 if ct == 'application/mercurial-cbor':
861 if ct == 'application/mercurial-cbor':
922 try:
862 try:
923 info = cbor.loads(rawdata)
863 info = cbor.loads(rawdata)
924 except cbor.CBORDecodeError:
864 except cbor.CBORDecodeError:
925 raise error.Abort(_('error decoding CBOR from remote server'),
865 raise error.Abort(_('error decoding CBOR from remote server'),
926 hint=_('try again and consider contacting '
866 hint=_('try again and consider contacting '
927 'the server operator'))
867 'the server operator'))
928
868
929 # We got a legacy response. That's fine.
869 # We got a legacy response. That's fine.
930 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
870 elif ct in ('application/mercurial-0.1', 'application/mercurial-0.2'):
931 info = {
871 info = {
932 'v1capabilities': set(rawdata.split())
872 'v1capabilities': set(rawdata.split())
933 }
873 }
934
874
935 else:
875 else:
936 raise error.RepoError(
876 raise error.RepoError(
937 _('unexpected response type from server: %s') % ct)
877 _('unexpected response type from server: %s') % ct)
938 else:
878 else:
939 info = {
879 info = {
940 'v1capabilities': set(rawdata.split())
880 'v1capabilities': set(rawdata.split())
941 }
881 }
942
882
943 return respurl, info
883 return respurl, info
944
884
945 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
885 def makepeer(ui, path, opener=None, requestbuilder=urlreq.request):
946 """Construct an appropriate HTTP peer instance.
886 """Construct an appropriate HTTP peer instance.
947
887
948 ``opener`` is an ``url.opener`` that should be used to establish
888 ``opener`` is an ``url.opener`` that should be used to establish
949 connections, perform HTTP requests.
889 connections, perform HTTP requests.
950
890
951 ``requestbuilder`` is the type used for constructing HTTP requests.
891 ``requestbuilder`` is the type used for constructing HTTP requests.
952 It exists as an argument so extensions can override the default.
892 It exists as an argument so extensions can override the default.
953 """
893 """
954 u = util.url(path)
894 u = util.url(path)
955 if u.query or u.fragment:
895 if u.query or u.fragment:
956 raise error.Abort(_('unsupported URL component: "%s"') %
896 raise error.Abort(_('unsupported URL component: "%s"') %
957 (u.query or u.fragment))
897 (u.query or u.fragment))
958
898
959 # urllib cannot handle URLs with embedded user or passwd.
899 # urllib cannot handle URLs with embedded user or passwd.
960 url, authinfo = u.authinfo()
900 url, authinfo = u.authinfo()
961 ui.debug('using %s\n' % url)
901 ui.debug('using %s\n' % url)
962
902
963 opener = opener or urlmod.opener(ui, authinfo)
903 opener = opener or urlmod.opener(ui, authinfo)
964
904
965 respurl, info = performhandshake(ui, url, opener, requestbuilder)
905 respurl, info = performhandshake(ui, url, opener, requestbuilder)
966
906
967 # Given the intersection of APIs that both we and the server support,
907 # Given the intersection of APIs that both we and the server support,
968 # sort by their advertised priority and pick the first one.
908 # sort by their advertised priority and pick the first one.
969 #
909 #
970 # TODO consider making this request-based and interface driven. For
910 # TODO consider making this request-based and interface driven. For
971 # example, the caller could say "I want a peer that does X." It's quite
911 # example, the caller could say "I want a peer that does X." It's quite
972 # possible that not all peers would do that. Since we know the service
912 # possible that not all peers would do that. Since we know the service
973 # capabilities, we could filter out services not meeting the
913 # capabilities, we could filter out services not meeting the
974 # requirements. Possibly by consulting the interfaces defined by the
914 # requirements. Possibly by consulting the interfaces defined by the
975 # peer type.
915 # peer type.
976 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
916 apipeerchoices = set(info.get('apis', {}).keys()) & set(API_PEERS.keys())
977
917
978 preferredchoices = sorted(apipeerchoices,
918 preferredchoices = sorted(apipeerchoices,
979 key=lambda x: API_PEERS[x]['priority'],
919 key=lambda x: API_PEERS[x]['priority'],
980 reverse=True)
920 reverse=True)
981
921
982 for service in preferredchoices:
922 for service in preferredchoices:
983 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
923 apipath = '%s/%s' % (info['apibase'].rstrip('/'), service)
984
924
985 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
925 return API_PEERS[service]['init'](ui, respurl, apipath, opener,
986 requestbuilder,
926 requestbuilder,
987 info['apis'][service])
927 info['apis'][service])
988
928
989 # Failed to construct an API peer. Fall back to legacy.
929 # Failed to construct an API peer. Fall back to legacy.
990 return httppeer(ui, path, respurl, opener, requestbuilder,
930 return httppeer(ui, path, respurl, opener, requestbuilder,
991 info['v1capabilities'])
931 info['v1capabilities'])
992
932
993 def instance(ui, path, create, intents=None):
933 def instance(ui, path, create, intents=None):
994 if create:
934 if create:
995 raise error.Abort(_('cannot create new http repository'))
935 raise error.Abort(_('cannot create new http repository'))
996 try:
936 try:
997 if path.startswith('https:') and not urlmod.has_https:
937 if path.startswith('https:') and not urlmod.has_https:
998 raise error.Abort(_('Python support for SSL and HTTPS '
938 raise error.Abort(_('Python support for SSL and HTTPS '
999 'is not installed'))
939 'is not installed'))
1000
940
1001 inst = makepeer(ui, path)
941 inst = makepeer(ui, path)
1002
942
1003 return inst
943 return inst
1004 except error.RepoError as httpexception:
944 except error.RepoError as httpexception:
1005 try:
945 try:
1006 r = statichttprepo.instance(ui, "static-" + path, create)
946 r = statichttprepo.instance(ui, "static-" + path, create)
1007 ui.note(_('(falling back to static-http)\n'))
947 ui.note(_('(falling back to static-http)\n'))
1008 return r
948 return r
1009 except error.RepoError:
949 except error.RepoError:
1010 raise httpexception # use the original http RepoError instead
950 raise httpexception # use the original http RepoError instead
@@ -1,121 +1,121
1 $ . $TESTDIR/wireprotohelpers.sh
1 $ . $TESTDIR/wireprotohelpers.sh
2
2
3 $ hg init server
3 $ hg init server
4 $ enablehttpv2 server
4 $ enablehttpv2 server
5 $ cd server
5 $ cd server
6 $ hg debugdrawdag << EOF
6 $ hg debugdrawdag << EOF
7 > C D
7 > C D
8 > |/
8 > |/
9 > B
9 > B
10 > |
10 > |
11 > A
11 > A
12 > EOF
12 > EOF
13
13
14 $ hg log -T '{rev}:{node} {desc}\n'
14 $ hg log -T '{rev}:{node} {desc}\n'
15 3:be0ef73c17ade3fc89dc41701eb9fc3a91b58282 D
15 3:be0ef73c17ade3fc89dc41701eb9fc3a91b58282 D
16 2:26805aba1e600a82e93661149f2313866a221a7b C
16 2:26805aba1e600a82e93661149f2313866a221a7b C
17 1:112478962961147124edd43549aedd1a335e44bf B
17 1:112478962961147124edd43549aedd1a335e44bf B
18 0:426bada5c67598ca65036d57d9e4b64b0c1ce7a0 A
18 0:426bada5c67598ca65036d57d9e4b64b0c1ce7a0 A
19
19
20 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
20 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
21 $ cat hg.pid > $DAEMON_PIDS
21 $ cat hg.pid > $DAEMON_PIDS
22
22
23 No arguments returns something reasonable
23 No arguments returns something reasonable
24
24
25 $ sendhttpv2peer << EOF
25 $ sendhttpv2peer << EOF
26 > command known
26 > command known
27 > EOF
27 > EOF
28 creating http peer for wire protocol version 2
28 creating http peer for wire protocol version 2
29 sending known command
29 sending known command
30 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
30 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
31 s> Accept-Encoding: identity\r\n
31 s> Accept-Encoding: identity\r\n
32 s> accept: application/mercurial-exp-framing-0003\r\n
32 s> accept: application/mercurial-exp-framing-0003\r\n
33 s> content-type: application/mercurial-exp-framing-0003\r\n
33 s> content-type: application/mercurial-exp-framing-0003\r\n
34 s> content-length: 20\r\n
34 s> content-length: 20\r\n
35 s> host: $LOCALIP:$HGPORT\r\n (glob)
35 s> host: $LOCALIP:$HGPORT\r\n (glob)
36 s> user-agent: Mercurial debugwireproto\r\n
36 s> user-agent: Mercurial debugwireproto\r\n
37 s> \r\n
37 s> \r\n
38 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEknown
38 s> \x0c\x00\x00\x01\x00\x01\x01\x11\xa1DnameEknown
39 s> makefile('rb', None)
39 s> makefile('rb', None)
40 s> HTTP/1.1 200 OK\r\n
40 s> HTTP/1.1 200 OK\r\n
41 s> Server: testing stub value\r\n
41 s> Server: testing stub value\r\n
42 s> Date: $HTTP_DATE$\r\n
42 s> Date: $HTTP_DATE$\r\n
43 s> Content-Type: application/mercurial-exp-framing-0003\r\n
43 s> Content-Type: application/mercurial-exp-framing-0003\r\n
44 s> Transfer-Encoding: chunked\r\n
44 s> Transfer-Encoding: chunked\r\n
45 s> \r\n
45 s> \r\n
46 s> 9\r\n
46 s> 9\r\n
47 s> \x01\x00\x00\x01\x00\x02\x01F
47 s> \x01\x00\x00\x01\x00\x02\x01F
48 s> @
48 s> @
49 s> \r\n
49 s> \r\n
50 received frame(size=1; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
50 received frame(size=1; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
51 s> 0\r\n
51 s> 0\r\n
52 s> \r\n
52 s> \r\n
53 response: []
53 response: [b'']
54
54
55 Single known node works
55 Single known node works
56
56
57 $ sendhttpv2peer << EOF
57 $ sendhttpv2peer << EOF
58 > command known
58 > command known
59 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0']
59 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0']
60 > EOF
60 > EOF
61 creating http peer for wire protocol version 2
61 creating http peer for wire protocol version 2
62 sending known command
62 sending known command
63 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
63 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
64 s> Accept-Encoding: identity\r\n
64 s> Accept-Encoding: identity\r\n
65 s> accept: application/mercurial-exp-framing-0003\r\n
65 s> accept: application/mercurial-exp-framing-0003\r\n
66 s> content-type: application/mercurial-exp-framing-0003\r\n
66 s> content-type: application/mercurial-exp-framing-0003\r\n
67 s> content-length: 54\r\n
67 s> content-length: 54\r\n
68 s> host: $LOCALIP:$HGPORT\r\n (glob)
68 s> host: $LOCALIP:$HGPORT\r\n (glob)
69 s> user-agent: Mercurial debugwireproto\r\n
69 s> user-agent: Mercurial debugwireproto\r\n
70 s> \r\n
70 s> \r\n
71 s> .\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1Enodes\x81TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0DnameEknown
71 s> .\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1Enodes\x81TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0DnameEknown
72 s> makefile('rb', None)
72 s> makefile('rb', None)
73 s> HTTP/1.1 200 OK\r\n
73 s> HTTP/1.1 200 OK\r\n
74 s> Server: testing stub value\r\n
74 s> Server: testing stub value\r\n
75 s> Date: $HTTP_DATE$\r\n
75 s> Date: $HTTP_DATE$\r\n
76 s> Content-Type: application/mercurial-exp-framing-0003\r\n
76 s> Content-Type: application/mercurial-exp-framing-0003\r\n
77 s> Transfer-Encoding: chunked\r\n
77 s> Transfer-Encoding: chunked\r\n
78 s> \r\n
78 s> \r\n
79 s> a\r\n
79 s> a\r\n
80 s> \x02\x00\x00\x01\x00\x02\x01F
80 s> \x02\x00\x00\x01\x00\x02\x01F
81 s> A1
81 s> A1
82 s> \r\n
82 s> \r\n
83 received frame(size=2; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
83 received frame(size=2; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
84 s> 0\r\n
84 s> 0\r\n
85 s> \r\n
85 s> \r\n
86 response: [b'1']
86 response: [b'1']
87
87
88 Multiple nodes works
88 Multiple nodes works
89
89
90 $ sendhttpv2peer << EOF
90 $ sendhttpv2peer << EOF
91 > command known
91 > command known
92 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0', b'00000000000000000000', b'\x11\x24\x78\x96\x29\x61\x14\x71\x24\xed\xd4\x35\x49\xae\xdd\x1a\x33\x5e\x44\xbf']
92 > nodes eval:[b'\x42\x6b\xad\xa5\xc6\x75\x98\xca\x65\x03\x6d\x57\xd9\xe4\xb6\x4b\x0c\x1c\xe7\xa0', b'00000000000000000000', b'\x11\x24\x78\x96\x29\x61\x14\x71\x24\xed\xd4\x35\x49\xae\xdd\x1a\x33\x5e\x44\xbf']
93 > EOF
93 > EOF
94 creating http peer for wire protocol version 2
94 creating http peer for wire protocol version 2
95 sending known command
95 sending known command
96 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
96 s> POST /api/exp-http-v2-0001/ro/known HTTP/1.1\r\n
97 s> Accept-Encoding: identity\r\n
97 s> Accept-Encoding: identity\r\n
98 s> accept: application/mercurial-exp-framing-0003\r\n
98 s> accept: application/mercurial-exp-framing-0003\r\n
99 s> content-type: application/mercurial-exp-framing-0003\r\n
99 s> content-type: application/mercurial-exp-framing-0003\r\n
100 s> content-length: 96\r\n
100 s> content-length: 96\r\n
101 s> host: $LOCALIP:$HGPORT\r\n (glob)
101 s> host: $LOCALIP:$HGPORT\r\n (glob)
102 s> user-agent: Mercurial debugwireproto\r\n
102 s> user-agent: Mercurial debugwireproto\r\n
103 s> \r\n
103 s> \r\n
104 s> X\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1Enodes\x83TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0T00000000000000000000T\x11$x\x96)a\x14q$\xed\xd45I\xae\xdd\x1a3^D\xbfDnameEknown
104 s> X\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1Enodes\x83TBk\xad\xa5\xc6u\x98\xcae\x03mW\xd9\xe4\xb6K\x0c\x1c\xe7\xa0T00000000000000000000T\x11$x\x96)a\x14q$\xed\xd45I\xae\xdd\x1a3^D\xbfDnameEknown
105 s> makefile('rb', None)
105 s> makefile('rb', None)
106 s> HTTP/1.1 200 OK\r\n
106 s> HTTP/1.1 200 OK\r\n
107 s> Server: testing stub value\r\n
107 s> Server: testing stub value\r\n
108 s> Date: $HTTP_DATE$\r\n
108 s> Date: $HTTP_DATE$\r\n
109 s> Content-Type: application/mercurial-exp-framing-0003\r\n
109 s> Content-Type: application/mercurial-exp-framing-0003\r\n
110 s> Transfer-Encoding: chunked\r\n
110 s> Transfer-Encoding: chunked\r\n
111 s> \r\n
111 s> \r\n
112 s> c\r\n
112 s> c\r\n
113 s> \x04\x00\x00\x01\x00\x02\x01F
113 s> \x04\x00\x00\x01\x00\x02\x01F
114 s> C101
114 s> C101
115 s> \r\n
115 s> \r\n
116 received frame(size=4; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
116 received frame(size=4; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
117 s> 0\r\n
117 s> 0\r\n
118 s> \r\n
118 s> \r\n
119 response: [b'101']
119 response: [b'101']
120
120
121 $ cat error.log
121 $ cat error.log
@@ -1,89 +1,89
1 $ . $TESTDIR/wireprotohelpers.sh
1 $ . $TESTDIR/wireprotohelpers.sh
2
2
3 $ hg init server
3 $ hg init server
4 $ enablehttpv2 server
4 $ enablehttpv2 server
5 $ cd server
5 $ cd server
6 $ cat >> .hg/hgrc << EOF
6 $ cat >> .hg/hgrc << EOF
7 > [web]
7 > [web]
8 > push_ssl = false
8 > push_ssl = false
9 > allow-push = *
9 > allow-push = *
10 > EOF
10 > EOF
11 $ hg debugdrawdag << EOF
11 $ hg debugdrawdag << EOF
12 > C D
12 > C D
13 > |/
13 > |/
14 > B
14 > B
15 > |
15 > |
16 > A
16 > A
17 > EOF
17 > EOF
18
18
19 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
19 $ hg serve -p $HGPORT -d --pid-file hg.pid -E error.log
20 $ cat hg.pid > $DAEMON_PIDS
20 $ cat hg.pid > $DAEMON_PIDS
21
21
22 pushkey for a bookmark works
22 pushkey for a bookmark works
23
23
24 $ sendhttpv2peer << EOF
24 $ sendhttpv2peer << EOF
25 > command pushkey
25 > command pushkey
26 > namespace bookmarks
26 > namespace bookmarks
27 > key @
27 > key @
28 > old
28 > old
29 > new 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
29 > new 426bada5c67598ca65036d57d9e4b64b0c1ce7a0
30 > EOF
30 > EOF
31 creating http peer for wire protocol version 2
31 creating http peer for wire protocol version 2
32 sending pushkey command
32 sending pushkey command
33 s> *\r\n (glob)
33 s> *\r\n (glob)
34 s> Accept-Encoding: identity\r\n
34 s> Accept-Encoding: identity\r\n
35 s> accept: application/mercurial-exp-framing-0003\r\n
35 s> accept: application/mercurial-exp-framing-0003\r\n
36 s> content-type: application/mercurial-exp-framing-0003\r\n
36 s> content-type: application/mercurial-exp-framing-0003\r\n
37 s> content-length: 105\r\n
37 s> content-length: 105\r\n
38 s> host: $LOCALIP:$HGPORT\r\n (glob)
38 s> host: $LOCALIP:$HGPORT\r\n (glob)
39 s> user-agent: Mercurial debugwireproto\r\n
39 s> user-agent: Mercurial debugwireproto\r\n
40 s> \r\n
40 s> \r\n
41 s> a\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa4CkeyA@CnewX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0Cold@InamespaceIbookmarksDnameGpushkey
41 s> a\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa4CkeyA@CnewX(426bada5c67598ca65036d57d9e4b64b0c1ce7a0Cold@InamespaceIbookmarksDnameGpushkey
42 s> makefile('rb', None)
42 s> makefile('rb', None)
43 s> HTTP/1.1 200 OK\r\n
43 s> HTTP/1.1 200 OK\r\n
44 s> Server: testing stub value\r\n
44 s> Server: testing stub value\r\n
45 s> Date: $HTTP_DATE$\r\n
45 s> Date: $HTTP_DATE$\r\n
46 s> Content-Type: application/mercurial-exp-framing-0003\r\n
46 s> Content-Type: application/mercurial-exp-framing-0003\r\n
47 s> Transfer-Encoding: chunked\r\n
47 s> Transfer-Encoding: chunked\r\n
48 s> \r\n
48 s> \r\n
49 s> 9\r\n
49 s> 9\r\n
50 s> *\x00\x01\x00\x02\x01F (glob)
50 s> *\x00\x01\x00\x02\x01F (glob)
51 s> \xf5
51 s> \xf5
52 s> \r\n
52 s> \r\n
53 received frame(size=*; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) (glob)
53 received frame(size=*; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor) (glob)
54 s> 0\r\n
54 s> 0\r\n
55 s> \r\n
55 s> \r\n
56 response: []
56 response: [True]
57
57
58 $ sendhttpv2peer << EOF
58 $ sendhttpv2peer << EOF
59 > command listkeys
59 > command listkeys
60 > namespace bookmarks
60 > namespace bookmarks
61 > EOF
61 > EOF
62 creating http peer for wire protocol version 2
62 creating http peer for wire protocol version 2
63 sending listkeys command
63 sending listkeys command
64 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
64 s> POST /api/exp-http-v2-0001/ro/listkeys HTTP/1.1\r\n
65 s> Accept-Encoding: identity\r\n
65 s> Accept-Encoding: identity\r\n
66 s> accept: application/mercurial-exp-framing-0003\r\n
66 s> accept: application/mercurial-exp-framing-0003\r\n
67 s> content-type: application/mercurial-exp-framing-0003\r\n
67 s> content-type: application/mercurial-exp-framing-0003\r\n
68 s> content-length: 49\r\n
68 s> content-length: 49\r\n
69 s> host: $LOCALIP:$HGPORT\r\n (glob)
69 s> host: $LOCALIP:$HGPORT\r\n (glob)
70 s> user-agent: Mercurial debugwireproto\r\n
70 s> user-agent: Mercurial debugwireproto\r\n
71 s> \r\n
71 s> \r\n
72 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
72 s> )\x00\x00\x01\x00\x01\x01\x11\xa2Dargs\xa1InamespaceIbookmarksDnameHlistkeys
73 s> makefile('rb', None)
73 s> makefile('rb', None)
74 s> HTTP/1.1 200 OK\r\n
74 s> HTTP/1.1 200 OK\r\n
75 s> Server: testing stub value\r\n
75 s> Server: testing stub value\r\n
76 s> Date: $HTTP_DATE$\r\n
76 s> Date: $HTTP_DATE$\r\n
77 s> Content-Type: application/mercurial-exp-framing-0003\r\n
77 s> Content-Type: application/mercurial-exp-framing-0003\r\n
78 s> Transfer-Encoding: chunked\r\n
78 s> Transfer-Encoding: chunked\r\n
79 s> \r\n
79 s> \r\n
80 s> 35\r\n
80 s> 35\r\n
81 s> -\x00\x00\x01\x00\x02\x01F
81 s> -\x00\x00\x01\x00\x02\x01F
82 s> \xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
82 s> \xa1A@X(426bada5c67598ca65036d57d9e4b64b0c1ce7a0
83 s> \r\n
83 s> \r\n
84 received frame(size=45; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
84 received frame(size=45; request=1; stream=2; streamflags=stream-begin; type=bytes-response; flags=eos|cbor)
85 s> 0\r\n
85 s> 0\r\n
86 s> \r\n
86 s> \r\n
87 response: [{b'@': b'426bada5c67598ca65036d57d9e4b64b0c1ce7a0'}]
87 response: [{b'@': b'426bada5c67598ca65036d57d9e4b64b0c1ce7a0'}]
88
88
89 $ cat error.log
89 $ cat error.log
General Comments 0
You need to be logged in to leave comments. Login now