~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: John Arbash Meinel
  • Date: 2010-02-17 17:11:16 UTC
  • mfrom: (4797.2.17 2.1)
  • mto: (4797.2.18 2.1)
  • mto: This revision was merged to the branch mainline in revision 5055.
  • Revision ID: john@arbash-meinel.com-20100217171116-h7t9223ystbnx5h8
merge bzr.2.1 in preparation for NEWS entry.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2006-2010 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
13
13
#
14
14
# You should have received a copy of the GNU General Public License
15
15
# along with this program; if not, write to the Free Software
16
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
20
20
import errno
21
21
import getpass
 
22
import logging
22
23
import os
23
24
import socket
24
25
import subprocess
25
26
import sys
26
27
 
27
 
from bzrlib.config import config_dir, ensure_config_dir_exists
28
 
from bzrlib.errors import (ConnectionError,
29
 
                           ParamikoNotPresent,
30
 
                           SocketConnectionError,
31
 
                           SSHVendorNotFound,
32
 
                           TransportError,
33
 
                           UnknownSSH,
34
 
                           )
35
 
 
36
 
from bzrlib.osutils import pathjoin
37
 
from bzrlib.trace import mutter, warning
38
 
import bzrlib.ui
 
28
from bzrlib import (
 
29
    config,
 
30
    errors,
 
31
    osutils,
 
32
    trace,
 
33
    ui,
 
34
    )
39
35
 
40
36
try:
41
37
    import paramiko
98
94
            try:
99
95
                vendor = self._ssh_vendors[vendor_name]
100
96
            except KeyError:
101
 
                raise UnknownSSH(vendor_name)
 
97
                vendor = self._get_vendor_from_path(vendor_name)
 
98
                if vendor is None:
 
99
                    raise errors.UnknownSSH(vendor_name)
 
100
                vendor.executable_path = vendor_name
102
101
            return vendor
103
102
        return None
104
103
 
114
113
            stdout = stderr = ''
115
114
        return stdout + stderr
116
115
 
117
 
    def _get_vendor_by_version_string(self, version, args):
 
116
    def _get_vendor_by_version_string(self, version, progname):
118
117
        """Return the vendor or None based on output from the subprocess.
119
118
 
120
119
        :param version: The output of 'ssh -V' like command.
122
121
        """
123
122
        vendor = None
124
123
        if 'OpenSSH' in version:
125
 
            mutter('ssh implementation is OpenSSH')
 
124
            trace.mutter('ssh implementation is OpenSSH')
126
125
            vendor = OpenSSHSubprocessVendor()
127
126
        elif 'SSH Secure Shell' in version:
128
 
            mutter('ssh implementation is SSH Corp.')
 
127
            trace.mutter('ssh implementation is SSH Corp.')
129
128
            vendor = SSHCorpSubprocessVendor()
130
 
        elif 'plink' in version and args[0] == 'plink':
131
 
            # Checking if "plink" was the executed argument as Windows sometimes 
132
 
            # reports 'ssh -V' incorrectly with 'plink' in it's version. 
133
 
            # See https://bugs.launchpad.net/bzr/+bug/107155
134
 
            mutter("ssh implementation is Putty's plink.")
 
129
        # As plink user prompts are not handled currently, don't auto-detect
 
130
        # 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
 
132
        elif 'plink' in version and progname == 'plink':
 
133
            # Checking if "plink" was the executed argument as Windows
 
134
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
135
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
 
136
            trace.mutter("ssh implementation is Putty's plink.")
135
137
            vendor = PLinkSubprocessVendor()
136
138
        return vendor
137
139
 
138
140
    def _get_vendor_by_inspection(self):
139
141
        """Return the vendor or None by checking for known SSH implementations."""
140
 
        for args in [['ssh', '-V'], ['plink', '-V']]:
141
 
            version = self._get_ssh_version_string(args)
142
 
            vendor = self._get_vendor_by_version_string(version, args)
143
 
            if vendor is not None:
144
 
                return vendor
145
 
        return None
 
142
        version = self._get_ssh_version_string(['ssh', '-V'])
 
143
        return self._get_vendor_by_version_string(version, "ssh")
 
144
 
 
145
    def _get_vendor_from_path(self, path):
 
146
        """Return the vendor or None using the program at the given path"""
 
147
        version = self._get_ssh_version_string([path, '-V'])
 
148
        return self._get_vendor_by_version_string(version, 
 
149
            os.path.splitext(os.path.basename(path))[0])
146
150
 
147
151
    def get_vendor(self, environment=None):
148
152
        """Find out what version of SSH is on the system.
156
160
            if vendor is None:
157
161
                vendor = self._get_vendor_by_inspection()
158
162
                if vendor is None:
159
 
                    mutter('falling back to default implementation')
 
163
                    trace.mutter('falling back to default implementation')
160
164
                    vendor = self._default_ssh_vendor
161
165
                    if vendor is None:
162
 
                        raise SSHVendorNotFound()
 
166
                        raise errors.SSHVendorNotFound()
163
167
            self._cached_ssh_vendor = vendor
164
168
        return self._cached_ssh_vendor
165
169
 
177
181
    signal.signal(signal.SIGINT, signal.SIG_IGN)
178
182
 
179
183
 
180
 
class LoopbackSFTP(object):
 
184
class SocketAsChannelAdapter(object):
181
185
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
182
186
 
183
187
    def __init__(self, sock):
184
188
        self.__socket = sock
185
 
 
 
189
 
 
190
    def get_name(self):
 
191
        return "bzr SocketAsChannelAdapter"
 
192
 
186
193
    def send(self, data):
187
194
        return self.__socket.send(data)
188
195
 
189
196
    def recv(self, n):
190
 
        return self.__socket.recv(n)
 
197
        try:
 
198
            return self.__socket.recv(n)
 
199
        except socket.error, e:
 
200
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
201
                             errno.EBADF):
 
202
                # Connection has closed.  Paramiko expects an empty string in
 
203
                # this case, not an exception.
 
204
                return ''
 
205
            raise
191
206
 
192
207
    def recv_ready(self):
 
208
        # TODO: jam 20051215 this function is necessary to support the
 
209
        # pipelined() function. In reality, it probably should use
 
210
        # poll() or select() to actually return if there is data
 
211
        # available, otherwise we probably don't get any benefit
193
212
        return True
194
213
 
195
214
    def close(self):
198
217
 
199
218
class SSHVendor(object):
200
219
    """Abstract base class for SSH vendor implementations."""
201
 
    
 
220
 
202
221
    def connect_sftp(self, username, password, host, port):
203
222
        """Make an SSH connection, and return an SFTPClient.
204
 
        
 
223
 
205
224
        :param username: an ascii string
206
225
        :param password: an ascii string
207
226
        :param host: a host name as an ascii string
216
235
 
217
236
    def connect_ssh(self, username, password, host, port, command):
218
237
        """Make an SSH connection.
219
 
        
 
238
 
220
239
        :returns: something with a `close` method, and a `get_filelike_channels`
221
240
            method that returns a pair of (read, write) filelike objects.
222
241
        """
223
242
        raise NotImplementedError(self.connect_ssh)
224
 
        
 
243
 
225
244
    def _raise_connection_error(self, host, port=None, orig_error=None,
226
245
                                msg='Unable to connect to SSH host'):
227
246
        """Raise a SocketConnectionError with properly formatted host.
229
248
        This just unifies all the locations that try to raise ConnectionError,
230
249
        so that they format things properly.
231
250
        """
232
 
        raise SocketConnectionError(host=host, port=port, msg=msg,
233
 
                                    orig_error=orig_error)
 
251
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
252
                                           orig_error=orig_error)
234
253
 
235
254
 
236
255
class LoopbackVendor(SSHVendor):
237
256
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
238
 
    
 
257
 
239
258
    def connect_sftp(self, username, password, host, port):
240
259
        sock = socket.socket()
241
260
        try:
242
261
            sock.connect((host, port))
243
262
        except socket.error, e:
244
263
            self._raise_connection_error(host, port=port, orig_error=e)
245
 
        return SFTPClient(LoopbackSFTP(sock))
 
264
        return SFTPClient(SocketAsChannelAdapter(sock))
246
265
 
247
266
register_ssh_vendor('loopback', LoopbackVendor())
248
267
 
263
282
 
264
283
    def _connect(self, username, password, host, port):
265
284
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
266
 
        
 
285
 
267
286
        load_host_keys()
268
287
 
269
288
        try:
272
291
            t.start_client()
273
292
        except (paramiko.SSHException, socket.error), e:
274
293
            self._raise_connection_error(host, port=port, orig_error=e)
275
 
            
 
294
 
276
295
        server_key = t.get_remote_server_key()
277
296
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
278
297
        keytype = server_key.get_name()
279
298
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
280
299
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
281
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
300
            our_server_key_hex = paramiko.util.hexify(
 
301
                our_server_key.get_fingerprint())
282
302
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
283
303
            our_server_key = BZR_HOSTKEYS[host][keytype]
284
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
304
            our_server_key_hex = paramiko.util.hexify(
 
305
                our_server_key.get_fingerprint())
285
306
        else:
286
 
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
 
307
            trace.warning('Adding %s host key for %s: %s'
 
308
                          % (keytype, host, server_key_hex))
287
309
            add = getattr(BZR_HOSTKEYS, 'add', None)
288
310
            if add is not None: # paramiko >= 1.X.X
289
311
                BZR_HOSTKEYS.add(host, keytype, server_key)
290
312
            else:
291
313
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
292
314
            our_server_key = server_key
293
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
315
            our_server_key_hex = paramiko.util.hexify(
 
316
                our_server_key.get_fingerprint())
294
317
            save_host_keys()
295
318
        if server_key != our_server_key:
296
319
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
297
 
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
298
 
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
 
320
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
321
            raise errors.TransportError(
 
322
                'Host keys for %s do not match!  %s != %s' %
299
323
                (host, our_server_key_hex, server_key_hex),
300
324
                ['Try editing %s or %s' % (filename1, filename2)])
301
325
 
302
 
        _paramiko_auth(username, password, host, t)
 
326
        _paramiko_auth(username, password, host, port, t)
303
327
        return t
304
 
        
 
328
 
305
329
    def connect_sftp(self, username, password, host, port):
306
330
        t = self._connect(username, password, host, port)
307
331
        try:
326
350
    register_ssh_vendor('paramiko', vendor)
327
351
    register_ssh_vendor('none', vendor)
328
352
    register_default_ssh_vendor(vendor)
 
353
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
329
354
    del vendor
 
355
else:
 
356
    _sftp_connection_errors = (EOFError,)
330
357
 
331
358
 
332
359
class SubprocessVendor(SSHVendor):
333
360
    """Abstract base class for vendors that use pipes to a subprocess."""
334
 
    
 
361
 
335
362
    def _connect(self, argv):
336
363
        proc = subprocess.Popen(argv,
337
364
                                stdin=subprocess.PIPE,
344
371
            argv = self._get_vendor_specific_argv(username, host, port,
345
372
                                                  subsystem='sftp')
346
373
            sock = self._connect(argv)
347
 
            return SFTPClient(sock)
348
 
        except (EOFError, paramiko.SSHException), e:
 
374
            return SFTPClient(SocketAsChannelAdapter(sock))
 
375
        except _sftp_connection_errors, e:
349
376
            self._raise_connection_error(host, port=port, orig_error=e)
350
377
        except (OSError, IOError), e:
351
378
            # If the machine is fast enough, ssh can actually exit
373
400
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
374
401
                                  command=None):
375
402
        """Returns the argument list to run the subprocess with.
376
 
        
 
403
 
377
404
        Exactly one of 'subsystem' and 'command' must be specified.
378
405
        """
379
406
        raise NotImplementedError(self._get_vendor_specific_argv)
381
408
 
382
409
class OpenSSHSubprocessVendor(SubprocessVendor):
383
410
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
384
 
    
 
411
 
 
412
    executable_path = 'ssh'
 
413
 
385
414
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
386
415
                                  command=None):
387
 
        assert subsystem is not None or command is not None, (
388
 
            'Must specify a command or subsystem')
389
 
        if subsystem is not None:
390
 
            assert command is None, (
391
 
                'subsystem and command are mutually exclusive')
392
 
        args = ['ssh',
 
416
        args = [self.executable_path,
393
417
                '-oForwardX11=no', '-oForwardAgent=no',
394
418
                '-oClearAllForwardings=yes', '-oProtocol=2',
395
419
                '-oNoHostAuthenticationForLocalhost=yes']
409
433
class SSHCorpSubprocessVendor(SubprocessVendor):
410
434
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
411
435
 
 
436
    executable_path = 'ssh'
 
437
 
412
438
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
413
439
                                  command=None):
414
 
        assert subsystem is not None or command is not None, (
415
 
            'Must specify a command or subsystem')
416
 
        if subsystem is not None:
417
 
            assert command is None, (
418
 
                'subsystem and command are mutually exclusive')
419
 
        args = ['ssh', '-x']
 
440
        args = [self.executable_path, '-x']
420
441
        if port is not None:
421
442
            args.extend(['-p', str(port)])
422
443
        if username is not None:
426
447
        else:
427
448
            args.extend([host] + command)
428
449
        return args
429
 
    
430
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
 
450
 
 
451
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
431
452
 
432
453
 
433
454
class PLinkSubprocessVendor(SubprocessVendor):
434
455
    """SSH vendor that uses the 'plink' executable from Putty."""
435
456
 
 
457
    executable_path = 'plink'
 
458
 
436
459
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
437
460
                                  command=None):
438
 
        assert subsystem is not None or command is not None, (
439
 
            'Must specify a command or subsystem')
440
 
        if subsystem is not None:
441
 
            assert command is None, (
442
 
                'subsystem and command are mutually exclusive')
443
 
        args = ['plink', '-x', '-a', '-ssh', '-2']
 
461
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
444
462
        if port is not None:
445
463
            args.extend(['-P', str(port)])
446
464
        if username is not None:
454
472
register_ssh_vendor('plink', PLinkSubprocessVendor())
455
473
 
456
474
 
457
 
def _paramiko_auth(username, password, host, paramiko_transport):
458
 
    # paramiko requires a username, but it might be none if nothing was supplied
459
 
    # use the local username, just in case.
460
 
    # We don't override username, because if we aren't using paramiko,
461
 
    # the username might be specified in ~/.ssh/config and we don't want to
462
 
    # force it to something else
463
 
    # Also, it would mess up the self.relpath() functionality
464
 
    username = username or getpass.getuser()
465
 
 
 
475
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
476
    auth = config.AuthenticationConfig()
 
477
    # paramiko requires a username, but it might be none if nothing was
 
478
    # supplied.  If so, use the local username.
 
479
    if username is None:
 
480
        username = auth.get_user('ssh', host, port=port,
 
481
                                 default=getpass.getuser())
466
482
    if _use_ssh_agent:
467
483
        agent = paramiko.Agent()
468
484
        for key in agent.get_keys():
469
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
485
            trace.mutter('Trying SSH agent key %s'
 
486
                         % paramiko.util.hexify(key.get_fingerprint()))
470
487
            try:
471
488
                paramiko_transport.auth_publickey(username, key)
472
489
                return
473
490
            except paramiko.SSHException, e:
474
491
                pass
475
 
    
 
492
 
476
493
    # okay, try finding id_rsa or id_dss?  (posix only)
477
494
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
478
495
        return
479
496
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
480
497
        return
481
498
 
 
499
    # If we have gotten this far, we are about to try for passwords, do an
 
500
    # auth_none check to see if it is even supported.
 
501
    supported_auth_types = []
 
502
    try:
 
503
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
504
        #    Authentication type (none) not permitted.
 
505
        # So we explicitly disable the logging level for this action
 
506
        old_level = paramiko_transport.logger.level
 
507
        paramiko_transport.logger.setLevel(logging.WARNING)
 
508
        try:
 
509
            paramiko_transport.auth_none(username)
 
510
        finally:
 
511
            paramiko_transport.logger.setLevel(old_level)
 
512
    except paramiko.BadAuthenticationType, e:
 
513
        # Supported methods are in the exception
 
514
        supported_auth_types = e.allowed_types
 
515
    except paramiko.SSHException, e:
 
516
        # Don't know what happened, but just ignore it
 
517
        pass
 
518
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
519
    # because Paramiko's auth_password method will automatically try
 
520
    # 'keyboard-interactive' auth (using the password as the response) if
 
521
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
522
    # OpenSSH servers require this.
 
523
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
524
    # requires something other than a single password, but we currently don't
 
525
    # support that.
 
526
    if ('password' not in supported_auth_types and
 
527
        'keyboard-interactive' not in supported_auth_types):
 
528
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
529
            '\n  %s@%s\nsupported auth types: %s'
 
530
            % (username, host, supported_auth_types))
 
531
 
482
532
    if password:
483
533
        try:
484
534
            paramiko_transport.auth_password(username, password)
487
537
            pass
488
538
 
489
539
    # give up and ask for a password
490
 
    password = bzrlib.ui.ui_factory.get_password(
491
 
            prompt='SSH %(user)s@%(host)s password',
492
 
            user=username, host=host)
493
 
    try:
494
 
        paramiko_transport.auth_password(username, password)
495
 
    except paramiko.SSHException, e:
496
 
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
497
 
                              (username, host), e)
 
540
    password = auth.get_password('ssh', host, username, port=port)
 
541
    # get_password can still return None, which means we should not prompt
 
542
    if password is not None:
 
543
        try:
 
544
            paramiko_transport.auth_password(username, password)
 
545
        except paramiko.SSHException, e:
 
546
            raise errors.ConnectionError(
 
547
                'Unable to authenticate to SSH host as'
 
548
                '\n  %s@%s\n' % (username, host), e)
 
549
    else:
 
550
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
551
                                     '  %s@%s' % (username, host))
498
552
 
499
553
 
500
554
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
504
558
        paramiko_transport.auth_publickey(username, key)
505
559
        return True
506
560
    except paramiko.PasswordRequiredException:
507
 
        password = bzrlib.ui.ui_factory.get_password(
508
 
                prompt='SSH %(filename)s password',
509
 
                filename=filename)
 
561
        password = ui.ui_factory.get_password(
 
562
            prompt='SSH %(filename)s password', filename=filename)
510
563
        try:
511
564
            key = pkey_class.from_private_key_file(filename, password)
512
565
            paramiko_transport.auth_publickey(username, key)
513
566
            return True
514
567
        except paramiko.SSHException:
515
 
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
568
            trace.mutter('SSH authentication via %s key failed.'
 
569
                         % (os.path.basename(filename),))
516
570
    except paramiko.SSHException:
517
 
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
571
        trace.mutter('SSH authentication via %s key failed.'
 
572
                     % (os.path.basename(filename),))
518
573
    except IOError:
519
574
        pass
520
575
    return False
527
582
    """
528
583
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
529
584
    try:
530
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
 
585
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
586
            os.path.expanduser('~/.ssh/known_hosts'))
531
587
    except IOError, e:
532
 
        mutter('failed to load system host keys: ' + str(e))
533
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
 
588
        trace.mutter('failed to load system host keys: ' + str(e))
 
589
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
534
590
    try:
535
591
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
536
592
    except IOError, e:
537
 
        mutter('failed to load bzr host keys: ' + str(e))
 
593
        trace.mutter('failed to load bzr host keys: ' + str(e))
538
594
        save_host_keys()
539
595
 
540
596
 
543
599
    Save "discovered" host keys in $(config)/ssh_host_keys/.
544
600
    """
545
601
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
546
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
547
 
    ensure_config_dir_exists()
 
602
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
603
    config.ensure_config_dir_exists()
548
604
 
549
605
    try:
550
606
        f = open(bzr_hostkey_path, 'w')
554
610
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
555
611
        f.close()
556
612
    except IOError, e:
557
 
        mutter('failed to save bzr host keys: ' + str(e))
 
613
        trace.mutter('failed to save bzr host keys: ' + str(e))
558
614
 
559
615
 
560
616
def os_specific_subprocess_params():
561
617
    """Get O/S specific subprocess parameters."""
562
618
    if sys.platform == 'win32':
563
 
        # setting the process group and closing fds is not supported on 
 
619
        # setting the process group and closing fds is not supported on
564
620
        # win32
565
621
        return {}
566
622
    else:
567
 
        # We close fds other than the pipes as the child process does not need 
 
623
        # We close fds other than the pipes as the child process does not need
568
624
        # them to be open.
569
625
        #
570
626
        # We also set the child process to ignore SIGINT.  Normally the signal
572
628
        # this causes it to be seen only by bzr and not by ssh.  Python will
573
629
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
574
630
        # to release locks or do other cleanup over ssh before the connection
575
 
        # goes away.  
 
631
        # goes away.
576
632
        # <https://launchpad.net/products/bzr/+bug/5987>
577
633
        #
578
634
        # Running it in a separate process group is not good because then it
582
638
                'close_fds': True,
583
639
                }
584
640
 
 
641
import weakref
 
642
_subproc_weakrefs = set()
 
643
 
 
644
def _close_ssh_proc(proc):
 
645
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
646
        try:
 
647
            func()
 
648
        except OSError:
 
649
            pass
 
650
 
585
651
 
586
652
class SSHSubprocess(object):
587
653
    """A socket-like object that talks to an ssh subprocess via pipes."""
588
654
 
589
655
    def __init__(self, proc):
590
656
        self.proc = proc
 
657
        # Add a weakref to proc that will attempt to do the same as self.close
 
658
        # to avoid leaving processes lingering indefinitely.
 
659
        def terminate(ref):
 
660
            _subproc_weakrefs.remove(ref)
 
661
            _close_ssh_proc(proc)
 
662
        _subproc_weakrefs.add(weakref.ref(self, terminate))
591
663
 
592
664
    def send(self, data):
593
665
        return os.write(self.proc.stdin.fileno(), data)
594
666
 
595
 
    def recv_ready(self):
596
 
        # TODO: jam 20051215 this function is necessary to support the
597
 
        # pipelined() function. In reality, it probably should use
598
 
        # poll() or select() to actually return if there is data
599
 
        # available, otherwise we probably don't get any benefit
600
 
        return True
601
 
 
602
667
    def recv(self, count):
603
668
        return os.read(self.proc.stdout.fileno(), count)
604
669
 
605
670
    def close(self):
606
 
        self.proc.stdin.close()
607
 
        self.proc.stdout.close()
608
 
        self.proc.wait()
 
671
        _close_ssh_proc(self.proc)
609
672
 
610
673
    def get_filelike_channels(self):
611
674
        return (self.proc.stdout, self.proc.stdin)