##// END OF EJS Templates
wireprototypes: clarify documentation of getbundle argument types...
Martin von Zweigbergk -
r43225:9668744c default
parent child Browse files
Show More
@@ -1,407 +1,407 b''
1 1 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 from __future__ import absolute_import
7 7
8 8 from .node import (
9 9 bin,
10 10 hex,
11 11 )
12 12 from .i18n import _
13 13 from .thirdparty import (
14 14 attr,
15 15 )
16 16 from . import (
17 17 error,
18 18 util,
19 19 )
20 20 from .interfaces import (
21 21 util as interfaceutil,
22 22 )
23 23 from .utils import (
24 24 compression,
25 25 )
26 26
27 27 # Names of the SSH protocol implementations.
28 28 SSHV1 = 'ssh-v1'
29 29 # These are advertised over the wire. Increment the counters at the end
30 30 # to reflect BC breakages.
31 31 SSHV2 = 'exp-ssh-v2-0003'
32 32 HTTP_WIREPROTO_V2 = 'exp-http-v2-0003'
33 33
34 34 NARROWCAP = 'exp-narrow-1'
35 35 ELLIPSESCAP1 = 'exp-ellipses-1'
36 36 ELLIPSESCAP = 'exp-ellipses-2'
37 37 SUPPORTED_ELLIPSESCAP = (ELLIPSESCAP1, ELLIPSESCAP)
38 38
39 39 # All available wire protocol transports.
40 40 TRANSPORTS = {
41 41 SSHV1: {
42 42 'transport': 'ssh',
43 43 'version': 1,
44 44 },
45 45 SSHV2: {
46 46 'transport': 'ssh',
47 47 # TODO mark as version 2 once all commands are implemented.
48 48 'version': 1,
49 49 },
50 50 'http-v1': {
51 51 'transport': 'http',
52 52 'version': 1,
53 53 },
54 54 HTTP_WIREPROTO_V2: {
55 55 'transport': 'http',
56 56 'version': 2,
57 57 }
58 58 }
59 59
60 60 class bytesresponse(object):
61 61 """A wire protocol response consisting of raw bytes."""
62 62 def __init__(self, data):
63 63 self.data = data
64 64
65 65 class ooberror(object):
66 66 """wireproto reply: failure of a batch of operation
67 67
68 68 Something failed during a batch call. The error message is stored in
69 69 `self.message`.
70 70 """
71 71 def __init__(self, message):
72 72 self.message = message
73 73
74 74 class pushres(object):
75 75 """wireproto reply: success with simple integer return
76 76
77 77 The call was successful and returned an integer contained in `self.res`.
78 78 """
79 79 def __init__(self, res, output):
80 80 self.res = res
81 81 self.output = output
82 82
83 83 class pusherr(object):
84 84 """wireproto reply: failure
85 85
86 86 The call failed. The `self.res` attribute contains the error message.
87 87 """
88 88 def __init__(self, res, output):
89 89 self.res = res
90 90 self.output = output
91 91
92 92 class streamres(object):
93 93 """wireproto reply: binary stream
94 94
95 95 The call was successful and the result is a stream.
96 96
97 97 Accepts a generator containing chunks of data to be sent to the client.
98 98
99 99 ``prefer_uncompressed`` indicates that the data is expected to be
100 100 uncompressable and that the stream should therefore use the ``none``
101 101 engine.
102 102 """
103 103 def __init__(self, gen=None, prefer_uncompressed=False):
104 104 self.gen = gen
105 105 self.prefer_uncompressed = prefer_uncompressed
106 106
107 107 class streamreslegacy(object):
108 108 """wireproto reply: uncompressed binary stream
109 109
110 110 The call was successful and the result is a stream.
111 111
112 112 Accepts a generator containing chunks of data to be sent to the client.
113 113
114 114 Like ``streamres``, but sends an uncompressed data for "version 1" clients
115 115 using the application/mercurial-0.1 media type.
116 116 """
117 117 def __init__(self, gen=None):
118 118 self.gen = gen
119 119
120 120 # list of nodes encoding / decoding
121 121 def decodelist(l, sep=' '):
122 122 if l:
123 123 return [bin(v) for v in l.split(sep)]
124 124 return []
125 125
126 126 def encodelist(l, sep=' '):
127 127 try:
128 128 return sep.join(map(hex, l))
129 129 except TypeError:
130 130 raise
131 131
132 132 # batched call argument encoding
133 133
134 134 def escapebatcharg(plain):
135 135 return (plain
136 136 .replace(':', ':c')
137 137 .replace(',', ':o')
138 138 .replace(';', ':s')
139 139 .replace('=', ':e'))
140 140
141 141 def unescapebatcharg(escaped):
142 142 return (escaped
143 143 .replace(':e', '=')
144 144 .replace(':s', ';')
145 145 .replace(':o', ',')
146 146 .replace(':c', ':'))
147 147
148 148 # mapping of options accepted by getbundle and their types
149 149 #
150 # Meant to be extended by extensions. It is extensions responsibility to ensure
151 # such options are properly processed in exchange.getbundle.
150 # Meant to be extended by extensions. It is the extension's responsibility to
151 # ensure such options are properly processed in exchange.getbundle.
152 152 #
153 153 # supported types are:
154 154 #
155 # :nodes: list of binary nodes
156 # :csv: list of comma-separated values
157 # :scsv: list of comma-separated values return as set
155 # :nodes: list of binary nodes, transmitted as space-separated hex nodes
156 # :csv: list of values, transmitted as comma-separated values
157 # :scsv: set of values, transmitted as comma-separated values
158 158 # :plain: string with no transformation needed.
159 159 GETBUNDLE_ARGUMENTS = {
160 160 'heads': 'nodes',
161 161 'bookmarks': 'boolean',
162 162 'common': 'nodes',
163 163 'obsmarkers': 'boolean',
164 164 'phases': 'boolean',
165 165 'bundlecaps': 'scsv',
166 166 'listkeys': 'csv',
167 167 'cg': 'boolean',
168 168 'cbattempted': 'boolean',
169 169 'stream': 'boolean',
170 170 'includepats': 'csv',
171 171 'excludepats': 'csv',
172 172 }
173 173
174 174 class baseprotocolhandler(interfaceutil.Interface):
175 175 """Abstract base class for wire protocol handlers.
176 176
177 177 A wire protocol handler serves as an interface between protocol command
178 178 handlers and the wire protocol transport layer. Protocol handlers provide
179 179 methods to read command arguments, redirect stdio for the duration of
180 180 the request, handle response types, etc.
181 181 """
182 182
183 183 name = interfaceutil.Attribute(
184 184 """The name of the protocol implementation.
185 185
186 186 Used for uniquely identifying the transport type.
187 187 """)
188 188
189 189 def getargs(args):
190 190 """return the value for arguments in <args>
191 191
192 192 For version 1 transports, returns a list of values in the same
193 193 order they appear in ``args``. For version 2 transports, returns
194 194 a dict mapping argument name to value.
195 195 """
196 196
197 197 def getprotocaps():
198 198 """Returns the list of protocol-level capabilities of client
199 199
200 200 Returns a list of capabilities as declared by the client for
201 201 the current request (or connection for stateful protocol handlers)."""
202 202
203 203 def getpayload():
204 204 """Provide a generator for the raw payload.
205 205
206 206 The caller is responsible for ensuring that the full payload is
207 207 processed.
208 208 """
209 209
210 210 def mayberedirectstdio():
211 211 """Context manager to possibly redirect stdio.
212 212
213 213 The context manager yields a file-object like object that receives
214 214 stdout and stderr output when the context manager is active. Or it
215 215 yields ``None`` if no I/O redirection occurs.
216 216
217 217 The intent of this context manager is to capture stdio output
218 218 so it may be sent in the response. Some transports support streaming
219 219 stdio to the client in real time. For these transports, stdio output
220 220 won't be captured.
221 221 """
222 222
223 223 def client():
224 224 """Returns a string representation of this client (as bytes)."""
225 225
226 226 def addcapabilities(repo, caps):
227 227 """Adds advertised capabilities specific to this protocol.
228 228
229 229 Receives the list of capabilities collected so far.
230 230
231 231 Returns a list of capabilities. The passed in argument can be returned.
232 232 """
233 233
234 234 def checkperm(perm):
235 235 """Validate that the client has permissions to perform a request.
236 236
237 237 The argument is the permission required to proceed. If the client
238 238 doesn't have that permission, the exception should raise or abort
239 239 in a protocol specific manner.
240 240 """
241 241
242 242 class commandentry(object):
243 243 """Represents a declared wire protocol command."""
244 244 def __init__(self, func, args='', transports=None,
245 245 permission='push', cachekeyfn=None, extracapabilitiesfn=None):
246 246 self.func = func
247 247 self.args = args
248 248 self.transports = transports or set()
249 249 self.permission = permission
250 250 self.cachekeyfn = cachekeyfn
251 251 self.extracapabilitiesfn = extracapabilitiesfn
252 252
253 253 def _merge(self, func, args):
254 254 """Merge this instance with an incoming 2-tuple.
255 255
256 256 This is called when a caller using the old 2-tuple API attempts
257 257 to replace an instance. The incoming values are merged with
258 258 data not captured by the 2-tuple and a new instance containing
259 259 the union of the two objects is returned.
260 260 """
261 261 return commandentry(func, args=args, transports=set(self.transports),
262 262 permission=self.permission)
263 263
264 264 # Old code treats instances as 2-tuples. So expose that interface.
265 265 def __iter__(self):
266 266 yield self.func
267 267 yield self.args
268 268
269 269 def __getitem__(self, i):
270 270 if i == 0:
271 271 return self.func
272 272 elif i == 1:
273 273 return self.args
274 274 else:
275 275 raise IndexError('can only access elements 0 and 1')
276 276
277 277 class commanddict(dict):
278 278 """Container for registered wire protocol commands.
279 279
280 280 It behaves like a dict. But __setitem__ is overwritten to allow silent
281 281 coercion of values from 2-tuples for API compatibility.
282 282 """
283 283 def __setitem__(self, k, v):
284 284 if isinstance(v, commandentry):
285 285 pass
286 286 # Cast 2-tuples to commandentry instances.
287 287 elif isinstance(v, tuple):
288 288 if len(v) != 2:
289 289 raise ValueError('command tuples must have exactly 2 elements')
290 290
291 291 # It is common for extensions to wrap wire protocol commands via
292 292 # e.g. ``wireproto.commands[x] = (newfn, args)``. Because callers
293 293 # doing this aren't aware of the new API that uses objects to store
294 294 # command entries, we automatically merge old state with new.
295 295 if k in self:
296 296 v = self[k]._merge(v[0], v[1])
297 297 else:
298 298 # Use default values from @wireprotocommand.
299 299 v = commandentry(v[0], args=v[1],
300 300 transports=set(TRANSPORTS),
301 301 permission='push')
302 302 else:
303 303 raise ValueError('command entries must be commandentry instances '
304 304 'or 2-tuples')
305 305
306 306 return super(commanddict, self).__setitem__(k, v)
307 307
308 308 def commandavailable(self, command, proto):
309 309 """Determine if a command is available for the requested protocol."""
310 310 assert proto.name in TRANSPORTS
311 311
312 312 entry = self.get(command)
313 313
314 314 if not entry:
315 315 return False
316 316
317 317 if proto.name not in entry.transports:
318 318 return False
319 319
320 320 return True
321 321
322 322 def supportedcompengines(ui, role):
323 323 """Obtain the list of supported compression engines for a request."""
324 324 assert role in (compression.CLIENTROLE, compression.SERVERROLE)
325 325
326 326 compengines = compression.compengines.supportedwireengines(role)
327 327
328 328 # Allow config to override default list and ordering.
329 329 if role == compression.SERVERROLE:
330 330 configengines = ui.configlist('server', 'compressionengines')
331 331 config = 'server.compressionengines'
332 332 else:
333 333 # This is currently implemented mainly to facilitate testing. In most
334 334 # cases, the server should be in charge of choosing a compression engine
335 335 # because a server has the most to lose from a sub-optimal choice. (e.g.
336 336 # CPU DoS due to an expensive engine or a network DoS due to poor
337 337 # compression ratio).
338 338 configengines = ui.configlist('experimental',
339 339 'clientcompressionengines')
340 340 config = 'experimental.clientcompressionengines'
341 341
342 342 # No explicit config. Filter out the ones that aren't supposed to be
343 343 # advertised and return default ordering.
344 344 if not configengines:
345 345 attr = 'serverpriority' if role == util.SERVERROLE else 'clientpriority'
346 346 return [e for e in compengines
347 347 if getattr(e.wireprotosupport(), attr) > 0]
348 348
349 349 # If compression engines are listed in the config, assume there is a good
350 350 # reason for it (like server operators wanting to achieve specific
351 351 # performance characteristics). So fail fast if the config references
352 352 # unusable compression engines.
353 353 validnames = set(e.name() for e in compengines)
354 354 invalidnames = set(e for e in configengines if e not in validnames)
355 355 if invalidnames:
356 356 raise error.Abort(_('invalid compression engine defined in %s: %s') %
357 357 (config, ', '.join(sorted(invalidnames))))
358 358
359 359 compengines = [e for e in compengines if e.name() in configengines]
360 360 compengines = sorted(compengines,
361 361 key=lambda e: configengines.index(e.name()))
362 362
363 363 if not compengines:
364 364 raise error.Abort(_('%s config option does not specify any known '
365 365 'compression engines') % config,
366 366 hint=_('usable compression engines: %s') %
367 367 ', '.sorted(validnames))
368 368
369 369 return compengines
370 370
371 371 @attr.s
372 372 class encodedresponse(object):
373 373 """Represents response data that is already content encoded.
374 374
375 375 Wire protocol version 2 only.
376 376
377 377 Commands typically emit Python objects that are encoded and sent over the
378 378 wire. If commands emit an object of this type, the encoding step is bypassed
379 379 and the content from this object is used instead.
380 380 """
381 381 data = attr.ib()
382 382
383 383 @attr.s
384 384 class alternatelocationresponse(object):
385 385 """Represents a response available at an alternate location.
386 386
387 387 Instances are sent in place of actual response objects when the server
388 388 is sending a "content redirect" response.
389 389
390 390 Only compatible with wire protocol version 2.
391 391 """
392 392 url = attr.ib()
393 393 mediatype = attr.ib()
394 394 size = attr.ib(default=None)
395 395 fullhashes = attr.ib(default=None)
396 396 fullhashseed = attr.ib(default=None)
397 397 serverdercerts = attr.ib(default=None)
398 398 servercadercerts = attr.ib(default=None)
399 399
400 400 @attr.s
401 401 class indefinitebytestringresponse(object):
402 402 """Represents an object to be encoded to an indefinite length bytestring.
403 403
404 404 Instances are initialized from an iterable of chunks, with each chunk being
405 405 a bytes instance.
406 406 """
407 407 chunks = attr.ib()
General Comments 0
You need to be logged in to leave comments. Login now