~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Martin Pool
  • Date: 2005-07-21 21:32:13 UTC
  • Revision ID: mbp@sourcefrog.net-20050721213213-c6ac0e8b06eaad0f
- bzr update-hashes shows some stats on what it did

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