/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Robert Collins
  • Date: 2009-04-24 00:45:11 UTC
  • mto: This revision was merged to the branch mainline in revision 4304.
  • Revision ID: robertc@robertcollins.net-20090424004511-8oszlwmvehlqwrla
Start building up a BzrDir.initialize_ex verb for the smart server.

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