mercurial/sshpeer.py
changeset 43077 687b865b95ad
parent 43076 2372284d9457
child 43089 c59eb1560c44
equal deleted inserted replaced
43076:2372284d9457 43077:687b865b95ad
    28 
    28 
    29 def _serverquote(s):
    29 def _serverquote(s):
    30     """quote a string for the remote shell ... which we assume is sh"""
    30     """quote a string for the remote shell ... which we assume is sh"""
    31     if not s:
    31     if not s:
    32         return s
    32         return s
    33     if re.match('[a-zA-Z0-9@%_+=:,./-]*$', s):
    33     if re.match(b'[a-zA-Z0-9@%_+=:,./-]*$', s):
    34         return s
    34         return s
    35     return "'%s'" % s.replace("'", "'\\''")
    35     return b"'%s'" % s.replace(b"'", b"'\\''")
    36 
    36 
    37 
    37 
    38 def _forwardoutput(ui, pipe):
    38 def _forwardoutput(ui, pipe):
    39     """display all data currently available on pipe as remote output.
    39     """display all data currently available on pipe as remote output.
    40 
    40 
    41     This is non blocking."""
    41     This is non blocking."""
    42     if pipe:
    42     if pipe:
    43         s = procutil.readpipe(pipe)
    43         s = procutil.readpipe(pipe)
    44         if s:
    44         if s:
    45             for l in s.splitlines():
    45             for l in s.splitlines():
    46                 ui.status(_("remote: "), l, '\n')
    46                 ui.status(_(b"remote: "), l, b'\n')
    47 
    47 
    48 
    48 
    49 class doublepipe(object):
    49 class doublepipe(object):
    50     """Operate a side-channel pipe in addition of a main one
    50     """Operate a side-channel pipe in addition of a main one
    51 
    51 
    89             # non supported yet case, assume all have data.
    89             # non supported yet case, assume all have data.
    90             act = fds
    90             act = fds
    91         return (self._main.fileno() in act, self._side.fileno() in act)
    91         return (self._main.fileno() in act, self._side.fileno() in act)
    92 
    92 
    93     def write(self, data):
    93     def write(self, data):
    94         return self._call('write', data)
    94         return self._call(b'write', data)
    95 
    95 
    96     def read(self, size):
    96     def read(self, size):
    97         r = self._call('read', size)
    97         r = self._call(b'read', size)
    98         if size != 0 and not r:
    98         if size != 0 and not r:
    99             # We've observed a condition that indicates the
    99             # We've observed a condition that indicates the
   100             # stdout closed unexpectedly. Check stderr one
   100             # stdout closed unexpectedly. Check stderr one
   101             # more time and snag anything that's there before
   101             # more time and snag anything that's there before
   102             # letting anyone know the main part of the pipe
   102             # letting anyone know the main part of the pipe
   103             # closed prematurely.
   103             # closed prematurely.
   104             _forwardoutput(self._ui, self._side)
   104             _forwardoutput(self._ui, self._side)
   105         return r
   105         return r
   106 
   106 
   107     def unbufferedread(self, size):
   107     def unbufferedread(self, size):
   108         r = self._call('unbufferedread', size)
   108         r = self._call(b'unbufferedread', size)
   109         if size != 0 and not r:
   109         if size != 0 and not r:
   110             # We've observed a condition that indicates the
   110             # We've observed a condition that indicates the
   111             # stdout closed unexpectedly. Check stderr one
   111             # stdout closed unexpectedly. Check stderr one
   112             # more time and snag anything that's there before
   112             # more time and snag anything that's there before
   113             # letting anyone know the main part of the pipe
   113             # letting anyone know the main part of the pipe
   114             # closed prematurely.
   114             # closed prematurely.
   115             _forwardoutput(self._ui, self._side)
   115             _forwardoutput(self._ui, self._side)
   116         return r
   116         return r
   117 
   117 
   118     def readline(self):
   118     def readline(self):
   119         return self._call('readline')
   119         return self._call(b'readline')
   120 
   120 
   121     def _call(self, methname, data=None):
   121     def _call(self, methname, data=None):
   122         """call <methname> on "main", forward output of "side" while blocking
   122         """call <methname> on "main", forward output of "side" while blocking
   123         """
   123         """
   124         # data can be '' or 0
   124         # data can be '' or 0
   125         if (data is not None and not data) or self._main.closed:
   125         if (data is not None and not data) or self._main.closed:
   126             _forwardoutput(self._ui, self._side)
   126             _forwardoutput(self._ui, self._side)
   127             return ''
   127             return b''
   128         while True:
   128         while True:
   129             mainready, sideready = self._wait()
   129             mainready, sideready = self._wait()
   130             if sideready:
   130             if sideready:
   131                 _forwardoutput(self._ui, self._side)
   131                 _forwardoutput(self._ui, self._side)
   132             if mainready:
   132             if mainready:
   152 
   152 
   153     if pipee:
   153     if pipee:
   154         # Try to read from the err descriptor until EOF.
   154         # Try to read from the err descriptor until EOF.
   155         try:
   155         try:
   156             for l in pipee:
   156             for l in pipee:
   157                 ui.status(_('remote: '), l)
   157                 ui.status(_(b'remote: '), l)
   158         except (IOError, ValueError):
   158         except (IOError, ValueError):
   159             pass
   159             pass
   160 
   160 
   161         pipee.close()
   161         pipee.close()
   162 
   162 
   165     """Create an SSH connection to a server.
   165     """Create an SSH connection to a server.
   166 
   166 
   167     Returns a tuple of (process, stdin, stdout, stderr) for the
   167     Returns a tuple of (process, stdin, stdout, stderr) for the
   168     spawned process.
   168     spawned process.
   169     """
   169     """
   170     cmd = '%s %s %s' % (
   170     cmd = b'%s %s %s' % (
   171         sshcmd,
   171         sshcmd,
   172         args,
   172         args,
   173         procutil.shellquote(
   173         procutil.shellquote(
   174             '%s -R %s serve --stdio'
   174             b'%s -R %s serve --stdio'
   175             % (_serverquote(remotecmd), _serverquote(path))
   175             % (_serverquote(remotecmd), _serverquote(path))
   176         ),
   176         ),
   177     )
   177     )
   178 
   178 
   179     ui.debug('running %s\n' % cmd)
   179     ui.debug(b'running %s\n' % cmd)
   180     cmd = procutil.quotecommand(cmd)
   180     cmd = procutil.quotecommand(cmd)
   181 
   181 
   182     # no buffer allow the use of 'select'
   182     # no buffer allow the use of 'select'
   183     # feel free to remove buffering and select usage when we ultimately
   183     # feel free to remove buffering and select usage when we ultimately
   184     # move to threading.
   184     # move to threading.
   190 def _clientcapabilities():
   190 def _clientcapabilities():
   191     """Return list of capabilities of this client.
   191     """Return list of capabilities of this client.
   192 
   192 
   193     Returns a list of capabilities that are supported by this client.
   193     Returns a list of capabilities that are supported by this client.
   194     """
   194     """
   195     protoparams = {'partial-pull'}
   195     protoparams = {b'partial-pull'}
   196     comps = [
   196     comps = [
   197         e.wireprotosupport().name
   197         e.wireprotosupport().name
   198         for e in util.compengines.supportedwireengines(util.CLIENTROLE)
   198         for e in util.compengines.supportedwireengines(util.CLIENTROLE)
   199     ]
   199     ]
   200     protoparams.add('comp=%s' % ','.join(comps))
   200     protoparams.add(b'comp=%s' % b','.join(comps))
   201     return protoparams
   201     return protoparams
   202 
   202 
   203 
   203 
   204 def _performhandshake(ui, stdin, stdout, stderr):
   204 def _performhandshake(ui, stdin, stdout, stderr):
   205     def badresponse():
   205     def badresponse():
   206         # Flush any output on stderr.
   206         # Flush any output on stderr.
   207         _forwardoutput(ui, stderr)
   207         _forwardoutput(ui, stderr)
   208 
   208 
   209         msg = _('no suitable response from remote hg')
   209         msg = _(b'no suitable response from remote hg')
   210         hint = ui.config('ui', 'ssherrorhint')
   210         hint = ui.config(b'ui', b'ssherrorhint')
   211         raise error.RepoError(msg, hint=hint)
   211         raise error.RepoError(msg, hint=hint)
   212 
   212 
   213     # The handshake consists of sending wire protocol commands in reverse
   213     # The handshake consists of sending wire protocol commands in reverse
   214     # order of protocol implementation and then sniffing for a response
   214     # order of protocol implementation and then sniffing for a response
   215     # to one of them.
   215     # to one of them.
   260     # print messages to stdout on login. Issuing commands on connection
   260     # print messages to stdout on login. Issuing commands on connection
   261     # allows us to flush this banner output from the server by scanning
   261     # allows us to flush this banner output from the server by scanning
   262     # for output to our well-known ``between`` command. Of course, if
   262     # for output to our well-known ``between`` command. Of course, if
   263     # the banner contains ``1\n\n``, this will throw off our detection.
   263     # the banner contains ``1\n\n``, this will throw off our detection.
   264 
   264 
   265     requestlog = ui.configbool('devel', 'debug.peer-request')
   265     requestlog = ui.configbool(b'devel', b'debug.peer-request')
   266 
   266 
   267     # Generate a random token to help identify responses to version 2
   267     # Generate a random token to help identify responses to version 2
   268     # upgrade request.
   268     # upgrade request.
   269     token = pycompat.sysbytes(str(uuid.uuid4()))
   269     token = pycompat.sysbytes(str(uuid.uuid4()))
   270     upgradecaps = [
   270     upgradecaps = [
   271         ('proto', wireprotoserver.SSHV2),
   271         (b'proto', wireprotoserver.SSHV2),
   272     ]
   272     ]
   273     upgradecaps = util.urlreq.urlencode(upgradecaps)
   273     upgradecaps = util.urlreq.urlencode(upgradecaps)
   274 
   274 
   275     try:
   275     try:
   276         pairsarg = '%s-%s' % ('0' * 40, '0' * 40)
   276         pairsarg = b'%s-%s' % (b'0' * 40, b'0' * 40)
   277         handshake = [
   277         handshake = [
   278             'hello\n',
   278             b'hello\n',
   279             'between\n',
   279             b'between\n',
   280             'pairs %d\n' % len(pairsarg),
   280             b'pairs %d\n' % len(pairsarg),
   281             pairsarg,
   281             pairsarg,
   282         ]
   282         ]
   283 
   283 
   284         # Request upgrade to version 2 if configured.
   284         # Request upgrade to version 2 if configured.
   285         if ui.configbool('experimental', 'sshpeer.advertise-v2'):
   285         if ui.configbool(b'experimental', b'sshpeer.advertise-v2'):
   286             ui.debug('sending upgrade request: %s %s\n' % (token, upgradecaps))
   286             ui.debug(b'sending upgrade request: %s %s\n' % (token, upgradecaps))
   287             handshake.insert(0, 'upgrade %s %s\n' % (token, upgradecaps))
   287             handshake.insert(0, b'upgrade %s %s\n' % (token, upgradecaps))
   288 
   288 
   289         if requestlog:
   289         if requestlog:
   290             ui.debug('devel-peer-request: hello+between\n')
   290             ui.debug(b'devel-peer-request: hello+between\n')
   291             ui.debug('devel-peer-request:   pairs: %d bytes\n' % len(pairsarg))
   291             ui.debug(b'devel-peer-request:   pairs: %d bytes\n' % len(pairsarg))
   292         ui.debug('sending hello command\n')
   292         ui.debug(b'sending hello command\n')
   293         ui.debug('sending between command\n')
   293         ui.debug(b'sending between command\n')
   294 
   294 
   295         stdin.write(''.join(handshake))
   295         stdin.write(b''.join(handshake))
   296         stdin.flush()
   296         stdin.flush()
   297     except IOError:
   297     except IOError:
   298         badresponse()
   298         badresponse()
   299 
   299 
   300     # Assume version 1 of wire protocol by default.
   300     # Assume version 1 of wire protocol by default.
   301     protoname = wireprototypes.SSHV1
   301     protoname = wireprototypes.SSHV1
   302     reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token))
   302     reupgraded = re.compile(b'^upgraded %s (.*)$' % stringutil.reescape(token))
   303 
   303 
   304     lines = ['', 'dummy']
   304     lines = [b'', b'dummy']
   305     max_noise = 500
   305     max_noise = 500
   306     while lines[-1] and max_noise:
   306     while lines[-1] and max_noise:
   307         try:
   307         try:
   308             l = stdout.readline()
   308             l = stdout.readline()
   309             _forwardoutput(ui, stderr)
   309             _forwardoutput(ui, stderr)
   311             # Look for reply to protocol upgrade request. It has a token
   311             # Look for reply to protocol upgrade request. It has a token
   312             # in it, so there should be no false positives.
   312             # in it, so there should be no false positives.
   313             m = reupgraded.match(l)
   313             m = reupgraded.match(l)
   314             if m:
   314             if m:
   315                 protoname = m.group(1)
   315                 protoname = m.group(1)
   316                 ui.debug('protocol upgraded to %s\n' % protoname)
   316                 ui.debug(b'protocol upgraded to %s\n' % protoname)
   317                 # If an upgrade was handled, the ``hello`` and ``between``
   317                 # If an upgrade was handled, the ``hello`` and ``between``
   318                 # requests are ignored. The next output belongs to the
   318                 # requests are ignored. The next output belongs to the
   319                 # protocol, so stop scanning lines.
   319                 # protocol, so stop scanning lines.
   320                 break
   320                 break
   321 
   321 
   322             # Otherwise it could be a banner, ``0\n`` response if server
   322             # Otherwise it could be a banner, ``0\n`` response if server
   323             # doesn't support upgrade.
   323             # doesn't support upgrade.
   324 
   324 
   325             if lines[-1] == '1\n' and l == '\n':
   325             if lines[-1] == b'1\n' and l == b'\n':
   326                 break
   326                 break
   327             if l:
   327             if l:
   328                 ui.debug('remote: ', l)
   328                 ui.debug(b'remote: ', l)
   329             lines.append(l)
   329             lines.append(l)
   330             max_noise -= 1
   330             max_noise -= 1
   331         except IOError:
   331         except IOError:
   332             badresponse()
   332             badresponse()
   333     else:
   333     else:
   339     # ``hello`` command.
   339     # ``hello`` command.
   340     if protoname == wireprototypes.SSHV1:
   340     if protoname == wireprototypes.SSHV1:
   341         for l in reversed(lines):
   341         for l in reversed(lines):
   342             # Look for response to ``hello`` command. Scan from the back so
   342             # Look for response to ``hello`` command. Scan from the back so
   343             # we don't misinterpret banner output as the command reply.
   343             # we don't misinterpret banner output as the command reply.
   344             if l.startswith('capabilities:'):
   344             if l.startswith(b'capabilities:'):
   345                 caps.update(l[:-1].split(':')[1].split())
   345                 caps.update(l[:-1].split(b':')[1].split())
   346                 break
   346                 break
   347     elif protoname == wireprotoserver.SSHV2:
   347     elif protoname == wireprotoserver.SSHV2:
   348         # We see a line with number of bytes to follow and then a value
   348         # We see a line with number of bytes to follow and then a value
   349         # looking like ``capabilities: *``.
   349         # looking like ``capabilities: *``.
   350         line = stdout.readline()
   350         line = stdout.readline()
   352             valuelen = int(line)
   352             valuelen = int(line)
   353         except ValueError:
   353         except ValueError:
   354             badresponse()
   354             badresponse()
   355 
   355 
   356         capsline = stdout.read(valuelen)
   356         capsline = stdout.read(valuelen)
   357         if not capsline.startswith('capabilities: '):
   357         if not capsline.startswith(b'capabilities: '):
   358             badresponse()
   358             badresponse()
   359 
   359 
   360         ui.debug('remote: %s\n' % capsline)
   360         ui.debug(b'remote: %s\n' % capsline)
   361 
   361 
   362         caps.update(capsline.split(':')[1].split())
   362         caps.update(capsline.split(b':')[1].split())
   363         # Trailing newline.
   363         # Trailing newline.
   364         stdout.read(1)
   364         stdout.read(1)
   365 
   365 
   366     # Error if we couldn't find capabilities, this means:
   366     # Error if we couldn't find capabilities, this means:
   367     #
   367     #
   410         self._autoreadstderr = autoreadstderr
   410         self._autoreadstderr = autoreadstderr
   411 
   411 
   412     # Commands that have a "framed" response where the first line of the
   412     # Commands that have a "framed" response where the first line of the
   413     # response contains the length of that response.
   413     # response contains the length of that response.
   414     _FRAMED_COMMANDS = {
   414     _FRAMED_COMMANDS = {
   415         'batch',
   415         b'batch',
   416     }
   416     }
   417 
   417 
   418     # Begin of ipeerconnection interface.
   418     # Begin of ipeerconnection interface.
   419 
   419 
   420     def url(self):
   420     def url(self):
   453 
   453 
   454     __del__ = _cleanup
   454     __del__ = _cleanup
   455 
   455 
   456     def _sendrequest(self, cmd, args, framed=False):
   456     def _sendrequest(self, cmd, args, framed=False):
   457         if self.ui.debugflag and self.ui.configbool(
   457         if self.ui.debugflag and self.ui.configbool(
   458             'devel', 'debug.peer-request'
   458             b'devel', b'debug.peer-request'
   459         ):
   459         ):
   460             dbg = self.ui.debug
   460             dbg = self.ui.debug
   461             line = 'devel-peer-request: %s\n'
   461             line = b'devel-peer-request: %s\n'
   462             dbg(line % cmd)
   462             dbg(line % cmd)
   463             for key, value in sorted(args.items()):
   463             for key, value in sorted(args.items()):
   464                 if not isinstance(value, dict):
   464                 if not isinstance(value, dict):
   465                     dbg(line % '  %s: %d bytes' % (key, len(value)))
   465                     dbg(line % b'  %s: %d bytes' % (key, len(value)))
   466                 else:
   466                 else:
   467                     for dk, dv in sorted(value.items()):
   467                     for dk, dv in sorted(value.items()):
   468                         dbg(line % '  %s-%s: %d' % (key, dk, len(dv)))
   468                         dbg(line % b'  %s-%s: %d' % (key, dk, len(dv)))
   469         self.ui.debug("sending %s command\n" % cmd)
   469         self.ui.debug(b"sending %s command\n" % cmd)
   470         self._pipeo.write("%s\n" % cmd)
   470         self._pipeo.write(b"%s\n" % cmd)
   471         _func, names = wireprotov1server.commands[cmd]
   471         _func, names = wireprotov1server.commands[cmd]
   472         keys = names.split()
   472         keys = names.split()
   473         wireargs = {}
   473         wireargs = {}
   474         for k in keys:
   474         for k in keys:
   475             if k == '*':
   475             if k == b'*':
   476                 wireargs['*'] = args
   476                 wireargs[b'*'] = args
   477                 break
   477                 break
   478             else:
   478             else:
   479                 wireargs[k] = args[k]
   479                 wireargs[k] = args[k]
   480                 del args[k]
   480                 del args[k]
   481         for k, v in sorted(wireargs.iteritems()):
   481         for k, v in sorted(wireargs.iteritems()):
   482             self._pipeo.write("%s %d\n" % (k, len(v)))
   482             self._pipeo.write(b"%s %d\n" % (k, len(v)))
   483             if isinstance(v, dict):
   483             if isinstance(v, dict):
   484                 for dk, dv in v.iteritems():
   484                 for dk, dv in v.iteritems():
   485                     self._pipeo.write("%s %d\n" % (dk, len(dv)))
   485                     self._pipeo.write(b"%s %d\n" % (dk, len(dv)))
   486                     self._pipeo.write(dv)
   486                     self._pipeo.write(dv)
   487             else:
   487             else:
   488                 self._pipeo.write(v)
   488                 self._pipeo.write(v)
   489         self._pipeo.flush()
   489         self._pipeo.flush()
   490 
   490 
   513     def _callpush(self, cmd, fp, **args):
   513     def _callpush(self, cmd, fp, **args):
   514         # The server responds with an empty frame if the client should
   514         # The server responds with an empty frame if the client should
   515         # continue submitting the payload.
   515         # continue submitting the payload.
   516         r = self._call(cmd, **args)
   516         r = self._call(cmd, **args)
   517         if r:
   517         if r:
   518             return '', r
   518             return b'', r
   519 
   519 
   520         # The payload consists of frames with content followed by an empty
   520         # The payload consists of frames with content followed by an empty
   521         # frame.
   521         # frame.
   522         for d in iter(lambda: fp.read(4096), ''):
   522         for d in iter(lambda: fp.read(4096), b''):
   523             self._writeframed(d)
   523             self._writeframed(d)
   524         self._writeframed("", flush=True)
   524         self._writeframed(b"", flush=True)
   525 
   525 
   526         # In case of success, there is an empty frame and a frame containing
   526         # In case of success, there is an empty frame and a frame containing
   527         # the integer result (as a string).
   527         # the integer result (as a string).
   528         # In case of error, there is a non-empty frame containing the error.
   528         # In case of error, there is a non-empty frame containing the error.
   529         r = self._readframed()
   529         r = self._readframed()
   530         if r:
   530         if r:
   531             return '', r
   531             return b'', r
   532         return self._readframed(), ''
   532         return self._readframed(), b''
   533 
   533 
   534     def _calltwowaystream(self, cmd, fp, **args):
   534     def _calltwowaystream(self, cmd, fp, **args):
   535         # The server responds with an empty frame if the client should
   535         # The server responds with an empty frame if the client should
   536         # continue submitting the payload.
   536         # continue submitting the payload.
   537         r = self._call(cmd, **args)
   537         r = self._call(cmd, **args)
   538         if r:
   538         if r:
   539             # XXX needs to be made better
   539             # XXX needs to be made better
   540             raise error.Abort(_('unexpected remote reply: %s') % r)
   540             raise error.Abort(_(b'unexpected remote reply: %s') % r)
   541 
   541 
   542         # The payload consists of frames with content followed by an empty
   542         # The payload consists of frames with content followed by an empty
   543         # frame.
   543         # frame.
   544         for d in iter(lambda: fp.read(4096), ''):
   544         for d in iter(lambda: fp.read(4096), b''):
   545             self._writeframed(d)
   545             self._writeframed(d)
   546         self._writeframed("", flush=True)
   546         self._writeframed(b"", flush=True)
   547 
   547 
   548         return self._pipei
   548         return self._pipei
   549 
   549 
   550     def _getamount(self):
   550     def _getamount(self):
   551         l = self._pipei.readline()
   551         l = self._pipei.readline()
   552         if l == '\n':
   552         if l == b'\n':
   553             if self._autoreadstderr:
   553             if self._autoreadstderr:
   554                 self._readerr()
   554                 self._readerr()
   555             msg = _('check previous remote output')
   555             msg = _(b'check previous remote output')
   556             self._abort(error.OutOfBandError(hint=msg))
   556             self._abort(error.OutOfBandError(hint=msg))
   557         if self._autoreadstderr:
   557         if self._autoreadstderr:
   558             self._readerr()
   558             self._readerr()
   559         try:
   559         try:
   560             return int(l)
   560             return int(l)
   561         except ValueError:
   561         except ValueError:
   562             self._abort(error.ResponseError(_("unexpected response:"), l))
   562             self._abort(error.ResponseError(_(b"unexpected response:"), l))
   563 
   563 
   564     def _readframed(self):
   564     def _readframed(self):
   565         size = self._getamount()
   565         size = self._getamount()
   566         if not size:
   566         if not size:
   567             return b''
   567             return b''
   568 
   568 
   569         return self._pipei.read(size)
   569         return self._pipei.read(size)
   570 
   570 
   571     def _writeframed(self, data, flush=False):
   571     def _writeframed(self, data, flush=False):
   572         self._pipeo.write("%d\n" % len(data))
   572         self._pipeo.write(b"%d\n" % len(data))
   573         if data:
   573         if data:
   574             self._pipeo.write(data)
   574             self._pipeo.write(data)
   575         if flush:
   575         if flush:
   576             self._pipeo.flush()
   576             self._pipeo.flush()
   577         if self._autoreadstderr:
   577         if self._autoreadstderr:
   629             autoreadstderr=autoreadstderr,
   629             autoreadstderr=autoreadstderr,
   630         )
   630         )
   631     else:
   631     else:
   632         _cleanuppipes(ui, stdout, stdin, stderr)
   632         _cleanuppipes(ui, stdout, stdin, stderr)
   633         raise error.RepoError(
   633         raise error.RepoError(
   634             _('unknown version of SSH protocol: %s') % protoname
   634             _(b'unknown version of SSH protocol: %s') % protoname
   635         )
   635         )
   636 
   636 
   637 
   637 
   638 def instance(ui, path, create, intents=None, createopts=None):
   638 def instance(ui, path, create, intents=None, createopts=None):
   639     """Create an SSH peer.
   639     """Create an SSH peer.
   640 
   640 
   641     The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
   641     The returned object conforms to the ``wireprotov1peer.wirepeer`` interface.
   642     """
   642     """
   643     u = util.url(path, parsequery=False, parsefragment=False)
   643     u = util.url(path, parsequery=False, parsefragment=False)
   644     if u.scheme != 'ssh' or not u.host or u.path is None:
   644     if u.scheme != b'ssh' or not u.host or u.path is None:
   645         raise error.RepoError(_("couldn't parse location %s") % path)
   645         raise error.RepoError(_(b"couldn't parse location %s") % path)
   646 
   646 
   647     util.checksafessh(path)
   647     util.checksafessh(path)
   648 
   648 
   649     if u.passwd is not None:
   649     if u.passwd is not None:
   650         raise error.RepoError(_('password in URL not supported'))
   650         raise error.RepoError(_(b'password in URL not supported'))
   651 
   651 
   652     sshcmd = ui.config('ui', 'ssh')
   652     sshcmd = ui.config(b'ui', b'ssh')
   653     remotecmd = ui.config('ui', 'remotecmd')
   653     remotecmd = ui.config(b'ui', b'remotecmd')
   654     sshaddenv = dict(ui.configitems('sshenv'))
   654     sshaddenv = dict(ui.configitems(b'sshenv'))
   655     sshenv = procutil.shellenviron(sshaddenv)
   655     sshenv = procutil.shellenviron(sshaddenv)
   656     remotepath = u.path or '.'
   656     remotepath = u.path or b'.'
   657 
   657 
   658     args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
   658     args = procutil.sshargs(sshcmd, u.host, u.user, u.port)
   659 
   659 
   660     if create:
   660     if create:
   661         # We /could/ do this, but only if the remote init command knows how to
   661         # We /could/ do this, but only if the remote init command knows how to
   662         # handle them. We don't yet make any assumptions about that. And without
   662         # handle them. We don't yet make any assumptions about that. And without
   663         # querying the remote, there's no way of knowing if the remote even
   663         # querying the remote, there's no way of knowing if the remote even
   664         # supports said requested feature.
   664         # supports said requested feature.
   665         if createopts:
   665         if createopts:
   666             raise error.RepoError(
   666             raise error.RepoError(
   667                 _('cannot create remote SSH repositories ' 'with extra options')
   667                 _(
       
   668                     b'cannot create remote SSH repositories '
       
   669                     b'with extra options'
       
   670                 )
   668             )
   671             )
   669 
   672 
   670         cmd = '%s %s %s' % (
   673         cmd = b'%s %s %s' % (
   671             sshcmd,
   674             sshcmd,
   672             args,
   675             args,
   673             procutil.shellquote(
   676             procutil.shellquote(
   674                 '%s init %s'
   677                 b'%s init %s'
   675                 % (_serverquote(remotecmd), _serverquote(remotepath))
   678                 % (_serverquote(remotecmd), _serverquote(remotepath))
   676             ),
   679             ),
   677         )
   680         )
   678         ui.debug('running %s\n' % cmd)
   681         ui.debug(b'running %s\n' % cmd)
   679         res = ui.system(cmd, blockedtag='sshpeer', environ=sshenv)
   682         res = ui.system(cmd, blockedtag=b'sshpeer', environ=sshenv)
   680         if res != 0:
   683         if res != 0:
   681             raise error.RepoError(_('could not create remote repo'))
   684             raise error.RepoError(_(b'could not create remote repo'))
   682 
   685 
   683     proc, stdin, stdout, stderr = _makeconnection(
   686     proc, stdin, stdout, stderr = _makeconnection(
   684         ui, sshcmd, args, remotecmd, remotepath, sshenv
   687         ui, sshcmd, args, remotecmd, remotepath, sshenv
   685     )
   688     )
   686 
   689 
   687     peer = makepeer(ui, path, proc, stdin, stdout, stderr)
   690     peer = makepeer(ui, path, proc, stdin, stdout, stderr)
   688 
   691 
   689     # Finally, if supported by the server, notify it about our own
   692     # Finally, if supported by the server, notify it about our own
   690     # capabilities.
   693     # capabilities.
   691     if 'protocaps' in peer.capabilities():
   694     if b'protocaps' in peer.capabilities():
   692         try:
   695         try:
   693             peer._call(
   696             peer._call(
   694                 "protocaps", caps=' '.join(sorted(_clientcapabilities()))
   697                 b"protocaps", caps=b' '.join(sorted(_clientcapabilities()))
   695             )
   698             )
   696         except IOError:
   699         except IOError:
   697             peer._cleanup()
   700             peer._cleanup()
   698             raise error.RepoError(_('capability exchange failed'))
   701             raise error.RepoError(_(b'capability exchange failed'))
   699 
   702 
   700     return peer
   703     return peer