~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

[merge] 0.7-bugfix: Fix fileid_involved to unescape xml characters, fix StubServer to handle paramiko > 1.5.2

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 LoopbackSFTP(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 send(self, data):
182
 
        return self.__socket.send(data)
183
 
 
184
 
    def recv(self, n):
185
 
        return self.__socket.recv(n)
186
 
 
187
 
    def recv_ready(self):
188
 
        return True
189
 
 
190
 
    def close(self):
191
 
        self.__socket.close()
192
 
 
193
 
 
194
 
class SSHVendor(object):
195
 
    """Abstract base class for SSH vendor implementations."""
196
 
 
197
 
    def connect_sftp(self, username, password, host, port):
198
 
        """Make an SSH connection, and return an SFTPClient.
199
 
        
200
 
        :param username: an ascii string
201
 
        :param password: an ascii string
202
 
        :param host: a host name as an ascii string
203
 
        :param port: a port number
204
 
        :type port: int
205
 
 
206
 
        :raises: ConnectionError if it cannot connect.
207
 
 
208
 
        :rtype: paramiko.sftp_client.SFTPClient
209
 
        """
210
 
        raise NotImplementedError(self.connect_sftp)
211
 
 
212
 
    def connect_ssh(self, username, password, host, port, command):
213
 
        """Make an SSH connection.
214
 
        
215
 
        :returns: something with a `close` method, and a `get_filelike_channels`
216
 
            method that returns a pair of (read, write) filelike objects.
217
 
        """
218
 
        raise NotImplementedError(self.connect_ssh)
219
 
 
220
 
    def _raise_connection_error(self, host, port=None, orig_error=None,
221
 
                                msg='Unable to connect to SSH host'):
222
 
        """Raise a SocketConnectionError with properly formatted host.
223
 
 
224
 
        This just unifies all the locations that try to raise ConnectionError,
225
 
        so that they format things properly.
226
 
        """
227
 
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
228
 
                                           orig_error=orig_error)
229
 
 
230
 
 
231
 
class LoopbackVendor(SSHVendor):
232
 
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
233
 
 
234
 
    def connect_sftp(self, username, password, host, port):
235
 
        sock = socket.socket()
236
 
        try:
237
 
            sock.connect((host, port))
238
 
        except socket.error, e:
239
 
            self._raise_connection_error(host, port=port, orig_error=e)
240
 
        return SFTPClient(LoopbackSFTP(sock))
241
 
 
242
 
register_ssh_vendor('loopback', LoopbackVendor())
243
 
 
244
 
 
245
 
class _ParamikoSSHConnection(object):
246
 
    def __init__(self, channel):
247
 
        self.channel = channel
248
 
 
249
 
    def get_filelike_channels(self):
250
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
251
 
 
252
 
    def close(self):
253
 
        return self.channel.close()
254
 
 
255
 
 
256
 
class ParamikoVendor(SSHVendor):
257
 
    """Vendor that uses paramiko."""
258
 
 
259
 
    def _connect(self, username, password, host, port):
260
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
261
 
 
262
 
        load_host_keys()
263
 
 
264
 
        try:
265
 
            t = paramiko.Transport((host, port or 22))
266
 
            t.set_log_channel('bzr.paramiko')
267
 
            t.start_client()
268
 
        except (paramiko.SSHException, socket.error), e:
269
 
            self._raise_connection_error(host, port=port, orig_error=e)
270
 
 
271
 
        server_key = t.get_remote_server_key()
272
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
273
 
        keytype = server_key.get_name()
274
 
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
275
 
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
276
 
            our_server_key_hex = paramiko.util.hexify(
277
 
                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(
281
 
                our_server_key.get_fingerprint())
282
 
        else:
283
 
            trace.warning('Adding %s host key for %s: %s'
284
 
                          % (keytype, host, server_key_hex))
285
 
            add = getattr(BZR_HOSTKEYS, 'add', None)
286
 
            if add is not None: # paramiko >= 1.X.X
287
 
                BZR_HOSTKEYS.add(host, keytype, server_key)
288
 
            else:
289
 
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
290
 
            our_server_key = server_key
291
 
            our_server_key_hex = paramiko.util.hexify(
292
 
                our_server_key.get_fingerprint())
293
 
            save_host_keys()
294
 
        if server_key != our_server_key:
295
 
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
296
 
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
297
 
            raise errors.TransportError(
298
 
                'Host keys for %s do not match!  %s != %s' %
299
 
                (host, our_server_key_hex, server_key_hex),
300
 
                ['Try editing %s or %s' % (filename1, filename2)])
301
 
 
302
 
        _paramiko_auth(username, password, host, port, t)
303
 
        return t
304
 
 
305
 
    def connect_sftp(self, username, password, host, port):
306
 
        t = self._connect(username, password, host, port)
307
 
        try:
308
 
            return t.open_sftp_client()
309
 
        except paramiko.SSHException, e:
310
 
            self._raise_connection_error(host, port=port, orig_error=e,
311
 
                                         msg='Unable to start sftp client')
312
 
 
313
 
    def connect_ssh(self, username, password, host, port, command):
314
 
        t = self._connect(username, password, host, port)
315
 
        try:
316
 
            channel = t.open_session()
317
 
            cmdline = ' '.join(command)
318
 
            channel.exec_command(cmdline)
319
 
            return _ParamikoSSHConnection(channel)
320
 
        except paramiko.SSHException, e:
321
 
            self._raise_connection_error(host, port=port, orig_error=e,
322
 
                                         msg='Unable to invoke remote bzr')
323
 
 
324
 
if paramiko is not None:
325
 
    vendor = ParamikoVendor()
326
 
    register_ssh_vendor('paramiko', vendor)
327
 
    register_ssh_vendor('none', vendor)
328
 
    register_default_ssh_vendor(vendor)
329
 
    del vendor
330
 
 
331
 
 
332
 
class SubprocessVendor(SSHVendor):
333
 
    """Abstract base class for vendors that use pipes to a subprocess."""
334
 
 
335
 
    def _connect(self, argv):
336
 
        proc = subprocess.Popen(argv,
337
 
                                stdin=subprocess.PIPE,
338
 
                                stdout=subprocess.PIPE,
339
 
                                **os_specific_subprocess_params())
340
 
        return SSHSubprocess(proc)
341
 
 
342
 
    def connect_sftp(self, username, password, host, port):
343
 
        try:
344
 
            argv = self._get_vendor_specific_argv(username, host, port,
345
 
                                                  subsystem='sftp')
346
 
            sock = self._connect(argv)
347
 
            return SFTPClient(sock)
348
 
        except (EOFError, paramiko.SSHException), e:
349
 
            self._raise_connection_error(host, port=port, orig_error=e)
350
 
        except (OSError, IOError), e:
351
 
            # If the machine is fast enough, ssh can actually exit
352
 
            # before we try and send it the sftp request, which
353
 
            # raises a Broken Pipe
354
 
            if e.errno not in (errno.EPIPE,):
355
 
                raise
356
 
            self._raise_connection_error(host, port=port, orig_error=e)
357
 
 
358
 
    def connect_ssh(self, username, password, host, port, command):
359
 
        try:
360
 
            argv = self._get_vendor_specific_argv(username, host, port,
361
 
                                                  command=command)
362
 
            return self._connect(argv)
363
 
        except (EOFError), e:
364
 
            self._raise_connection_error(host, port=port, orig_error=e)
365
 
        except (OSError, IOError), e:
366
 
            # If the machine is fast enough, ssh can actually exit
367
 
            # before we try and send it the sftp request, which
368
 
            # raises a Broken Pipe
369
 
            if e.errno not in (errno.EPIPE,):
370
 
                raise
371
 
            self._raise_connection_error(host, port=port, orig_error=e)
372
 
 
373
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
374
 
                                  command=None):
375
 
        """Returns the argument list to run the subprocess with.
376
 
        
377
 
        Exactly one of 'subsystem' and 'command' must be specified.
378
 
        """
379
 
        raise NotImplementedError(self._get_vendor_specific_argv)
380
 
 
381
 
 
382
 
class OpenSSHSubprocessVendor(SubprocessVendor):
383
 
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
384
 
 
385
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
386
 
                                  command=None):
387
 
        assert subsystem is not None or command is not None, (
388
 
            'Must specify a command or subsystem')
389
 
        if subsystem is not None:
390
 
            assert command is None, (
391
 
                'subsystem and command are mutually exclusive')
392
 
        args = ['ssh',
393
 
                '-oForwardX11=no', '-oForwardAgent=no',
394
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
395
 
                '-oNoHostAuthenticationForLocalhost=yes']
396
 
        if port is not None:
397
 
            args.extend(['-p', str(port)])
398
 
        if username is not None:
399
 
            args.extend(['-l', username])
400
 
        if subsystem is not None:
401
 
            args.extend(['-s', host, subsystem])
402
 
        else:
403
 
            args.extend([host] + command)
404
 
        return args
405
 
 
406
 
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
407
 
 
408
 
 
409
 
class SSHCorpSubprocessVendor(SubprocessVendor):
410
 
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
411
 
 
412
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
413
 
                                  command=None):
414
 
        assert subsystem is not None or command is not None, (
415
 
            'Must specify a command or subsystem')
416
 
        if subsystem is not None:
417
 
            assert command is None, (
418
 
                'subsystem and command are mutually exclusive')
419
 
        args = ['ssh', '-x']
420
 
        if port is not None:
421
 
            args.extend(['-p', str(port)])
422
 
        if username is not None:
423
 
            args.extend(['-l', username])
424
 
        if subsystem is not None:
425
 
            args.extend(['-s', subsystem, host])
426
 
        else:
427
 
            args.extend([host] + command)
428
 
        return args
429
 
 
430
 
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
431
 
 
432
 
 
433
 
class PLinkSubprocessVendor(SubprocessVendor):
434
 
    """SSH vendor that uses the 'plink' executable from Putty."""
435
 
 
436
 
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
437
 
                                  command=None):
438
 
        assert subsystem is not None or command is not None, (
439
 
            'Must specify a command or subsystem')
440
 
        if subsystem is not None:
441
 
            assert command is None, (
442
 
                'subsystem and command are mutually exclusive')
443
 
        args = ['plink', '-x', '-a', '-ssh', '-2']
444
 
        if port is not None:
445
 
            args.extend(['-P', str(port)])
446
 
        if username is not None:
447
 
            args.extend(['-l', username])
448
 
        if subsystem is not None:
449
 
            args.extend(['-s', host, subsystem])
450
 
        else:
451
 
            args.extend([host] + command)
452
 
        return args
453
 
 
454
 
register_ssh_vendor('plink', PLinkSubprocessVendor())
455
 
 
456
 
 
457
 
def _paramiko_auth(username, password, host, port, paramiko_transport):
458
 
    # paramiko requires a username, but it might be none if nothing was supplied
459
 
    # use the local username, just in case.
460
 
    # We don't override username, because if we aren't using paramiko,
461
 
    # the username might be specified in ~/.ssh/config and we don't want to
462
 
    # force it to something else
463
 
    # Also, it would mess up the self.relpath() functionality
464
 
    auth = config.AuthenticationConfig()
465
 
    if username is None:
466
 
        username = auth.get_user('ssh', host, port=port)
467
 
        if username is None:
468
 
            # Default to local user
469
 
            username = getpass.getuser()
470
 
 
471
 
    if _use_ssh_agent:
472
 
        agent = paramiko.Agent()
473
 
        for key in agent.get_keys():
474
 
            trace.mutter('Trying SSH agent key %s'
475
 
                         % paramiko.util.hexify(key.get_fingerprint()))
476
 
            try:
477
 
                paramiko_transport.auth_publickey(username, key)
478
 
                return
479
 
            except paramiko.SSHException, e:
480
 
                pass
481
 
 
482
 
    # okay, try finding id_rsa or id_dss?  (posix only)
483
 
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
484
 
        return
485
 
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
486
 
        return
487
 
 
488
 
    if password:
489
 
        try:
490
 
            paramiko_transport.auth_password(username, password)
491
 
            return
492
 
        except paramiko.SSHException, e:
493
 
            pass
494
 
 
495
 
    # give up and ask for a password
496
 
    password = auth.get_password('ssh', host, username, port=port)
497
 
    try:
498
 
        paramiko_transport.auth_password(username, password)
499
 
    except paramiko.SSHException, e:
500
 
        raise errors.ConnectionError(
501
 
            'Unable to authenticate to SSH host as %s@%s' % (username, host), e)
502
 
 
503
 
 
504
 
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
505
 
    filename = os.path.expanduser('~/.ssh/' + filename)
506
 
    try:
507
 
        key = pkey_class.from_private_key_file(filename)
508
 
        paramiko_transport.auth_publickey(username, key)
509
 
        return True
510
 
    except paramiko.PasswordRequiredException:
511
 
        password = ui.ui_factory.get_password(
512
 
            prompt='SSH %(filename)s password', filename=filename)
513
 
        try:
514
 
            key = pkey_class.from_private_key_file(filename, password)
515
 
            paramiko_transport.auth_publickey(username, key)
516
 
            return True
517
 
        except paramiko.SSHException:
518
 
            trace.mutter('SSH authentication via %s key failed.'
519
 
                         % (os.path.basename(filename),))
520
 
    except paramiko.SSHException:
521
 
        trace.mutter('SSH authentication via %s key failed.'
522
 
                     % (os.path.basename(filename),))
523
 
    except IOError:
524
 
        pass
525
 
    return False
526
 
 
527
 
 
528
 
def load_host_keys():
529
 
    """
530
 
    Load system host keys (probably doesn't work on windows) and any
531
 
    "discovered" keys from previous sessions.
532
 
    """
533
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
534
 
    try:
535
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
536
 
            os.path.expanduser('~/.ssh/known_hosts'))
537
 
    except IOError, e:
538
 
        trace.mutter('failed to load system host keys: ' + str(e))
539
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
540
 
    try:
541
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
542
 
    except IOError, e:
543
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
544
 
        save_host_keys()
545
 
 
546
 
 
547
 
def save_host_keys():
548
 
    """
549
 
    Save "discovered" host keys in $(config)/ssh_host_keys/.
550
 
    """
551
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
552
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
553
 
    config.ensure_config_dir_exists()
554
 
 
555
 
    try:
556
 
        f = open(bzr_hostkey_path, 'w')
557
 
        f.write('# SSH host keys collected by bzr\n')
558
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
559
 
            for keytype, key in keys.iteritems():
560
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
561
 
        f.close()
562
 
    except IOError, e:
563
 
        trace.mutter('failed to save bzr host keys: ' + str(e))
564
 
 
565
 
 
566
 
def os_specific_subprocess_params():
567
 
    """Get O/S specific subprocess parameters."""
568
 
    if sys.platform == 'win32':
569
 
        # setting the process group and closing fds is not supported on 
570
 
        # win32
571
 
        return {}
572
 
    else:
573
 
        # We close fds other than the pipes as the child process does not need 
574
 
        # them to be open.
575
 
        #
576
 
        # We also set the child process to ignore SIGINT.  Normally the signal
577
 
        # would be sent to every process in the foreground process group, but
578
 
        # this causes it to be seen only by bzr and not by ssh.  Python will
579
 
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
580
 
        # to release locks or do other cleanup over ssh before the connection
581
 
        # goes away.  
582
 
        # <https://launchpad.net/products/bzr/+bug/5987>
583
 
        #
584
 
        # Running it in a separate process group is not good because then it
585
 
        # can't get non-echoed input of a password or passphrase.
586
 
        # <https://launchpad.net/products/bzr/+bug/40508>
587
 
        return {'preexec_fn': _ignore_sigint,
588
 
                'close_fds': True,
589
 
                }
590
 
 
591
 
 
592
 
class SSHSubprocess(object):
593
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
594
 
 
595
 
    def __init__(self, proc):
596
 
        self.proc = proc
597
 
 
598
 
    def send(self, data):
599
 
        return os.write(self.proc.stdin.fileno(), data)
600
 
 
601
 
    def recv_ready(self):
602
 
        # TODO: jam 20051215 this function is necessary to support the
603
 
        # pipelined() function. In reality, it probably should use
604
 
        # poll() or select() to actually return if there is data
605
 
        # available, otherwise we probably don't get any benefit
606
 
        return True
607
 
 
608
 
    def recv(self, count):
609
 
        return os.read(self.proc.stdout.fileno(), count)
610
 
 
611
 
    def close(self):
612
 
        self.proc.stdin.close()
613
 
        self.proc.stdout.close()
614
 
        self.proc.wait()
615
 
 
616
 
    def get_filelike_channels(self):
617
 
        return (self.proc.stdout, self.proc.stdin)
618