~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: John Arbash Meinel
  • Date: 2007-04-12 21:33:07 UTC
  • mfrom: (2413.4.1 api-doc-builders)
  • mto: This revision was merged to the branch mainline in revision 2566.
  • Revision ID: john@arbash-meinel.com-20070412213307-kuh07cnzaud12wx1
[merge] api-doc-builder and remove the pydoctor build code for now.

Show diffs side-by-side

added added

removed removed

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