/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/ssh.py

  • Committer: Jelmer Vernooij
  • Date: 2018-07-08 14:45:27 UTC
  • mto: This revision was merged to the branch mainline in revision 7036.
  • Revision ID: jelmer@jelmer.uk-20180708144527-codhlvdcdg9y0nji
Fix a bunch of merge tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2006-2011 Robey Pointer <robey@lag.net>
2
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
 
20
from __future__ import absolute_import
 
21
 
20
22
import errno
21
23
import getpass
22
24
import logging
24
26
import socket
25
27
import subprocess
26
28
import sys
 
29
from binascii import hexlify
27
30
 
28
 
from bzrlib import (
 
31
from .. import (
29
32
    config,
30
33
    errors,
31
34
    osutils,
35
38
 
36
39
try:
37
40
    import paramiko
38
 
except ImportError, e:
 
41
except ImportError as e:
39
42
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
40
43
    # access
41
44
    paramiko = None
43
46
    from paramiko.sftp_client import SFTPClient
44
47
 
45
48
 
 
49
class StrangeHostname(errors.BzrError):
 
50
    _fmt = "Refusing to connect to strange SSH hostname %(hostname)s"
 
51
 
 
52
 
46
53
SYSTEM_HOSTKEYS = {}
47
 
BZR_HOSTKEYS = {}
48
 
 
49
 
 
50
 
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
51
 
 
52
 
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
53
 
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
54
 
# so we get an AttributeError exception. So we will not try to
55
 
# connect to an agent if we are on win32 and using Paramiko older than 1.6
56
 
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
54
BRZ_HOSTKEYS = {}
57
55
 
58
56
 
59
57
class SSHVendorManager(object):
60
58
    """Manager for manage SSH vendors."""
61
59
 
62
60
    # Note, although at first sign the class interface seems similar to
63
 
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
61
    # breezy.registry.Registry it is not possible/convenient to directly use
64
62
    # the Registry because the class just has "get()" interface instead of the
65
63
    # Registry's "get(key)".
66
64
 
82
80
        self._cached_ssh_vendor = None
83
81
 
84
82
    def _get_vendor_by_environment(self, environment=None):
85
 
        """Return the vendor or None based on BZR_SSH environment variable.
 
83
        """Return the vendor or None based on BRZ_SSH environment variable.
86
84
 
87
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
85
        :raises UnknownSSH: if the BRZ_SSH environment variable contains
88
86
                            unknown vendor name
89
87
        """
90
88
        if environment is None:
91
89
            environment = os.environ
92
 
        if 'BZR_SSH' in environment:
93
 
            vendor_name = environment['BZR_SSH']
 
90
        if 'BRZ_SSH' in environment:
 
91
            vendor_name = environment['BRZ_SSH']
94
92
            try:
95
93
                vendor = self._ssh_vendors[vendor_name]
96
94
            except KeyError:
107
105
            p = subprocess.Popen(args,
108
106
                                 stdout=subprocess.PIPE,
109
107
                                 stderr=subprocess.PIPE,
 
108
                                 bufsize=0,
110
109
                                 **os_specific_subprocess_params())
111
110
            stdout, stderr = p.communicate()
112
111
        except OSError:
113
 
            stdout = stderr = ''
 
112
            stdout = stderr = b''
114
113
        return stdout + stderr
115
114
 
116
115
    def _get_vendor_by_version_string(self, version, progname):
126
125
        elif 'SSH Secure Shell' in version:
127
126
            trace.mutter('ssh implementation is SSH Corp.')
128
127
            vendor = SSHCorpSubprocessVendor()
 
128
        elif 'lsh' in version:
 
129
            trace.mutter('ssh implementation is GNU lsh.')
 
130
            vendor = LSHSubprocessVendor()
129
131
        # As plink user prompts are not handled currently, don't auto-detect
130
132
        # it by inspection below, but keep this vendor detection for if a path
131
 
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
 
133
        # is given in BRZ_SSH. See https://bugs.launchpad.net/bugs/414743
132
134
        elif 'plink' in version and progname == 'plink':
133
135
            # Checking if "plink" was the executed argument as Windows
134
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
136
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
135
137
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
136
138
            trace.mutter("ssh implementation is Putty's plink.")
137
139
            vendor = PLinkSubprocessVendor()
152
154
        """Find out what version of SSH is on the system.
153
155
 
154
156
        :raises SSHVendorNotFound: if no any SSH vendor is found
155
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
157
        :raises UnknownSSH: if the BRZ_SSH environment variable contains
156
158
                            unknown vendor name
157
159
        """
158
160
        if self._cached_ssh_vendor is None:
199
201
    def recv(self, n):
200
202
        try:
201
203
            return self.__socket.recv(n)
202
 
        except socket.error, e:
 
204
        except socket.error as e:
203
205
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
204
206
                             errno.EBADF):
205
207
                # Connection has closed.  Paramiko expects an empty string in
239
241
    def connect_ssh(self, username, password, host, port, command):
240
242
        """Make an SSH connection.
241
243
 
242
 
        :returns: something with a `close` method, and a `get_filelike_channels`
243
 
            method that returns a pair of (read, write) filelike objects.
 
244
        :returns: an SSHConnection.
244
245
        """
245
246
        raise NotImplementedError(self.connect_ssh)
246
247
 
262
263
        sock = socket.socket()
263
264
        try:
264
265
            sock.connect((host, port))
265
 
        except socket.error, e:
 
266
        except socket.error as e:
266
267
            self._raise_connection_error(host, port=port, orig_error=e)
267
268
        return SFTPClient(SocketAsChannelAdapter(sock))
268
269
 
269
270
register_ssh_vendor('loopback', LoopbackVendor())
270
271
 
271
272
 
272
 
class _ParamikoSSHConnection(object):
273
 
    def __init__(self, channel):
274
 
        self.channel = channel
275
 
 
276
 
    def get_filelike_channels(self):
277
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
278
 
 
279
 
    def close(self):
280
 
        return self.channel.close()
281
 
 
282
 
 
283
273
class ParamikoVendor(SSHVendor):
284
274
    """Vendor that uses paramiko."""
285
275
 
 
276
    def _hexify(self, s):
 
277
        return hexlify(s).upper()
 
278
 
286
279
    def _connect(self, username, password, host, port):
287
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
280
        global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
288
281
 
289
282
        load_host_keys()
290
283
 
292
285
            t = paramiko.Transport((host, port or 22))
293
286
            t.set_log_channel('bzr.paramiko')
294
287
            t.start_client()
295
 
        except (paramiko.SSHException, socket.error), e:
 
288
        except (paramiko.SSHException, socket.error) as e:
296
289
            self._raise_connection_error(host, port=port, orig_error=e)
297
290
 
298
291
        server_key = t.get_remote_server_key()
299
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
292
        server_key_hex = self._hexify(server_key.get_fingerprint())
300
293
        keytype = server_key.get_name()
301
294
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
302
295
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
303
 
            our_server_key_hex = paramiko.util.hexify(
304
 
                our_server_key.get_fingerprint())
305
 
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
306
 
            our_server_key = BZR_HOSTKEYS[host][keytype]
307
 
            our_server_key_hex = paramiko.util.hexify(
308
 
                our_server_key.get_fingerprint())
 
296
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
297
        elif host in BRZ_HOSTKEYS and keytype in BRZ_HOSTKEYS[host]:
 
298
            our_server_key = BRZ_HOSTKEYS[host][keytype]
 
299
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
309
300
        else:
310
301
            trace.warning('Adding %s host key for %s: %s'
311
302
                          % (keytype, host, server_key_hex))
312
 
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
303
            add = getattr(BRZ_HOSTKEYS, 'add', None)
313
304
            if add is not None: # paramiko >= 1.X.X
314
 
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
305
                BRZ_HOSTKEYS.add(host, keytype, server_key)
315
306
            else:
316
 
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
307
                BRZ_HOSTKEYS.setdefault(host, {})[keytype] = server_key
317
308
            our_server_key = server_key
318
 
            our_server_key_hex = paramiko.util.hexify(
319
 
                our_server_key.get_fingerprint())
 
309
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
320
310
            save_host_keys()
321
311
        if server_key != our_server_key:
322
312
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
333
323
        t = self._connect(username, password, host, port)
334
324
        try:
335
325
            return t.open_sftp_client()
336
 
        except paramiko.SSHException, e:
 
326
        except paramiko.SSHException as e:
337
327
            self._raise_connection_error(host, port=port, orig_error=e,
338
328
                                         msg='Unable to start sftp client')
339
329
 
344
334
            cmdline = ' '.join(command)
345
335
            channel.exec_command(cmdline)
346
336
            return _ParamikoSSHConnection(channel)
347
 
        except paramiko.SSHException, e:
 
337
        except paramiko.SSHException as e:
348
338
            self._raise_connection_error(host, port=port, orig_error=e,
349
339
                                         msg='Unable to invoke remote bzr')
350
340
 
 
341
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
351
342
if paramiko is not None:
352
343
    vendor = ParamikoVendor()
353
344
    register_ssh_vendor('paramiko', vendor)
354
345
    register_ssh_vendor('none', vendor)
355
346
    register_default_ssh_vendor(vendor)
356
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
347
    _ssh_connection_errors += (paramiko.SSHException,)
357
348
    del vendor
358
 
else:
359
 
    _sftp_connection_errors = (EOFError,)
360
349
 
361
350
 
362
351
class SubprocessVendor(SSHVendor):
363
352
    """Abstract base class for vendors that use pipes to a subprocess."""
364
353
 
 
354
    # In general stderr should be inherited from the parent process so prompts
 
355
    # are visible on the terminal. This can be overriden to another file for
 
356
    # tests, but beware of using PIPE which may hang due to not being read.
 
357
    _stderr_target = None
 
358
 
 
359
    @staticmethod
 
360
    def _check_hostname(arg):
 
361
        if arg.startswith('-'):
 
362
            raise StrangeHostname(hostname=arg)
 
363
 
365
364
    def _connect(self, argv):
366
 
        proc = subprocess.Popen(argv,
367
 
                                stdin=subprocess.PIPE,
368
 
                                stdout=subprocess.PIPE,
 
365
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
366
        # subprocess.  We prefer sockets to pipes because they support
 
367
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
368
        # whatever) chunks.
 
369
        try:
 
370
            my_sock, subproc_sock = socket.socketpair()
 
371
            osutils.set_fd_cloexec(my_sock)
 
372
        except (AttributeError, socket.error):
 
373
            # This platform doesn't support socketpair(), so just use ordinary
 
374
            # pipes instead.
 
375
            stdin = stdout = subprocess.PIPE
 
376
            my_sock, subproc_sock = None, None
 
377
        else:
 
378
            stdin = stdout = subproc_sock
 
379
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
380
                                stderr=self._stderr_target,
 
381
                                bufsize=0,
369
382
                                **os_specific_subprocess_params())
370
 
        return SSHSubprocess(proc)
 
383
        if subproc_sock is not None:
 
384
            subproc_sock.close()
 
385
        return SSHSubprocessConnection(proc, sock=my_sock)
371
386
 
372
387
    def connect_sftp(self, username, password, host, port):
373
388
        try:
375
390
                                                  subsystem='sftp')
376
391
            sock = self._connect(argv)
377
392
            return SFTPClient(SocketAsChannelAdapter(sock))
378
 
        except _sftp_connection_errors, e:
379
 
            self._raise_connection_error(host, port=port, orig_error=e)
380
 
        except (OSError, IOError), e:
381
 
            # If the machine is fast enough, ssh can actually exit
382
 
            # before we try and send it the sftp request, which
383
 
            # raises a Broken Pipe
384
 
            if e.errno not in (errno.EPIPE,):
385
 
                raise
 
393
        except _ssh_connection_errors as e:
386
394
            self._raise_connection_error(host, port=port, orig_error=e)
387
395
 
388
396
    def connect_ssh(self, username, password, host, port, command):
390
398
            argv = self._get_vendor_specific_argv(username, host, port,
391
399
                                                  command=command)
392
400
            return self._connect(argv)
393
 
        except (EOFError), e:
394
 
            self._raise_connection_error(host, port=port, orig_error=e)
395
 
        except (OSError, IOError), e:
396
 
            # If the machine is fast enough, ssh can actually exit
397
 
            # before we try and send it the sftp request, which
398
 
            # raises a Broken Pipe
399
 
            if e.errno not in (errno.EPIPE,):
400
 
                raise
 
401
        except _ssh_connection_errors as e:
401
402
            self._raise_connection_error(host, port=port, orig_error=e)
402
403
 
403
404
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
418
419
                                  command=None):
419
420
        args = [self.executable_path,
420
421
                '-oForwardX11=no', '-oForwardAgent=no',
421
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
422
                '-oClearAllForwardings=yes',
422
423
                '-oNoHostAuthenticationForLocalhost=yes']
423
424
        if port is not None:
424
425
            args.extend(['-p', str(port)])
425
426
        if username is not None:
426
427
            args.extend(['-l', username])
427
428
        if subsystem is not None:
428
 
            args.extend(['-s', host, subsystem])
 
429
            args.extend(['-s', '--', host, subsystem])
429
430
        else:
430
 
            args.extend([host] + command)
 
431
            args.extend(['--', host] + command)
431
432
        return args
432
433
 
433
434
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
440
441
 
441
442
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
442
443
                                  command=None):
 
444
        self._check_hostname(host)
443
445
        args = [self.executable_path, '-x']
444
446
        if port is not None:
445
447
            args.extend(['-p', str(port)])
454
456
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
455
457
 
456
458
 
 
459
class LSHSubprocessVendor(SubprocessVendor):
 
460
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
461
 
 
462
    executable_path = 'lsh'
 
463
 
 
464
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
465
                                  command=None):
 
466
        self._check_hostname(host)
 
467
        args = [self.executable_path]
 
468
        if port is not None:
 
469
            args.extend(['-p', str(port)])
 
470
        if username is not None:
 
471
            args.extend(['-l', username])
 
472
        if subsystem is not None:
 
473
            args.extend(['--subsystem', subsystem, host])
 
474
        else:
 
475
            args.extend([host] + command)
 
476
        return args
 
477
 
 
478
register_ssh_vendor('lsh', LSHSubprocessVendor())
 
479
 
 
480
 
457
481
class PLinkSubprocessVendor(SubprocessVendor):
458
482
    """SSH vendor that uses the 'plink' executable from Putty."""
459
483
 
461
485
 
462
486
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
463
487
                                  command=None):
 
488
        self._check_hostname(host)
464
489
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
465
490
        if port is not None:
466
491
            args.extend(['-P', str(port)])
482
507
    if username is None:
483
508
        username = auth.get_user('ssh', host, port=port,
484
509
                                 default=getpass.getuser())
485
 
    if _use_ssh_agent:
486
 
        agent = paramiko.Agent()
487
 
        for key in agent.get_keys():
488
 
            trace.mutter('Trying SSH agent key %s'
489
 
                         % paramiko.util.hexify(key.get_fingerprint()))
490
 
            try:
491
 
                paramiko_transport.auth_publickey(username, key)
492
 
                return
493
 
            except paramiko.SSHException, e:
494
 
                pass
 
510
    agent = paramiko.Agent()
 
511
    for key in agent.get_keys():
 
512
        trace.mutter('Trying SSH agent key %s'
 
513
                     % self._hexify(key.get_fingerprint()))
 
514
        try:
 
515
            paramiko_transport.auth_publickey(username, key)
 
516
            return
 
517
        except paramiko.SSHException as e:
 
518
            pass
495
519
 
496
520
    # okay, try finding id_rsa or id_dss?  (posix only)
497
521
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
512
536
            paramiko_transport.auth_none(username)
513
537
        finally:
514
538
            paramiko_transport.logger.setLevel(old_level)
515
 
    except paramiko.BadAuthenticationType, e:
 
539
    except paramiko.BadAuthenticationType as e:
516
540
        # Supported methods are in the exception
517
541
        supported_auth_types = e.allowed_types
518
 
    except paramiko.SSHException, e:
 
542
    except paramiko.SSHException as e:
519
543
        # Don't know what happened, but just ignore it
520
544
        pass
521
545
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
536
560
        try:
537
561
            paramiko_transport.auth_password(username, password)
538
562
            return
539
 
        except paramiko.SSHException, e:
 
563
        except paramiko.SSHException as e:
540
564
            pass
541
565
 
542
566
    # give up and ask for a password
545
569
    if password is not None:
546
570
        try:
547
571
            paramiko_transport.auth_password(username, password)
548
 
        except paramiko.SSHException, e:
 
572
        except paramiko.SSHException as e:
549
573
            raise errors.ConnectionError(
550
574
                'Unable to authenticate to SSH host as'
551
575
                '\n  %s@%s\n' % (username, host), e)
562
586
        return True
563
587
    except paramiko.PasswordRequiredException:
564
588
        password = ui.ui_factory.get_password(
565
 
            prompt='SSH %(filename)s password', filename=filename)
 
589
            prompt=u'SSH %(filename)s password',
 
590
            filename=filename.decode(osutils._fs_enc))
566
591
        try:
567
592
            key = pkey_class.from_private_key_file(filename, password)
568
593
            paramiko_transport.auth_publickey(username, key)
583
608
    Load system host keys (probably doesn't work on windows) and any
584
609
    "discovered" keys from previous sessions.
585
610
    """
586
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
611
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
587
612
    try:
588
613
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
589
614
            os.path.expanduser('~/.ssh/known_hosts'))
590
 
    except IOError, e:
 
615
    except IOError as e:
591
616
        trace.mutter('failed to load system host keys: ' + str(e))
592
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
617
    brz_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
593
618
    try:
594
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
595
 
    except IOError, e:
596
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
619
        BRZ_HOSTKEYS = paramiko.util.load_host_keys(brz_hostkey_path)
 
620
    except IOError as e:
 
621
        trace.mutter('failed to load brz host keys: ' + str(e))
597
622
        save_host_keys()
598
623
 
599
624
 
601
626
    """
602
627
    Save "discovered" host keys in $(config)/ssh_host_keys/.
603
628
    """
604
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
629
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
605
630
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
606
631
    config.ensure_config_dir_exists()
607
632
 
608
633
    try:
609
 
        f = open(bzr_hostkey_path, 'w')
610
 
        f.write('# SSH host keys collected by bzr\n')
611
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
612
 
            for keytype, key in keys.iteritems():
613
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
614
 
        f.close()
615
 
    except IOError, e:
 
634
        with open(bzr_hostkey_path, 'w') as f:
 
635
            f.write('# SSH host keys collected by bzr\n')
 
636
            for hostname, keys in BRZ_HOSTKEYS.items():
 
637
                for keytype, key in keys.items():
 
638
                    f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
639
    except IOError as e:
616
640
        trace.mutter('failed to save bzr host keys: ' + str(e))
617
641
 
618
642
 
644
668
import weakref
645
669
_subproc_weakrefs = set()
646
670
 
647
 
def _close_ssh_proc(proc):
648
 
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
671
def _close_ssh_proc(proc, sock):
 
672
    """Carefully close stdin/stdout and reap the SSH process.
 
673
 
 
674
    If the pipes are already closed and/or the process has already been
 
675
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
676
    to clean up (whether or not a clean up was already tried).
 
677
    """
 
678
    funcs = []
 
679
    for closeable in (proc.stdin, proc.stdout, sock):
 
680
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
681
        # stdout streams to close, or that we will have been passed a socket to
 
682
        # close, with the option not in use being None.
 
683
        if closeable is not None:
 
684
            funcs.append(closeable.close)
 
685
    funcs.append(proc.wait)
 
686
    for func in funcs:
649
687
        try:
650
688
            func()
651
689
        except OSError:
652
 
            pass
653
 
 
654
 
 
655
 
class SSHSubprocess(object):
656
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
657
 
 
658
 
    def __init__(self, proc):
 
690
            # It's ok for the pipe to already be closed, or the process to
 
691
            # already be finished.
 
692
            continue
 
693
 
 
694
 
 
695
class SSHConnection(object):
 
696
    """Abstract base class for SSH connections."""
 
697
 
 
698
    def get_sock_or_pipes(self):
 
699
        """Returns a (kind, io_object) pair.
 
700
 
 
701
        If kind == 'socket', then io_object is a socket.
 
702
 
 
703
        If kind == 'pipes', then io_object is a pair of file-like objects
 
704
        (read_from, write_to).
 
705
        """
 
706
        raise NotImplementedError(self.get_sock_or_pipes)
 
707
 
 
708
    def close(self):
 
709
        raise NotImplementedError(self.close)
 
710
 
 
711
 
 
712
class SSHSubprocessConnection(SSHConnection):
 
713
    """A connection to an ssh subprocess via pipes or a socket.
 
714
 
 
715
    This class is also socket-like enough to be used with
 
716
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
717
    """
 
718
 
 
719
    def __init__(self, proc, sock=None):
 
720
        """Constructor.
 
721
 
 
722
        :param proc: a subprocess.Popen
 
723
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
724
            should breezy's half of that socketpair.  If not passed, proc's
 
725
            stdin/out is assumed to be ordinary pipes.
 
726
        """
659
727
        self.proc = proc
 
728
        self._sock = sock
660
729
        # Add a weakref to proc that will attempt to do the same as self.close
661
730
        # to avoid leaving processes lingering indefinitely.
662
731
        def terminate(ref):
663
732
            _subproc_weakrefs.remove(ref)
664
 
            _close_ssh_proc(proc)
 
733
            _close_ssh_proc(proc, sock)
665
734
        _subproc_weakrefs.add(weakref.ref(self, terminate))
666
735
 
667
736
    def send(self, data):
668
 
        return os.write(self.proc.stdin.fileno(), data)
 
737
        if self._sock is not None:
 
738
            return self._sock.send(data)
 
739
        else:
 
740
            return os.write(self.proc.stdin.fileno(), data)
669
741
 
670
742
    def recv(self, count):
671
 
        return os.read(self.proc.stdout.fileno(), count)
672
 
 
673
 
    def close(self):
674
 
        _close_ssh_proc(self.proc)
675
 
 
676
 
    def get_filelike_channels(self):
677
 
        return (self.proc.stdout, self.proc.stdin)
 
743
        if self._sock is not None:
 
744
            return self._sock.recv(count)
 
745
        else:
 
746
            return os.read(self.proc.stdout.fileno(), count)
 
747
 
 
748
    def close(self):
 
749
        _close_ssh_proc(self.proc, self._sock)
 
750
 
 
751
    def get_sock_or_pipes(self):
 
752
        if self._sock is not None:
 
753
            return 'socket', self._sock
 
754
        else:
 
755
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
756
 
 
757
 
 
758
class _ParamikoSSHConnection(SSHConnection):
 
759
    """An SSH connection via paramiko."""
 
760
 
 
761
    def __init__(self, channel):
 
762
        self.channel = channel
 
763
 
 
764
    def get_sock_or_pipes(self):
 
765
        return ('socket', self.channel)
 
766
 
 
767
    def close(self):
 
768
        return self.channel.close()
 
769
 
678
770