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