~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: aaron.bentley at utoronto
  • Date: 2005-08-27 04:42:41 UTC
  • mfrom: (1092.1.43)
  • mto: (1185.3.4)
  • mto: This revision was merged to the branch mainline in revision 1178.
  • Revision ID: aaron.bentley@utoronto.ca-20050827044241-23d676133b9fc981
Merge of robertc@robertcollins.net-20050826013321-52eee1f1da679ee9

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
 
#
4
 
# This program is free software; you can redistribute it and/or modify
5
 
# it under the terms of the GNU General Public License as published by
6
 
# the Free Software Foundation; either version 2 of the License, or
7
 
# (at your option) any later version.
8
 
#
9
 
# This program is distributed in the hope that it will be useful,
10
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
# GNU General Public License for more details.
13
 
#
14
 
# You should have received a copy of the GNU General Public License
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
17
 
 
18
 
"""Foundation SSH support for SFTP and smart server."""
19
 
 
20
 
import errno
21
 
import getpass
22
 
import os
23
 
import socket
24
 
import subprocess
25
 
import sys
26
 
 
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
39
 
 
40
 
try:
41
 
    import paramiko
42
 
except ImportError, e:
43
 
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
44
 
    # access
45
 
    paramiko = None
46
 
else:
47
 
    from paramiko.sftp_client import SFTPClient
48
 
 
49
 
 
50
 
SYSTEM_HOSTKEYS = {}
51
 
BZR_HOSTKEYS = {}
52
 
 
53
 
 
54
 
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
55
 
 
56
 
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
57
 
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
58
 
# so we get an AttributeError exception. So we will not try to
59
 
# connect to an agent if we are on win32 and using Paramiko older than 1.6
60
 
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
61
 
 
62
 
 
63
 
class SSHVendorManager(object):
64
 
    """Manager for manage SSH vendors."""
65
 
 
66
 
    # Note, although at first sign the class interface seems similar to
67
 
    # bzrlib.registry.Registry it is not possible/convenient to directly use
68
 
    # the Registry because the class just has "get()" interface instead of the
69
 
    # Registry's "get(key)".
70
 
 
71
 
    def __init__(self):
72
 
        self._ssh_vendors = {}
73
 
        self._cached_ssh_vendor = None
74
 
        self._default_ssh_vendor = None
75
 
 
76
 
    def register_default_vendor(self, vendor):
77
 
        """Register default SSH vendor."""
78
 
        self._default_ssh_vendor = vendor
79
 
 
80
 
    def register_vendor(self, name, vendor):
81
 
        """Register new SSH vendor by name."""
82
 
        self._ssh_vendors[name] = vendor
83
 
 
84
 
    def clear_cache(self):
85
 
        """Clear previously cached lookup result."""
86
 
        self._cached_ssh_vendor = None
87
 
 
88
 
    def _get_vendor_by_environment(self, environment=None):
89
 
        """Return the vendor or None based on BZR_SSH environment variable.
90
 
 
91
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
92
 
                            unknown vendor name
93
 
        """
94
 
        if environment is None:
95
 
            environment = os.environ
96
 
        if 'BZR_SSH' in environment:
97
 
            vendor_name = environment['BZR_SSH']
98
 
            try:
99
 
                vendor = self._ssh_vendors[vendor_name]
100
 
            except KeyError:
101
 
                raise UnknownSSH(vendor_name)
102
 
            return vendor
103
 
        return None
104
 
 
105
 
    def _get_ssh_version_string(self, args):
106
 
        """Return SSH version string from the subprocess."""
107
 
        try:
108
 
            p = subprocess.Popen(args,
109
 
                                 stdout=subprocess.PIPE,
110
 
                                 stderr=subprocess.PIPE,
111
 
                                 **os_specific_subprocess_params())
112
 
            stdout, stderr = p.communicate()
113
 
        except OSError:
114
 
            stdout = stderr = ''
115
 
        return stdout + stderr
116
 
 
117
 
    def _get_vendor_by_version_string(self, version):
118
 
        """Return the vendor or None based on output from the subprocess.
119
 
 
120
 
        :param version: The output of 'ssh -V' like command.
121
 
        """
122
 
        vendor = None
123
 
        if 'OpenSSH' in version:
124
 
            mutter('ssh implementation is OpenSSH')
125
 
            vendor = OpenSSHSubprocessVendor()
126
 
        elif 'SSH Secure Shell' in version:
127
 
            mutter('ssh implementation is SSH Corp.')
128
 
            vendor = SSHCorpSubprocessVendor()
129
 
        elif 'plink' in version:
130
 
            mutter("ssh implementation is Putty's plink.")
131
 
            vendor = PLinkSubprocessVendor()
132
 
        return vendor
133
 
 
134
 
    def _get_vendor_by_inspection(self):
135
 
        """Return the vendor or None by checking for known SSH implementations."""
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
142
 
 
143
 
    def get_vendor(self, environment=None):
144
 
        """Find out what version of SSH is on the system.
145
 
 
146
 
        :raises SSHVendorNotFound: if no any SSH vendor is found
147
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
148
 
                            unknown vendor name
149
 
        """
150
 
        if self._cached_ssh_vendor is None:
151
 
            vendor = self._get_vendor_by_environment(environment)
152
 
            if vendor is None:
153
 
                vendor = self._get_vendor_by_inspection()
154
 
                if vendor is None:
155
 
                    mutter('falling back to default implementation')
156
 
                    vendor = self._default_ssh_vendor
157
 
                    if vendor is None:
158
 
                        raise SSHVendorNotFound()
159
 
            self._cached_ssh_vendor = vendor
160
 
        return self._cached_ssh_vendor
161
 
 
162
 
_ssh_vendor_manager = SSHVendorManager()
163
 
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
164
 
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
165
 
register_ssh_vendor = _ssh_vendor_manager.register_vendor
166
 
 
167
 
 
168
 
def _ignore_sigint():
169
 
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
170
 
    # doesn't handle it itself.
171
 
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
172
 
    import signal
173
 
    signal.signal(signal.SIGINT, signal.SIG_IGN)
174
 
 
175
 
 
176
 
class LoopbackSFTP(object):
177
 
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
178
 
 
179
 
    def __init__(self, sock):
180
 
        self.__socket = sock
181
 
 
182
 
    def send(self, data):
183
 
        return self.__socket.send(data)
184
 
 
185
 
    def recv(self, n):
186
 
        return self.__socket.recv(n)
187
 
 
188
 
    def recv_ready(self):
189
 
        return True
190
 
 
191
 
    def close(self):
192
 
        self.__socket.close()
193
 
 
194
 
 
195
 
class SSHVendor(object):
196
 
    """Abstract base class for SSH vendor implementations."""
197
 
    
198
 
    def connect_sftp(self, username, password, host, port):
199
 
        """Make an SSH connection, and return an SFTPClient.
200
 
        
201
 
        :param username: an ascii string
202
 
        :param password: an ascii string
203
 
        :param host: a host name as an ascii string
204
 
        :param port: a port number
205
 
        :type port: int
206
 
 
207
 
        :raises: ConnectionError if it cannot connect.
208
 
 
209
 
        :rtype: paramiko.sftp_client.SFTPClient
210
 
        """
211
 
        raise NotImplementedError(self.connect_sftp)
212
 
 
213
 
    def connect_ssh(self, username, password, host, port, command):
214
 
        """Make an SSH connection.
215
 
        
216
 
        :returns: something with a `close` method, and a `get_filelike_channels`
217
 
            method that returns a pair of (read, write) filelike objects.
218
 
        """
219
 
        raise NotImplementedError(self.connect_ssh)
220
 
        
221
 
    def _raise_connection_error(self, host, port=None, orig_error=None,
222
 
                                msg='Unable to connect to SSH host'):
223
 
        """Raise a SocketConnectionError with properly formatted host.
224
 
 
225
 
        This just unifies all the locations that try to raise ConnectionError,
226
 
        so that they format things properly.
227
 
        """
228
 
        raise SocketConnectionError(host=host, port=port, msg=msg,
229
 
                                    orig_error=orig_error)
230
 
 
231
 
 
232
 
class LoopbackVendor(SSHVendor):
233
 
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
234
 
    
235
 
    def connect_sftp(self, username, password, host, port):
236
 
        sock = socket.socket()
237
 
        try:
238
 
            sock.connect((host, port))
239
 
        except socket.error, e:
240
 
            self._raise_connection_error(host, port=port, orig_error=e)
241
 
        return SFTPClient(LoopbackSFTP(sock))
242
 
 
243
 
register_ssh_vendor('loopback', LoopbackVendor())
244
 
 
245
 
 
246
 
class _ParamikoSSHConnection(object):
247
 
    def __init__(self, channel):
248
 
        self.channel = channel
249
 
 
250
 
    def get_filelike_channels(self):
251
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
252
 
 
253
 
    def close(self):
254
 
        return self.channel.close()
255
 
 
256
 
 
257
 
class ParamikoVendor(SSHVendor):
258
 
    """Vendor that uses paramiko."""
259
 
 
260
 
    def _connect(self, username, password, host, port):
261
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
262
 
        
263
 
        load_host_keys()
264
 
 
265
 
        try:
266
 
            t = paramiko.Transport((host, port or 22))
267
 
            t.set_log_channel('bzr.paramiko')
268
 
            t.start_client()
269
 
        except (paramiko.SSHException, socket.error), e:
270
 
            self._raise_connection_error(host, port=port, orig_error=e)
271
 
            
272
 
        server_key = t.get_remote_server_key()
273
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
274
 
        keytype = server_key.get_name()
275
 
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
276
 
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
277
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
278
 
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
279
 
            our_server_key = BZR_HOSTKEYS[host][keytype]
280
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
281
 
        else:
282
 
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
283
 
            add = getattr(BZR_HOSTKEYS, 'add', None)
284
 
            if add is not None: # paramiko >= 1.X.X
285
 
                BZR_HOSTKEYS.add(host, keytype, server_key)
286
 
            else:
287
 
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
288
 
            our_server_key = server_key
289
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
290
 
            save_host_keys()
291
 
        if server_key != our_server_key:
292
 
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
293
 
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
294
 
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
295
 
                (host, our_server_key_hex, server_key_hex),
296
 
                ['Try editing %s or %s' % (filename1, filename2)])
297
 
 
298
 
        _paramiko_auth(username, password, host, t)
299
 
        return t
300
 
        
301
 
    def connect_sftp(self, username, password, host, port):
302
 
        t = self._connect(username, password, host, port)
303
 
        try:
304
 
            return t.open_sftp_client()
305
 
        except paramiko.SSHException, e:
306
 
            self._raise_connection_error(host, port=port, orig_error=e,
307
 
                                         msg='Unable to start sftp client')
308
 
 
309
 
    def connect_ssh(self, username, password, host, port, command):
310
 
        t = self._connect(username, password, host, port)
311
 
        try:
312
 
            channel = t.open_session()
313
 
            cmdline = ' '.join(command)
314
 
            channel.exec_command(cmdline)
315
 
            return _ParamikoSSHConnection(channel)
316
 
        except paramiko.SSHException, e:
317
 
            self._raise_connection_error(host, port=port, orig_error=e,
318
 
                                         msg='Unable to invoke remote bzr')
319
 
 
320
 
if paramiko is not None:
321
 
    vendor = ParamikoVendor()
322
 
    register_ssh_vendor('paramiko', vendor)
323
 
    register_ssh_vendor('none', vendor)
324
 
    register_default_ssh_vendor(vendor)
325
 
    del vendor
326
 
 
327
 
 
328
 
class SubprocessVendor(SSHVendor):
329
 
    """Abstract base class for vendors that use pipes to a subprocess."""
330
 
    
331
 
    def _connect(self, argv):
332
 
        proc = subprocess.Popen(argv,
333
 
                                stdin=subprocess.PIPE,
334
 
                                stdout=subprocess.PIPE,
335
 
                                **os_specific_subprocess_params())
336
 
        return SSHSubprocess(proc)
337
 
 
338
 
    def connect_sftp(self, username, password, host, port):
339
 
        try:
340
 
            argv = self._get_vendor_specific_argv(username, host, port,
341
 
                                                  subsystem='sftp')
342
 
            sock = self._connect(argv)
343
 
            return SFTPClient(sock)
344
 
        except (EOFError, paramiko.SSHException), e:
345
 
            self._raise_connection_error(host, port=port, orig_error=e)
346
 
        except (OSError, IOError), e:
347
 
            # If the machine is fast enough, ssh can actually exit
348
 
            # before we try and send it the sftp request, which
349
 
            # raises a Broken Pipe
350
 
            if e.errno not in (errno.EPIPE,):
351
 
                raise
352
 
            self._raise_connection_error(host, port=port, orig_error=e)
353
 
 
354
 
    def connect_ssh(self, username, password, host, port, command):
355
 
        try:
356
 
            argv = self._get_vendor_specific_argv(username, host, port,
357
 
                                                  command=command)
358
 
            return self._connect(argv)
359
 
        except (EOFError), e:
360
 
            self._raise_connection_error(host, port=port, orig_error=e)
361
 
        except (OSError, IOError), e:
362
 
            # If the machine is fast enough, ssh can actually exit
363
 
            # before we try and send it the sftp request, which
364
 
            # raises a Broken Pipe
365
 
            if e.errno not in (errno.EPIPE,):
366
 
                raise
367
 
            self._raise_connection_error(host, port=port, orig_error=e)
368
 
 
369
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
370
 
                                  command=None):
371
 
        """Returns the argument list to run the subprocess with.
372
 
        
373
 
        Exactly one of 'subsystem' and 'command' must be specified.
374
 
        """
375
 
        raise NotImplementedError(self._get_vendor_specific_argv)
376
 
 
377
 
 
378
 
class OpenSSHSubprocessVendor(SubprocessVendor):
379
 
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
380
 
    
381
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
382
 
                                  command=None):
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',
389
 
                '-oForwardX11=no', '-oForwardAgent=no',
390
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
391
 
                '-oNoHostAuthenticationForLocalhost=yes']
392
 
        if port is not None:
393
 
            args.extend(['-p', str(port)])
394
 
        if username is not None:
395
 
            args.extend(['-l', username])
396
 
        if subsystem is not None:
397
 
            args.extend(['-s', host, subsystem])
398
 
        else:
399
 
            args.extend([host] + command)
400
 
        return args
401
 
 
402
 
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
403
 
 
404
 
 
405
 
class SSHCorpSubprocessVendor(SubprocessVendor):
406
 
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
407
 
 
408
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
409
 
                                  command=None):
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']
416
 
        if port is not None:
417
 
            args.extend(['-p', str(port)])
418
 
        if username is not None:
419
 
            args.extend(['-l', username])
420
 
        if subsystem is not None:
421
 
            args.extend(['-s', subsystem, host])
422
 
        else:
423
 
            args.extend([host] + command)
424
 
        return args
425
 
    
426
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
427
 
 
428
 
 
429
 
class PLinkSubprocessVendor(SubprocessVendor):
430
 
    """SSH vendor that uses the 'plink' executable from Putty."""
431
 
 
432
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
433
 
                                  command=None):
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']
440
 
        if port is not None:
441
 
            args.extend(['-P', str(port)])
442
 
        if username is not None:
443
 
            args.extend(['-l', username])
444
 
        if subsystem is not None:
445
 
            args.extend(['-s', host, subsystem])
446
 
        else:
447
 
            args.extend([host] + command)
448
 
        return args
449
 
 
450
 
register_ssh_vendor('plink', PLinkSubprocessVendor())
451
 
 
452
 
 
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
 
 
462
 
    if _use_ssh_agent:
463
 
        agent = paramiko.Agent()
464
 
        for key in agent.get_keys():
465
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
466
 
            try:
467
 
                paramiko_transport.auth_publickey(username, key)
468
 
                return
469
 
            except paramiko.SSHException, e:
470
 
                pass
471
 
    
472
 
    # okay, try finding id_rsa or id_dss?  (posix only)
473
 
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
474
 
        return
475
 
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
476
 
        return
477
 
 
478
 
    if password:
479
 
        try:
480
 
            paramiko_transport.auth_password(username, password)
481
 
            return
482
 
        except paramiko.SSHException, e:
483
 
            pass
484
 
 
485
 
    # give up and ask for a password
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)
494
 
 
495
 
 
496
 
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
497
 
    filename = os.path.expanduser('~/.ssh/' + filename)
498
 
    try:
499
 
        key = pkey_class.from_private_key_file(filename)
500
 
        paramiko_transport.auth_publickey(username, key)
501
 
        return True
502
 
    except paramiko.PasswordRequiredException:
503
 
        password = bzrlib.ui.ui_factory.get_password(
504
 
                prompt='SSH %(filename)s password',
505
 
                filename=filename)
506
 
        try:
507
 
            key = pkey_class.from_private_key_file(filename, password)
508
 
            paramiko_transport.auth_publickey(username, key)
509
 
            return True
510
 
        except paramiko.SSHException:
511
 
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
512
 
    except paramiko.SSHException:
513
 
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
514
 
    except IOError:
515
 
        pass
516
 
    return False
517
 
 
518
 
 
519
 
def load_host_keys():
520
 
    """
521
 
    Load system host keys (probably doesn't work on windows) and any
522
 
    "discovered" keys from previous sessions.
523
 
    """
524
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
525
 
    try:
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')
530
 
    try:
531
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
532
 
    except Exception, e:
533
 
        mutter('failed to load bzr host keys: ' + str(e))
534
 
        save_host_keys()
535
 
 
536
 
 
537
 
def save_host_keys():
538
 
    """
539
 
    Save "discovered" host keys in $(config)/ssh_host_keys/.
540
 
    """
541
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
542
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
543
 
    ensure_config_dir_exists()
544
 
 
545
 
    try:
546
 
        f = open(bzr_hostkey_path, 'w')
547
 
        f.write('# SSH host keys collected by bzr\n')
548
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
549
 
            for keytype, key in keys.iteritems():
550
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
551
 
        f.close()
552
 
    except IOError, e:
553
 
        mutter('failed to save bzr host keys: ' + str(e))
554
 
 
555
 
 
556
 
def os_specific_subprocess_params():
557
 
    """Get O/S specific subprocess parameters."""
558
 
    if sys.platform == 'win32':
559
 
        # setting the process group and closing fds is not supported on 
560
 
        # win32
561
 
        return {}
562
 
    else:
563
 
        # We close fds other than the pipes as the child process does not need 
564
 
        # them to be open.
565
 
        #
566
 
        # We also set the child process to ignore SIGINT.  Normally the signal
567
 
        # would be sent to every process in the foreground process group, but
568
 
        # this causes it to be seen only by bzr and not by ssh.  Python will
569
 
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
570
 
        # to release locks or do other cleanup over ssh before the connection
571
 
        # goes away.  
572
 
        # <https://launchpad.net/products/bzr/+bug/5987>
573
 
        #
574
 
        # Running it in a separate process group is not good because then it
575
 
        # can't get non-echoed input of a password or passphrase.
576
 
        # <https://launchpad.net/products/bzr/+bug/40508>
577
 
        return {'preexec_fn': _ignore_sigint,
578
 
                'close_fds': True,
579
 
                }
580
 
 
581
 
 
582
 
class SSHSubprocess(object):
583
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
584
 
 
585
 
    def __init__(self, proc):
586
 
        self.proc = proc
587
 
 
588
 
    def send(self, data):
589
 
        return os.write(self.proc.stdin.fileno(), data)
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
 
 
598
 
    def recv(self, count):
599
 
        return os.read(self.proc.stdout.fileno(), count)
600
 
 
601
 
    def close(self):
602
 
        self.proc.stdin.close()
603
 
        self.proc.stdout.close()
604
 
        self.proc.wait()
605
 
 
606
 
    def get_filelike_channels(self):
607
 
        return (self.proc.stdout, self.proc.stdin)
608