##// END OF EJS Templates
sshpeer: add a method to check if a doublepipe is closed...
Valentin Gatien-Baron -
r47407:c9434fcb default draft
parent child Browse files
Show More
@@ -1,707 +1,711 b''
1 1 # sshpeer.py - ssh repository proxy class for mercurial
2 2 #
3 3 # Copyright 2005, 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import re
11 11 import uuid
12 12
13 13 from .i18n import _
14 14 from .pycompat import getattr
15 15 from . import (
16 16 error,
17 17 pycompat,
18 18 util,
19 19 wireprotoserver,
20 20 wireprototypes,
21 21 wireprotov1peer,
22 22 wireprotov1server,
23 23 )
24 24 from .utils import (
25 25 procutil,
26 26 stringutil,
27 27 )
28 28
29 29
30 30 def _serverquote(s):
31 31 """quote a string for the remote shell ... which we assume is sh"""
32 32 if not s:
33 33 return s
34 34 if re.match(b'[a-zA-Z0-9@%_+=:,./-]*$', s):
35 35 return s
36 36 return b"'%s'" % s.replace(b"'", b"'\\''")
37 37
38 38
39 39 def _forwardoutput(ui, pipe, warn=False):
40 40 """display all data currently available on pipe as remote output.
41 41
42 42 This is non blocking."""
43 43 if pipe:
44 44 s = procutil.readpipe(pipe)
45 45 if s:
46 46 display = ui.warn if warn else ui.status
47 47 for l in s.splitlines():
48 48 display(_(b"remote: "), l, b'\n')
49 49
50 50
51 51 class doublepipe(object):
52 52 """Operate a side-channel pipe in addition of a main one
53 53
54 54 The side-channel pipe contains server output to be forwarded to the user
55 55 input. The double pipe will behave as the "main" pipe, but will ensure the
56 56 content of the "side" pipe is properly processed while we wait for blocking
57 57 call on the "main" pipe.
58 58
59 59 If large amounts of data are read from "main", the forward will cease after
60 60 the first bytes start to appear. This simplifies the implementation
61 61 without affecting actual output of sshpeer too much as we rarely issue
62 62 large read for data not yet emitted by the server.
63 63
64 64 The main pipe is expected to be a 'bufferedinputpipe' from the util module
65 65 that handle all the os specific bits. This class lives in this module
66 66 because it focus on behavior specific to the ssh protocol."""
67 67
68 68 def __init__(self, ui, main, side):
69 69 self._ui = ui
70 70 self._main = main
71 71 self._side = side
72 72
73 73 def _wait(self):
74 74 """wait until some data are available on main or side
75 75
76 76 return a pair of boolean (ismainready, issideready)
77 77
78 78 (This will only wait for data if the setup is supported by `util.poll`)
79 79 """
80 80 if (
81 81 isinstance(self._main, util.bufferedinputpipe)
82 82 and self._main.hasbuffer
83 83 ):
84 84 # Main has data. Assume side is worth poking at.
85 85 return True, True
86 86
87 87 fds = [self._main.fileno(), self._side.fileno()]
88 88 try:
89 89 act = util.poll(fds)
90 90 except NotImplementedError:
91 91 # non supported yet case, assume all have data.
92 92 act = fds
93 93 return (self._main.fileno() in act, self._side.fileno() in act)
94 94
95 95 def write(self, data):
96 96 return self._call(b'write', data)
97 97
98 98 def read(self, size):
99 99 r = self._call(b'read', size)
100 100 if size != 0 and not r:
101 101 # We've observed a condition that indicates the
102 102 # stdout closed unexpectedly. Check stderr one
103 103 # more time and snag anything that's there before
104 104 # letting anyone know the main part of the pipe
105 105 # closed prematurely.
106 106 _forwardoutput(self._ui, self._side)
107 107 return r
108 108
109 109 def unbufferedread(self, size):
110 110 r = self._call(b'unbufferedread', size)
111 111 if size != 0 and not r:
112 112 # We've observed a condition that indicates the
113 113 # stdout closed unexpectedly. Check stderr one
114 114 # more time and snag anything that's there before
115 115 # letting anyone know the main part of the pipe
116 116 # closed prematurely.
117 117 _forwardoutput(self._ui, self._side)
118 118 return r
119 119
120 120 def readline(self):
121 121 return self._call(b'readline')
122 122
123 123 def _call(self, methname, data=None):
124 124 """call <methname> on "main", forward output of "side" while blocking"""
125 125 # data can be '' or 0
126 126 if (data is not None and not data) or self._main.closed:
127 127 _forwardoutput(self._ui, self._side)
128 128 return b''
129 129 while True:
130 130 mainready, sideready = self._wait()
131 131 if sideready:
132 132 _forwardoutput(self._ui, self._side)
133 133 if mainready:
134 134 meth = getattr(self._main, methname)
135 135 if data is None:
136 136 return meth()
137 137 else:
138 138 return meth(data)
139 139
140 140 def close(self):
141 141 return self._main.close()
142 142
143 @property
144 def closed(self):
145 return self._main.closed
146
143 147 def flush(self):
144 148 return self._main.flush()
145 149
146 150
147 151 def _cleanuppipes(ui, pipei, pipeo, pipee):
148 152 """Clean up pipes used by an SSH connection."""
149 153 if pipeo:
150 154 pipeo.close()
151 155 if pipei:
152 156 pipei.close()
153 157
154 158 if pipee:
155 159 # Try to read from the err descriptor until EOF.
156 160 try:
157 161 for l in pipee:
158 162 ui.status(_(b'remote: '), l)
159 163 except (IOError, ValueError):
160 164 pass
161 165
162 166 pipee.close()
163 167
164 168
165 169 def _makeconnection(ui, sshcmd, args, remotecmd, path, sshenv=None):
166 170 """Create an SSH connection to a server.
167 171
168 172 Returns a tuple of (process, stdin, stdout, stderr) for the
169 173 spawned process.
170 174 """
171 175 cmd = b'%s %s %s' % (
172 176 sshcmd,
173 177 args,
174 178 procutil.shellquote(
175 179 b'%s -R %s serve --stdio'
176 180 % (_serverquote(remotecmd), _serverquote(path))
177 181 ),
178 182 )
179 183
180 184 ui.debug(b'running %s\n' % cmd)
181 185
182 186 # no buffer allow the use of 'select'
183 187 # feel free to remove buffering and select usage when we ultimately
184 188 # move to threading.
185 189 stdin, stdout, stderr, proc = procutil.popen4(cmd, bufsize=0, env=sshenv)
186 190
187 191 return proc, stdin, stdout, stderr
188 192
189 193
190 194 def _clientcapabilities():
191 195 """Return list of capabilities of this client.
192 196
193 197 Returns a list of capabilities that are supported by this client.
194 198 """
195 199 protoparams = {b'partial-pull'}
196 200 comps = [
197 201 e.wireprotosupport().name
198 202 for e in util.compengines.supportedwireengines(util.CLIENTROLE)
199 203 ]
200 204 protoparams.add(b'comp=%s' % b','.join(comps))
201 205 return protoparams
202 206
203 207
204 208 def _performhandshake(ui, stdin, stdout, stderr):
205 209 def badresponse():
206 210 # Flush any output on stderr. In general, the stderr contains errors
207 211 # from the remote (ssh errors, some hg errors), and status indications
208 212 # (like "adding changes"), with no current way to tell them apart.
209 213 # Here we failed so early that it's almost certainly only errors, so
210 214 # use warn=True so -q doesn't hide them.
211 215 _forwardoutput(ui, stderr, warn=True)
212 216
213 217 msg = _(b'no suitable response from remote hg')
214 218 hint = ui.config(b'ui', b'ssherrorhint')
215 219 raise error.RepoError(msg, hint=hint)
216 220
217 221 # The handshake consists of sending wire protocol commands in reverse
218 222 # order of protocol implementation and then sniffing for a response
219 223 # to one of them.
220 224 #
221 225 # Those commands (from oldest to newest) are:
222 226 #
223 227 # ``between``
224 228 # Asks for the set of revisions between a pair of revisions. Command
225 229 # present in all Mercurial server implementations.
226 230 #
227 231 # ``hello``
228 232 # Instructs the server to advertise its capabilities. Introduced in
229 233 # Mercurial 0.9.1.
230 234 #
231 235 # ``upgrade``
232 236 # Requests upgrade from default transport protocol version 1 to
233 237 # a newer version. Introduced in Mercurial 4.6 as an experimental
234 238 # feature.
235 239 #
236 240 # The ``between`` command is issued with a request for the null
237 241 # range. If the remote is a Mercurial server, this request will
238 242 # generate a specific response: ``1\n\n``. This represents the
239 243 # wire protocol encoded value for ``\n``. We look for ``1\n\n``
240 244 # in the output stream and know this is the response to ``between``
241 245 # and we're at the end of our handshake reply.
242 246 #
243 247 # The response to the ``hello`` command will be a line with the
244 248 # length of the value returned by that command followed by that
245 249 # value. If the server doesn't support ``hello`` (which should be
246 250 # rare), that line will be ``0\n``. Otherwise, the value will contain
247 251 # RFC 822 like lines. Of these, the ``capabilities:`` line contains
248 252 # the capabilities of the server.
249 253 #
250 254 # The ``upgrade`` command isn't really a command in the traditional
251 255 # sense of version 1 of the transport because it isn't using the
252 256 # proper mechanism for formatting insteads: instead, it just encodes
253 257 # arguments on the line, delimited by spaces.
254 258 #
255 259 # The ``upgrade`` line looks like ``upgrade <token> <capabilities>``.
256 260 # If the server doesn't support protocol upgrades, it will reply to
257 261 # this line with ``0\n``. Otherwise, it emits an
258 262 # ``upgraded <token> <protocol>`` line to both stdout and stderr.
259 263 # Content immediately following this line describes additional
260 264 # protocol and server state.
261 265 #
262 266 # In addition to the responses to our command requests, the server
263 267 # may emit "banner" output on stdout. SSH servers are allowed to
264 268 # print messages to stdout on login. Issuing commands on connection
265 269 # allows us to flush this banner output from the server by scanning
266 270 # for output to our well-known ``between`` command. Of course, if
267 271 # the banner contains ``1\n\n``, this will throw off our detection.
268 272
269 273 requestlog = ui.configbool(b'devel', b'debug.peer-request')
270 274
271 275 # Generate a random token to help identify responses to version 2
272 276 # upgrade request.
273 277 token = pycompat.sysbytes(str(uuid.uuid4()))
274 278 upgradecaps = [
275 279 (b'proto', wireprotoserver.SSHV2),
276 280 ]
277 281 upgradecaps = util.urlreq.urlencode(upgradecaps)
278 282
279 283 try:
280 284 pairsarg = b'%s-%s' % (b'0' * 40, b'0' * 40)
281 285 handshake = [
282 286 b'hello\n',
283 287 b'between\n',
284 288 b'pairs %d\n' % len(pairsarg),
285 289 pairsarg,
286 290 ]
287 291
288 292 # Request upgrade to version 2 if configured.
289 293 if ui.configbool(b'experimental', b'sshpeer.advertise-v2'):
290 294 ui.debug(b'sending upgrade request: %s %s\n' % (token, upgradecaps))
291 295 handshake.insert(0, b'upgrade %s %s\n' % (token, upgradecaps))
292 296
293 297 if requestlog:
294 298 ui.debug(b'devel-peer-request: hello+between\n')
295 299 ui.debug(b'devel-peer-request: pairs: %d bytes\n' % len(pairsarg))
296 300 ui.debug(b'sending hello command\n')
297 301 ui.debug(b'sending between command\n')
298 302
299 303 stdin.write(b''.join(handshake))
300 304 stdin.flush()
301 305 except IOError:
302 306 badresponse()
303 307
304 308 # Assume version 1 of wire protocol by default.
305 309 protoname = wireprototypes.SSHV1
306 310 reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token))
307 311
308 312 lines = [b'', b'dummy']
309 313 max_noise = 500
310 314 while lines[-1] and max_noise:
311 315 try:
312 316 l = stdout.readline()
313 317 _forwardoutput(ui, stderr, warn=True)
314 318
315 319 # Look for reply to protocol upgrade request. It has a token
316 320 # in it, so there should be no false positives.
317 321 m = reupgraded.match(l)
318 322 if m:
319 323 protoname = m.group(1)
320 324 ui.debug(b'protocol upgraded to %s\n' % protoname)
321 325 # If an upgrade was handled, the ``hello`` and ``between``
322 326 # requests are ignored. The next output belongs to the
323 327 # protocol, so stop scanning lines.
324 328 break
325 329
326 330 # Otherwise it could be a banner, ``0\n`` response if server
327 331 # doesn't support upgrade.
328 332
329 333 if lines[-1] == b'1\n' and l == b'\n':
330 334 break
331 335 if l:
332 336 ui.debug(b'remote: ', l)
333 337 lines.append(l)
334 338 max_noise -= 1
335 339 except IOError:
336 340 badresponse()
337 341 else:
338 342 badresponse()
339 343
340 344 caps = set()
341 345
342 346 # For version 1, we should see a ``capabilities`` line in response to the
343 347 # ``hello`` command.
344 348 if protoname == wireprototypes.SSHV1:
345 349 for l in reversed(lines):
346 350 # Look for response to ``hello`` command. Scan from the back so
347 351 # we don't misinterpret banner output as the command reply.
348 352 if l.startswith(b'capabilities:'):
349 353 caps.update(l[:-1].split(b':')[1].split())
350 354 break
351 355 elif protoname == wireprotoserver.SSHV2:
352 356 # We see a line with number of bytes to follow and then a value
353 357 # looking like ``capabilities: *``.
354 358 line = stdout.readline()
355 359 try:
356 360 valuelen = int(line)
357 361 except ValueError:
358 362 badresponse()
359 363
360 364 capsline = stdout.read(valuelen)
361 365 if not capsline.startswith(b'capabilities: '):
362 366 badresponse()
363 367
364 368 ui.debug(b'remote: %s\n' % capsline)
365 369
366 370 caps.update(capsline.split(b':')[1].split())
367 371 # Trailing newline.
368 372 stdout.read(1)
369 373
370 374 # Error if we couldn't find capabilities, this means:
371 375 #
372 376 # 1. Remote isn't a Mercurial server
373 377 # 2. Remote is a <0.9.1 Mercurial server
374 378 # 3. Remote is a future Mercurial server that dropped ``hello``
375 379 # and other attempted handshake mechanisms.
376 380 if not caps:
377 381 badresponse()
378 382
379 383 # Flush any output on stderr before proceeding.
380 384 _forwardoutput(ui, stderr, warn=True)
381 385
382 386 return protoname, caps
383 387
384 388
385 389 class sshv1peer(wireprotov1peer.wirepeer):
386 390 def __init__(
387 391 self, ui, url, proc, stdin, stdout, stderr, caps, autoreadstderr=True
388 392 ):
389 393 """Create a peer from an existing SSH connection.
390 394
391 395 ``proc`` is a handle on the underlying SSH process.
392 396 ``stdin``, ``stdout``, and ``stderr`` are handles on the stdio
393 397 pipes for that process.
394 398 ``caps`` is a set of capabilities supported by the remote.
395 399 ``autoreadstderr`` denotes whether to automatically read from
396 400 stderr and to forward its output.
397 401 """
398 402 self._url = url
399 403 self.ui = ui
400 404 # self._subprocess is unused. Keeping a handle on the process
401 405 # holds a reference and prevents it from being garbage collected.
402 406 self._subprocess = proc
403 407
404 408 # And we hook up our "doublepipe" wrapper to allow querying
405 409 # stderr any time we perform I/O.
406 410 if autoreadstderr:
407 411 stdout = doublepipe(ui, util.bufferedinputpipe(stdout), stderr)
408 412 stdin = doublepipe(ui, stdin, stderr)
409 413
410 414 self._pipeo = stdin
411 415 self._pipei = stdout
412 416 self._pipee = stderr
413 417 self._caps = caps
414 418 self._autoreadstderr = autoreadstderr
415 419
416 420 # Commands that have a "framed" response where the first line of the
417 421 # response contains the length of that response.
418 422 _FRAMED_COMMANDS = {
419 423 b'batch',
420 424 }
421 425
422 426 # Begin of ipeerconnection interface.
423 427
424 428 def url(self):
425 429 return self._url
426 430
427 431 def local(self):
428 432 return None
429 433
430 434 def peer(self):
431 435 return self
432 436
433 437 def canpush(self):
434 438 return True
435 439
436 440 def close(self):
437 441 self._cleanup()
438 442
439 443 # End of ipeerconnection interface.
440 444
441 445 # Begin of ipeercommands interface.
442 446
443 447 def capabilities(self):
444 448 return self._caps
445 449
446 450 # End of ipeercommands interface.
447 451
448 452 def _readerr(self):
449 453 _forwardoutput(self.ui, self._pipee)
450 454
451 455 def _abort(self, exception):
452 456 self._cleanup()
453 457 raise exception
454 458
455 459 def _cleanup(self):
456 460 _cleanuppipes(self.ui, self._pipei, self._pipeo, self._pipee)
457 461
458 462 __del__ = _cleanup
459 463
460 464 def _sendrequest(self, cmd, args, framed=False):
461 465 if self.ui.debugflag and self.ui.configbool(
462 466 b'devel', b'debug.peer-request'
463 467 ):
464 468 dbg = self.ui.debug
465 469 line = b'devel-peer-request: %s\n'
466 470 dbg(line % cmd)
467 471 for key, value in sorted(args.items()):
468 472 if not isinstance(value, dict):
469 473 dbg(line % b' %s: %d bytes' % (key, len(value)))
470 474 else:
471 475 for dk, dv in sorted(value.items()):
472 476 dbg(line % b' %s-%s: %d' % (key, dk, len(dv)))
473 477 self.ui.debug(b"sending %s command\n" % cmd)
474 478 self._pipeo.write(b"%s\n" % cmd)
475 479 _func, names = wireprotov1server.commands[cmd]
476 480 keys = names.split()
477 481 wireargs = {}
478 482 for k in keys:
479 483 if k == b'*':
480 484 wireargs[b'*'] = args
481 485 break
482 486 else:
483 487 wireargs[k] = args[k]
484 488 del args[k]
485 489 for k, v in sorted(pycompat.iteritems(wireargs)):
486 490 self._pipeo.write(b"%s %d\n" % (k, len(v)))
487 491 if isinstance(v, dict):
488 492 for dk, dv in pycompat.iteritems(v):
489 493 self._pipeo.write(b"%s %d\n" % (dk, len(dv)))
490 494 self._pipeo.write(dv)
491 495 else:
492 496 self._pipeo.write(v)
493 497 self._pipeo.flush()
494 498
495 499 # We know exactly how many bytes are in the response. So return a proxy
496 500 # around the raw output stream that allows reading exactly this many
497 501 # bytes. Callers then can read() without fear of overrunning the
498 502 # response.
499 503 if framed:
500 504 amount = self._getamount()
501 505 return util.cappedreader(self._pipei, amount)
502 506
503 507 return self._pipei
504 508
505 509 def _callstream(self, cmd, **args):
506 510 args = pycompat.byteskwargs(args)
507 511 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
508 512
509 513 def _callcompressable(self, cmd, **args):
510 514 args = pycompat.byteskwargs(args)
511 515 return self._sendrequest(cmd, args, framed=cmd in self._FRAMED_COMMANDS)
512 516
513 517 def _call(self, cmd, **args):
514 518 args = pycompat.byteskwargs(args)
515 519 return self._sendrequest(cmd, args, framed=True).read()
516 520
517 521 def _callpush(self, cmd, fp, **args):
518 522 # The server responds with an empty frame if the client should
519 523 # continue submitting the payload.
520 524 r = self._call(cmd, **args)
521 525 if r:
522 526 return b'', r
523 527
524 528 # The payload consists of frames with content followed by an empty
525 529 # frame.
526 530 for d in iter(lambda: fp.read(4096), b''):
527 531 self._writeframed(d)
528 532 self._writeframed(b"", flush=True)
529 533
530 534 # In case of success, there is an empty frame and a frame containing
531 535 # the integer result (as a string).
532 536 # In case of error, there is a non-empty frame containing the error.
533 537 r = self._readframed()
534 538 if r:
535 539 return b'', r
536 540 return self._readframed(), b''
537 541
538 542 def _calltwowaystream(self, cmd, fp, **args):
539 543 # The server responds with an empty frame if the client should
540 544 # continue submitting the payload.
541 545 r = self._call(cmd, **args)
542 546 if r:
543 547 # XXX needs to be made better
544 548 raise error.Abort(_(b'unexpected remote reply: %s') % r)
545 549
546 550 # The payload consists of frames with content followed by an empty
547 551 # frame.
548 552 for d in iter(lambda: fp.read(4096), b''):
549 553 self._writeframed(d)
550 554 self._writeframed(b"", flush=True)
551 555
552 556 return self._pipei
553 557
554 558 def _getamount(self):
555 559 l = self._pipei.readline()
556 560 if l == b'\n':
557 561 if self._autoreadstderr:
558 562 self._readerr()
559 563 msg = _(b'check previous remote output')
560 564 self._abort(error.OutOfBandError(hint=msg))
561 565 if self._autoreadstderr:
562 566 self._readerr()
563 567 try:
564 568 return int(l)
565 569 except ValueError:
566 570 self._abort(error.ResponseError(_(b"unexpected response:"), l))
567 571
568 572 def _readframed(self):
569 573 size = self._getamount()
570 574 if not size:
571 575 return b''
572 576
573 577 return self._pipei.read(size)
574 578
575 579 def _writeframed(self, data, flush=False):
576 580 self._pipeo.write(b"%d\n" % len(data))
577 581 if data:
578 582 self._pipeo.write(data)
579 583 if flush:
580 584 self._pipeo.flush()
581 585 if self._autoreadstderr:
582 586 self._readerr()
583 587
584 588
585 589 class sshv2peer(sshv1peer):
586 590 """A peer that speakers version 2 of the transport protocol."""
587 591
588 592 # Currently version 2 is identical to version 1 post handshake.
589 593 # And handshake is performed before the peer is instantiated. So
590 594 # we need no custom code.
591 595
592 596
593 597 def makepeer(ui, path, proc, stdin, stdout, stderr, autoreadstderr=True):
594 598 """Make a peer instance from existing pipes.
595 599
596 600 ``path`` and ``proc`` are stored on the eventual peer instance and may
597 601 not be used for anything meaningful.
598 602
599 603 ``stdin``, ``stdout``, and ``stderr`` are the pipes connected to the
600 604 SSH server's stdio handles.
601 605
602 606 This function is factored out to allow creating peers that don't
603 607 actually spawn a new process. It is useful for starting SSH protocol
604 608 servers and clients via non-standard means, which can be useful for
605 609 testing.
606 610 """
607 611 try:
608 612 protoname, caps = _performhandshake(ui, stdin, stdout, stderr)
609 613 except Exception:
610 614 _cleanuppipes(ui, stdout, stdin, stderr)
611 615 raise
612 616
613 617 if protoname == wireprototypes.SSHV1:
614 618 return sshv1peer(
615 619 ui,
616 620 path,
617 621 proc,
618 622 stdin,
619 623 stdout,
620 624 stderr,
621 625 caps,
622 626 autoreadstderr=autoreadstderr,
623 627 )
624 628 elif protoname == wireprototypes.SSHV2:
625 629 return sshv2peer(
626 630 ui,
627 631 path,
628 632 proc,
629 633 stdin,
630 634 stdout,
631 635 stderr,
632 636 caps,
633 637 autoreadstderr=autoreadstderr,
634 638 )
635 639 else:
636 640 _cleanuppipes(ui, stdout, stdin, stderr)
637 641 raise error.RepoError(
638 642 _(b'unknown version of SSH protocol: %s') % protoname
639 643 )
640 644
641 645
642 646 def instance(ui, path, create, intents=None, createopts=None):
643 647 """Create an SSH peer.
644 648
645 649 The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
646 650 """
647 651 u = util.url(path, parsequery=False, parsefragment=False)
648 652 if u.scheme != b'ssh' or not u.host or u.path is None:
649 653 raise error.RepoError(_(b"couldn't parse location %s") % path)
650 654
651 655 util.checksafessh(path)
652 656
653 657 if u.passwd is not None:
654 658 raise error.RepoError(_(b'password in URL not supported'))
655 659
656 660 sshcmd = ui.config(b'ui', b'ssh')
657 661 remotecmd = ui.config(b'ui', b'remotecmd')
658 662 sshaddenv = dict(ui.configitems(b'sshenv'))
659 663 sshenv = procutil.shellenviron(sshaddenv)
660 664 remotepath = u.path or b'.'
661 665
662 666 args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
663 667
664 668 if create:
665 669 # We /could/ do this, but only if the remote init command knows how to
666 670 # handle them. We don't yet make any assumptions about that. And without
667 671 # querying the remote, there's no way of knowing if the remote even
668 672 # supports said requested feature.
669 673 if createopts:
670 674 raise error.RepoError(
671 675 _(
672 676 b'cannot create remote SSH repositories '
673 677 b'with extra options'
674 678 )
675 679 )
676 680
677 681 cmd = b'%s %s %s' % (
678 682 sshcmd,
679 683 args,
680 684 procutil.shellquote(
681 685 b'%s init %s'
682 686 % (_serverquote(remotecmd), _serverquote(remotepath))
683 687 ),
684 688 )
685 689 ui.debug(b'running %s\n' % cmd)
686 690 res = ui.system(cmd, blockedtag=b'sshpeer', environ=sshenv)
687 691 if res != 0:
688 692 raise error.RepoError(_(b'could not create remote repo'))
689 693
690 694 proc, stdin, stdout, stderr = _makeconnection(
691 695 ui, sshcmd, args, remotecmd, remotepath, sshenv
692 696 )
693 697
694 698 peer = makepeer(ui, path, proc, stdin, stdout, stderr)
695 699
696 700 # Finally, if supported by the server, notify it about our own
697 701 # capabilities.
698 702 if b'protocaps' in peer.capabilities():
699 703 try:
700 704 peer._call(
701 705 b"protocaps", caps=b' '.join(sorted(_clientcapabilities()))
702 706 )
703 707 except IOError:
704 708 peer._cleanup()
705 709 raise error.RepoError(_(b'capability exchange failed'))
706 710
707 711 return peer
@@ -1,283 +1,287 b''
1 1 # Test that certain objects conform to well-defined interfaces.
2 2
3 3 from __future__ import absolute_import, print_function
4 4
5 5 from mercurial import encoding
6 6
7 7 encoding.environ[b'HGREALINTERFACES'] = b'1'
8 8
9 9 import os
10 10 import subprocess
11 11 import sys
12 12
13 13 # Only run if tests are run in a repo
14 14 if subprocess.call(
15 15 [sys.executable, '%s/hghave' % os.environ['TESTDIR'], 'test-repo']
16 16 ):
17 17 sys.exit(80)
18 18
19 19 from mercurial.interfaces import (
20 20 dirstate as intdirstate,
21 21 repository,
22 22 )
23 23 from mercurial.thirdparty.zope import interface as zi
24 24 from mercurial.thirdparty.zope.interface import verify as ziverify
25 25 from mercurial import (
26 26 bundlerepo,
27 27 dirstate,
28 28 filelog,
29 29 httppeer,
30 30 localrepo,
31 31 manifest,
32 32 pycompat,
33 33 revlog,
34 34 sshpeer,
35 35 statichttprepo,
36 36 ui as uimod,
37 37 unionrepo,
38 38 vfs as vfsmod,
39 39 wireprotoserver,
40 40 wireprototypes,
41 41 wireprotov1peer,
42 42 wireprotov2server,
43 43 )
44 44
45 45 testdir = os.path.dirname(__file__)
46 46 rootdir = pycompat.fsencode(os.path.normpath(os.path.join(testdir, '..')))
47 47
48 48 sys.path[0:0] = [testdir]
49 49 import simplestorerepo
50 50
51 51 del sys.path[0]
52 52
53 53
54 54 def checkzobject(o, allowextra=False):
55 55 """Verify an object with a zope interface."""
56 56 ifaces = zi.providedBy(o)
57 57 if not ifaces:
58 58 print('%r does not provide any zope interfaces' % o)
59 59 return
60 60
61 61 # Run zope.interface's built-in verification routine. This verifies that
62 62 # everything that is supposed to be present is present.
63 63 for iface in ifaces:
64 64 ziverify.verifyObject(iface, o)
65 65
66 66 if allowextra:
67 67 return
68 68
69 69 # Now verify that the object provides no extra public attributes that
70 70 # aren't declared as part of interfaces.
71 71 allowed = set()
72 72 for iface in ifaces:
73 73 allowed |= set(iface.names(all=True))
74 74
75 75 public = {a for a in dir(o) if not a.startswith('_')}
76 76
77 77 for attr in sorted(public - allowed):
78 78 print(
79 79 'public attribute not declared in interfaces: %s.%s'
80 80 % (o.__class__.__name__, attr)
81 81 )
82 82
83 83
84 84 # Facilitates testing localpeer.
85 85 class dummyrepo(object):
86 86 def __init__(self):
87 87 self.ui = uimod.ui()
88 88
89 89 def filtered(self, name):
90 90 pass
91 91
92 92 def _restrictcapabilities(self, caps):
93 93 pass
94 94
95 95
96 96 class dummyopener(object):
97 97 handlers = []
98 98
99 99
100 100 # Facilitates testing sshpeer without requiring a server.
101 101 class badpeer(httppeer.httppeer):
102 102 def __init__(self):
103 103 super(badpeer, self).__init__(
104 104 None, None, None, dummyopener(), None, None
105 105 )
106 106 self.badattribute = True
107 107
108 108 def badmethod(self):
109 109 pass
110 110
111 111
112 112 class dummypipe(object):
113 113 def close(self):
114 114 pass
115 115
116 @property
117 def closed(self):
118 pass
119
116 120
117 121 def main():
118 122 ui = uimod.ui()
119 123 # Needed so we can open a local repo with obsstore without a warning.
120 124 ui.setconfig(b'experimental', b'evolution.createmarkers', True)
121 125
122 126 checkzobject(badpeer())
123 127
124 128 ziverify.verifyClass(repository.ipeerbase, httppeer.httppeer)
125 129 checkzobject(httppeer.httppeer(None, None, None, dummyopener(), None, None))
126 130
127 131 ziverify.verifyClass(repository.ipeerv2, httppeer.httpv2peer)
128 132 checkzobject(httppeer.httpv2peer(None, b'', b'', None, None, None))
129 133
130 134 ziverify.verifyClass(repository.ipeerbase, localrepo.localpeer)
131 135 checkzobject(localrepo.localpeer(dummyrepo()))
132 136
133 137 ziverify.verifyClass(
134 138 repository.ipeercommandexecutor, localrepo.localcommandexecutor
135 139 )
136 140 checkzobject(localrepo.localcommandexecutor(None))
137 141
138 142 ziverify.verifyClass(
139 143 repository.ipeercommandexecutor, wireprotov1peer.peerexecutor
140 144 )
141 145 checkzobject(wireprotov1peer.peerexecutor(None))
142 146
143 147 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv1peer)
144 148 checkzobject(
145 149 sshpeer.sshv1peer(
146 150 ui,
147 151 b'ssh://localhost/foo',
148 152 b'',
149 153 dummypipe(),
150 154 dummypipe(),
151 155 None,
152 156 None,
153 157 )
154 158 )
155 159
156 160 ziverify.verifyClass(repository.ipeerbase, sshpeer.sshv2peer)
157 161 checkzobject(
158 162 sshpeer.sshv2peer(
159 163 ui,
160 164 b'ssh://localhost/foo',
161 165 b'',
162 166 dummypipe(),
163 167 dummypipe(),
164 168 None,
165 169 None,
166 170 )
167 171 )
168 172
169 173 ziverify.verifyClass(repository.ipeerbase, bundlerepo.bundlepeer)
170 174 checkzobject(bundlerepo.bundlepeer(dummyrepo()))
171 175
172 176 ziverify.verifyClass(repository.ipeerbase, statichttprepo.statichttppeer)
173 177 checkzobject(statichttprepo.statichttppeer(dummyrepo()))
174 178
175 179 ziverify.verifyClass(repository.ipeerbase, unionrepo.unionpeer)
176 180 checkzobject(unionrepo.unionpeer(dummyrepo()))
177 181
178 182 ziverify.verifyClass(
179 183 repository.ilocalrepositorymain, localrepo.localrepository
180 184 )
181 185 ziverify.verifyClass(
182 186 repository.ilocalrepositoryfilestorage, localrepo.revlogfilestorage
183 187 )
184 188 repo = localrepo.makelocalrepository(ui, rootdir)
185 189 checkzobject(repo)
186 190
187 191 ziverify.verifyClass(
188 192 wireprototypes.baseprotocolhandler, wireprotoserver.sshv1protocolhandler
189 193 )
190 194 ziverify.verifyClass(
191 195 wireprototypes.baseprotocolhandler, wireprotoserver.sshv2protocolhandler
192 196 )
193 197 ziverify.verifyClass(
194 198 wireprototypes.baseprotocolhandler,
195 199 wireprotoserver.httpv1protocolhandler,
196 200 )
197 201 ziverify.verifyClass(
198 202 wireprototypes.baseprotocolhandler,
199 203 wireprotov2server.httpv2protocolhandler,
200 204 )
201 205
202 206 sshv1 = wireprotoserver.sshv1protocolhandler(None, None, None)
203 207 checkzobject(sshv1)
204 208 sshv2 = wireprotoserver.sshv2protocolhandler(None, None, None)
205 209 checkzobject(sshv2)
206 210
207 211 httpv1 = wireprotoserver.httpv1protocolhandler(None, None, None)
208 212 checkzobject(httpv1)
209 213 httpv2 = wireprotov2server.httpv2protocolhandler(None, None)
210 214 checkzobject(httpv2)
211 215
212 216 ziverify.verifyClass(repository.ifilestorage, filelog.filelog)
213 217 ziverify.verifyClass(repository.imanifestdict, manifest.manifestdict)
214 218 ziverify.verifyClass(repository.imanifestdict, manifest.treemanifest)
215 219 ziverify.verifyClass(
216 220 repository.imanifestrevisionstored, manifest.manifestctx
217 221 )
218 222 ziverify.verifyClass(
219 223 repository.imanifestrevisionwritable, manifest.memmanifestctx
220 224 )
221 225 ziverify.verifyClass(
222 226 repository.imanifestrevisionstored, manifest.treemanifestctx
223 227 )
224 228 ziverify.verifyClass(
225 229 repository.imanifestrevisionwritable, manifest.memtreemanifestctx
226 230 )
227 231 ziverify.verifyClass(repository.imanifestlog, manifest.manifestlog)
228 232 ziverify.verifyClass(repository.imanifeststorage, manifest.manifestrevlog)
229 233
230 234 ziverify.verifyClass(
231 235 repository.irevisiondelta, simplestorerepo.simplestorerevisiondelta
232 236 )
233 237 ziverify.verifyClass(repository.ifilestorage, simplestorerepo.filestorage)
234 238 ziverify.verifyClass(
235 239 repository.iverifyproblem, simplestorerepo.simplefilestoreproblem
236 240 )
237 241
238 242 ziverify.verifyClass(intdirstate.idirstate, dirstate.dirstate)
239 243
240 244 vfs = vfsmod.vfs(b'.')
241 245 fl = filelog.filelog(vfs, b'dummy.i')
242 246 checkzobject(fl, allowextra=True)
243 247
244 248 # Conforms to imanifestlog.
245 249 ml = manifest.manifestlog(
246 250 vfs, repo, manifest.manifestrevlog(repo.svfs), repo.narrowmatch()
247 251 )
248 252 checkzobject(ml)
249 253 checkzobject(repo.manifestlog)
250 254
251 255 # Conforms to imanifestrevision.
252 256 mctx = ml[repo[0].manifestnode()]
253 257 checkzobject(mctx)
254 258
255 259 # Conforms to imanifestrevisionwritable.
256 260 checkzobject(mctx.copy())
257 261
258 262 # Conforms to imanifestdict.
259 263 checkzobject(mctx.read())
260 264
261 265 mrl = manifest.manifestrevlog(vfs)
262 266 checkzobject(mrl)
263 267
264 268 ziverify.verifyClass(repository.irevisiondelta, revlog.revlogrevisiondelta)
265 269
266 270 rd = revlog.revlogrevisiondelta(
267 271 node=b'',
268 272 p1node=b'',
269 273 p2node=b'',
270 274 basenode=b'',
271 275 linknode=b'',
272 276 flags=b'',
273 277 baserevisionsize=None,
274 278 revision=b'',
275 279 delta=None,
276 280 )
277 281 checkzobject(rd)
278 282
279 283 ziverify.verifyClass(repository.iverifyproblem, revlog.revlogproblem)
280 284 checkzobject(revlog.revlogproblem())
281 285
282 286
283 287 main()
General Comments 0
You need to be logged in to leave comments. Login now