/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/remote.py

  • Committer: Aaron Bentley
  • Date: 2008-11-18 21:43:36 UTC
  • mto: This revision was merged to the branch mainline in revision 3840.
  • Revision ID: aaron@aaronbentley.com-20081118214336-53wku2p6og6kvl5r
Updates from review

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""RemoteTransport client for the smart-server.
 
18
 
 
19
This module shouldn't be accessed directly.  The classes defined here should be
 
20
imported from bzrlib.smart.
 
21
"""
 
22
 
 
23
__all__ = ['RemoteTransport', 'RemoteTCPTransport', 'RemoteSSHTransport']
 
24
 
 
25
from cStringIO import StringIO
 
26
 
 
27
from bzrlib import (
 
28
    config,
 
29
    debug,
 
30
    errors,
 
31
    remote,
 
32
    trace,
 
33
    transport,
 
34
    urlutils,
 
35
    )
 
36
from bzrlib.smart import client, medium
 
37
from bzrlib.symbol_versioning import (deprecated_method, one_four)
 
38
 
 
39
 
 
40
class _SmartStat(object):
 
41
 
 
42
    def __init__(self, size, mode):
 
43
        self.st_size = size
 
44
        self.st_mode = mode
 
45
 
 
46
 
 
47
class RemoteTransport(transport.ConnectedTransport):
 
48
    """Connection to a smart server.
 
49
 
 
50
    The connection holds references to the medium that can be used to send
 
51
    requests to the server.
 
52
 
 
53
    The connection has a notion of the current directory to which it's
 
54
    connected; this is incorporated in filenames passed to the server.
 
55
    
 
56
    This supports some higher-level RPC operations and can also be treated 
 
57
    like a Transport to do file-like operations.
 
58
 
 
59
    The connection can be made over a tcp socket, an ssh pipe or a series of
 
60
    http requests.  There are concrete subclasses for each type:
 
61
    RemoteTCPTransport, etc.
 
62
    """
 
63
 
 
64
    # When making a readv request, cap it at requesting 5MB of data
 
65
    _max_readv_bytes = 5*1024*1024
 
66
 
 
67
    # IMPORTANT FOR IMPLEMENTORS: RemoteTransport MUST NOT be given encoding
 
68
    # responsibilities: Put those on SmartClient or similar. This is vital for
 
69
    # the ability to support multiple versions of the smart protocol over time:
 
70
    # RemoteTransport is an adapter from the Transport object model to the 
 
71
    # SmartClient model, not an encoder.
 
72
 
 
73
    # FIXME: the medium parameter should be private, only the tests requires
 
74
    # it. It may be even clearer to define a TestRemoteTransport that handles
 
75
    # the specific cases of providing a _client and/or a _medium, and leave
 
76
    # RemoteTransport as an abstract class.
 
77
    def __init__(self, url, _from_transport=None, medium=None, _client=None):
 
78
        """Constructor.
 
79
 
 
80
        :param _from_transport: Another RemoteTransport instance that this
 
81
            one is being cloned from.  Attributes such as the medium will
 
82
            be reused.
 
83
 
 
84
        :param medium: The medium to use for this RemoteTransport.  If None,
 
85
            the medium from the _from_transport is shared.  If both this
 
86
            and _from_transport are None, a new medium will be built.
 
87
            _from_transport and medium cannot both be specified.
 
88
 
 
89
        :param _client: Override the _SmartClient used by this transport.  This
 
90
            should only be used for testing purposes; normally this is
 
91
            determined from the medium.
 
92
        """
 
93
        super(RemoteTransport, self).__init__(url,
 
94
                                              _from_transport=_from_transport)
 
95
 
 
96
        # The medium is the connection, except when we need to share it with
 
97
        # other objects (RemoteBzrDir, RemoteRepository etc). In these cases
 
98
        # what we want to share is really the shared connection.
 
99
 
 
100
        if _from_transport is None:
 
101
            # If no _from_transport is specified, we need to intialize the
 
102
            # shared medium.
 
103
            credentials = None
 
104
            if medium is None:
 
105
                medium, credentials = self._build_medium()
 
106
                if 'hpss' in debug.debug_flags:
 
107
                    trace.mutter('hpss: Built a new medium: %s',
 
108
                                 medium.__class__.__name__)
 
109
            self._shared_connection = transport._SharedConnection(medium,
 
110
                                                                  credentials,
 
111
                                                                  self.base)
 
112
        elif medium is None:
 
113
            # No medium was specified, so share the medium from the
 
114
            # _from_transport.
 
115
            medium = self._shared_connection.connection
 
116
        else:
 
117
            raise AssertionError(
 
118
                "Both _from_transport (%r) and medium (%r) passed to "
 
119
                "RemoteTransport.__init__, but these parameters are mutally "
 
120
                "exclusive." % (_from_transport, medium))
 
121
 
 
122
        if _client is None:
 
123
            self._client = client._SmartClient(medium)
 
124
        else:
 
125
            self._client = _client
 
126
 
 
127
    def _build_medium(self):
 
128
        """Create the medium if _from_transport does not provide one.
 
129
 
 
130
        The medium is analogous to the connection for ConnectedTransport: it
 
131
        allows connection sharing.
 
132
        """
 
133
        # No credentials
 
134
        return None, None
 
135
 
 
136
    def is_readonly(self):
 
137
        """Smart server transport can do read/write file operations."""
 
138
        try:
 
139
            resp = self._call2('Transport.is_readonly')
 
140
        except errors.UnknownSmartMethod:
 
141
            # XXX: nasty hack: servers before 0.16 don't have a
 
142
            # 'Transport.is_readonly' verb, so we do what clients before 0.16
 
143
            # did: assume False.
 
144
            return False
 
145
        if resp == ('yes', ):
 
146
            return True
 
147
        elif resp == ('no', ):
 
148
            return False
 
149
        else:
 
150
            raise errors.UnexpectedSmartServerResponse(resp)
 
151
 
 
152
    def get_smart_client(self):
 
153
        return self._get_connection()
 
154
 
 
155
    def get_smart_medium(self):
 
156
        return self._get_connection()
 
157
 
 
158
    @deprecated_method(one_four)
 
159
    def get_shared_medium(self):
 
160
        return self._get_shared_connection()
 
161
 
 
162
    def _remote_path(self, relpath):
 
163
        """Returns the Unicode version of the absolute path for relpath."""
 
164
        return self._combine_paths(self._path, relpath)
 
165
 
 
166
    def _call(self, method, *args):
 
167
        resp = self._call2(method, *args)
 
168
        self._ensure_ok(resp)
 
169
 
 
170
    def _call2(self, method, *args):
 
171
        """Call a method on the remote server."""
 
172
        try:
 
173
            return self._client.call(method, *args)
 
174
        except errors.ErrorFromSmartServer, err:
 
175
            self._translate_error(err)
 
176
 
 
177
    def _call_with_body_bytes(self, method, args, body):
 
178
        """Call a method on the remote server with body bytes."""
 
179
        try:
 
180
            return self._client.call_with_body_bytes(method, args, body)
 
181
        except errors.ErrorFromSmartServer, err:
 
182
            self._translate_error(err)
 
183
 
 
184
    def has(self, relpath):
 
185
        """Indicate whether a remote file of the given name exists or not.
 
186
 
 
187
        :see: Transport.has()
 
188
        """
 
189
        resp = self._call2('has', self._remote_path(relpath))
 
190
        if resp == ('yes', ):
 
191
            return True
 
192
        elif resp == ('no', ):
 
193
            return False
 
194
        else:
 
195
            raise errors.UnexpectedSmartServerResponse(resp)
 
196
 
 
197
    def get(self, relpath):
 
198
        """Return file-like object reading the contents of a remote file.
 
199
        
 
200
        :see: Transport.get_bytes()/get_file()
 
201
        """
 
202
        return StringIO(self.get_bytes(relpath))
 
203
 
 
204
    def get_bytes(self, relpath):
 
205
        remote = self._remote_path(relpath)
 
206
        try:
 
207
            resp, response_handler = self._client.call_expecting_body('get', remote)
 
208
        except errors.ErrorFromSmartServer, err:
 
209
            self._translate_error(err, relpath)
 
210
        if resp != ('ok', ):
 
211
            response_handler.cancel_read_body()
 
212
            raise errors.UnexpectedSmartServerResponse(resp)
 
213
        return response_handler.read_body_bytes()
 
214
 
 
215
    def _serialise_optional_mode(self, mode):
 
216
        if mode is None:
 
217
            return ''
 
218
        else:
 
219
            return '%d' % mode
 
220
 
 
221
    def mkdir(self, relpath, mode=None):
 
222
        resp = self._call2('mkdir', self._remote_path(relpath),
 
223
            self._serialise_optional_mode(mode))
 
224
 
 
225
    def open_write_stream(self, relpath, mode=None):
 
226
        """See Transport.open_write_stream."""
 
227
        self.put_bytes(relpath, "", mode)
 
228
        result = transport.AppendBasedFileStream(self, relpath)
 
229
        transport._file_streams[self.abspath(relpath)] = result
 
230
        return result
 
231
 
 
232
    def put_bytes(self, relpath, upload_contents, mode=None):
 
233
        # FIXME: upload_file is probably not safe for non-ascii characters -
 
234
        # should probably just pass all parameters as length-delimited
 
235
        # strings?
 
236
        if type(upload_contents) is unicode:
 
237
            # Although not strictly correct, we raise UnicodeEncodeError to be
 
238
            # compatible with other transports.
 
239
            raise UnicodeEncodeError(
 
240
                'undefined', upload_contents, 0, 1,
 
241
                'put_bytes must be given bytes, not unicode.')
 
242
        resp = self._call_with_body_bytes('put',
 
243
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
 
244
            upload_contents)
 
245
        self._ensure_ok(resp)
 
246
        return len(upload_contents)
 
247
 
 
248
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
249
                             create_parent_dir=False,
 
250
                             dir_mode=None):
 
251
        """See Transport.put_bytes_non_atomic."""
 
252
        # FIXME: no encoding in the transport!
 
253
        create_parent_str = 'F'
 
254
        if create_parent_dir:
 
255
            create_parent_str = 'T'
 
256
 
 
257
        resp = self._call_with_body_bytes(
 
258
            'put_non_atomic',
 
259
            (self._remote_path(relpath), self._serialise_optional_mode(mode),
 
260
             create_parent_str, self._serialise_optional_mode(dir_mode)),
 
261
            bytes)
 
262
        self._ensure_ok(resp)
 
263
 
 
264
    def put_file(self, relpath, upload_file, mode=None):
 
265
        # its not ideal to seek back, but currently put_non_atomic_file depends
 
266
        # on transports not reading before failing - which is a faulty
 
267
        # assumption I think - RBC 20060915
 
268
        pos = upload_file.tell()
 
269
        try:
 
270
            return self.put_bytes(relpath, upload_file.read(), mode)
 
271
        except:
 
272
            upload_file.seek(pos)
 
273
            raise
 
274
 
 
275
    def put_file_non_atomic(self, relpath, f, mode=None,
 
276
                            create_parent_dir=False,
 
277
                            dir_mode=None):
 
278
        return self.put_bytes_non_atomic(relpath, f.read(), mode=mode,
 
279
                                         create_parent_dir=create_parent_dir,
 
280
                                         dir_mode=dir_mode)
 
281
 
 
282
    def append_file(self, relpath, from_file, mode=None):
 
283
        return self.append_bytes(relpath, from_file.read(), mode)
 
284
        
 
285
    def append_bytes(self, relpath, bytes, mode=None):
 
286
        resp = self._call_with_body_bytes(
 
287
            'append',
 
288
            (self._remote_path(relpath), self._serialise_optional_mode(mode)),
 
289
            bytes)
 
290
        if resp[0] == 'appended':
 
291
            return int(resp[1])
 
292
        raise errors.UnexpectedSmartServerResponse(resp)
 
293
 
 
294
    def delete(self, relpath):
 
295
        resp = self._call2('delete', self._remote_path(relpath))
 
296
        self._ensure_ok(resp)
 
297
 
 
298
    def external_url(self):
 
299
        """See bzrlib.transport.Transport.external_url."""
 
300
        # the external path for RemoteTransports is the base
 
301
        return self.base
 
302
 
 
303
    def recommended_page_size(self):
 
304
        """Return the recommended page size for this transport."""
 
305
        return 64 * 1024
 
306
        
 
307
    def _readv(self, relpath, offsets):
 
308
        if not offsets:
 
309
            return
 
310
 
 
311
        offsets = list(offsets)
 
312
 
 
313
        sorted_offsets = sorted(offsets)
 
314
        coalesced = list(self._coalesce_offsets(sorted_offsets,
 
315
                               limit=self._max_readv_combine,
 
316
                               fudge_factor=self._bytes_to_read_before_seek))
 
317
 
 
318
        # now that we've coallesced things, avoid making enormous requests
 
319
        requests = []
 
320
        cur_request = []
 
321
        cur_len = 0
 
322
        for c in coalesced:
 
323
            if c.length + cur_len > self._max_readv_bytes:
 
324
                requests.append(cur_request)
 
325
                cur_request = [c]
 
326
                cur_len = c.length
 
327
                continue
 
328
            cur_request.append(c)
 
329
            cur_len += c.length
 
330
        if cur_request:
 
331
            requests.append(cur_request)
 
332
        if 'hpss' in debug.debug_flags:
 
333
            trace.mutter('%s.readv %s offsets => %s coalesced'
 
334
                         ' => %s requests (%s)',
 
335
                         self.__class__.__name__, len(offsets), len(coalesced),
 
336
                         len(requests), sum(map(len, requests)))
 
337
        # Cache the results, but only until they have been fulfilled
 
338
        data_map = {}
 
339
        # turn the list of offsets into a single stack to iterate
 
340
        offset_stack = iter(offsets)
 
341
        # using a list so it can be modified when passing down and coming back
 
342
        next_offset = [offset_stack.next()]
 
343
        for cur_request in requests:
 
344
            try:
 
345
                result = self._client.call_with_body_readv_array(
 
346
                    ('readv', self._remote_path(relpath),),
 
347
                    [(c.start, c.length) for c in cur_request])
 
348
                resp, response_handler = result
 
349
            except errors.ErrorFromSmartServer, err:
 
350
                self._translate_error(err)
 
351
 
 
352
            if resp[0] != 'readv':
 
353
                # This should raise an exception
 
354
                response_handler.cancel_read_body()
 
355
                raise errors.UnexpectedSmartServerResponse(resp)
 
356
 
 
357
            for res in self._handle_response(offset_stack, cur_request,
 
358
                                             response_handler,
 
359
                                             data_map,
 
360
                                             next_offset):
 
361
                yield res
 
362
 
 
363
    def _handle_response(self, offset_stack, coalesced, response_handler,
 
364
                         data_map, next_offset):
 
365
        cur_offset_and_size = next_offset[0]
 
366
        # FIXME: this should know how many bytes are needed, for clarity.
 
367
        data = response_handler.read_body_bytes()
 
368
        data_offset = 0
 
369
        for c_offset in coalesced:
 
370
            if len(data) < c_offset.length:
 
371
                raise errors.ShortReadvError(relpath, c_offset.start,
 
372
                            c_offset.length, actual=len(data))
 
373
            for suboffset, subsize in c_offset.ranges:
 
374
                key = (c_offset.start+suboffset, subsize)
 
375
                this_data = data[data_offset+suboffset:
 
376
                                 data_offset+suboffset+subsize]
 
377
                # Special case when the data is in-order, rather than packing
 
378
                # into a map and then back out again. Benchmarking shows that
 
379
                # this has 100% hit rate, but leave in the data_map work just
 
380
                # in case.
 
381
                # TODO: Could we get away with using buffer() to avoid the
 
382
                #       memory copy?  Callers would need to realize they may
 
383
                #       not have a real string.
 
384
                if key == cur_offset_and_size:
 
385
                    yield cur_offset_and_size[0], this_data
 
386
                    cur_offset_and_size = next_offset[0] = offset_stack.next()
 
387
                else:
 
388
                    data_map[key] = this_data
 
389
            data_offset += c_offset.length
 
390
 
 
391
            # Now that we've read some data, see if we can yield anything back
 
392
            while cur_offset_and_size in data_map:
 
393
                this_data = data_map.pop(cur_offset_and_size)
 
394
                yield cur_offset_and_size[0], this_data
 
395
                cur_offset_and_size = next_offset[0] = offset_stack.next()
 
396
 
 
397
    def rename(self, rel_from, rel_to):
 
398
        self._call('rename',
 
399
                   self._remote_path(rel_from),
 
400
                   self._remote_path(rel_to))
 
401
 
 
402
    def move(self, rel_from, rel_to):
 
403
        self._call('move',
 
404
                   self._remote_path(rel_from),
 
405
                   self._remote_path(rel_to))
 
406
 
 
407
    def rmdir(self, relpath):
 
408
        resp = self._call('rmdir', self._remote_path(relpath))
 
409
 
 
410
    def _ensure_ok(self, resp):
 
411
        if resp[0] != 'ok':
 
412
            raise errors.UnexpectedSmartServerResponse(resp)
 
413
        
 
414
    def _translate_error(self, err, orig_path=None):
 
415
        remote._translate_error(err, path=orig_path)
 
416
 
 
417
    def disconnect(self):
 
418
        self.get_smart_medium().disconnect()
 
419
 
 
420
    def stat(self, relpath):
 
421
        resp = self._call2('stat', self._remote_path(relpath))
 
422
        if resp[0] == 'stat':
 
423
            return _SmartStat(int(resp[1]), int(resp[2], 8))
 
424
        raise errors.UnexpectedSmartServerResponse(resp)
 
425
 
 
426
    ## def lock_read(self, relpath):
 
427
    ##     """Lock the given file for shared (read) access.
 
428
    ##     :return: A lock object, which should be passed to Transport.unlock()
 
429
    ##     """
 
430
    ##     # The old RemoteBranch ignore lock for reading, so we will
 
431
    ##     # continue that tradition and return a bogus lock object.
 
432
    ##     class BogusLock(object):
 
433
    ##         def __init__(self, path):
 
434
    ##             self.path = path
 
435
    ##         def unlock(self):
 
436
    ##             pass
 
437
    ##     return BogusLock(relpath)
 
438
 
 
439
    def listable(self):
 
440
        return True
 
441
 
 
442
    def list_dir(self, relpath):
 
443
        resp = self._call2('list_dir', self._remote_path(relpath))
 
444
        if resp[0] == 'names':
 
445
            return [name.encode('ascii') for name in resp[1:]]
 
446
        raise errors.UnexpectedSmartServerResponse(resp)
 
447
 
 
448
    def iter_files_recursive(self):
 
449
        resp = self._call2('iter_files_recursive', self._remote_path(''))
 
450
        if resp[0] == 'names':
 
451
            return resp[1:]
 
452
        raise errors.UnexpectedSmartServerResponse(resp)
 
453
 
 
454
 
 
455
class RemoteTCPTransport(RemoteTransport):
 
456
    """Connection to smart server over plain tcp.
 
457
    
 
458
    This is essentially just a factory to get 'RemoteTransport(url,
 
459
        SmartTCPClientMedium).
 
460
    """
 
461
 
 
462
    def _build_medium(self):
 
463
        client_medium = medium.SmartTCPClientMedium(
 
464
            self._host, self._port, self.base)
 
465
        return client_medium, None
 
466
 
 
467
 
 
468
class RemoteTCPTransportV2Only(RemoteTransport):
 
469
    """Connection to smart server over plain tcp with the client hard-coded to
 
470
    assume protocol v2 and remote server version <= 1.6.
 
471
 
 
472
    This should only be used for testing.
 
473
    """
 
474
 
 
475
    def _build_medium(self):
 
476
        client_medium = medium.SmartTCPClientMedium(
 
477
            self._host, self._port, self.base)
 
478
        client_medium._protocol_version = 2
 
479
        client_medium._remember_remote_is_before((1, 6))
 
480
        return client_medium, None
 
481
 
 
482
 
 
483
class RemoteSSHTransport(RemoteTransport):
 
484
    """Connection to smart server over SSH.
 
485
 
 
486
    This is essentially just a factory to get 'RemoteTransport(url,
 
487
        SmartSSHClientMedium).
 
488
    """
 
489
 
 
490
    def _build_medium(self):
 
491
        location_config = config.LocationConfig(self.base)
 
492
        bzr_remote_path = location_config.get_bzr_remote_path()
 
493
        user = self._user
 
494
        if user is None:
 
495
            auth = config.AuthenticationConfig()
 
496
            user = auth.get_user('ssh', self._host, self._port)
 
497
        client_medium = medium.SmartSSHClientMedium(self._host, self._port,
 
498
            user, self._password, self.base,
 
499
            bzr_remote_path=bzr_remote_path)
 
500
        return client_medium, (user, self._password)
 
501
 
 
502
 
 
503
class RemoteHTTPTransport(RemoteTransport):
 
504
    """Just a way to connect between a bzr+http:// url and http://.
 
505
    
 
506
    This connection operates slightly differently than the RemoteSSHTransport.
 
507
    It uses a plain http:// transport underneath, which defines what remote
 
508
    .bzr/smart URL we are connected to. From there, all paths that are sent are
 
509
    sent as relative paths, this way, the remote side can properly
 
510
    de-reference them, since it is likely doing rewrite rules to translate an
 
511
    HTTP path into a local path.
 
512
    """
 
513
 
 
514
    def __init__(self, base, _from_transport=None, http_transport=None):
 
515
        if http_transport is None:
 
516
            # FIXME: the password may be lost here because it appears in the
 
517
            # url only for an intial construction (when the url came from the
 
518
            # command-line).
 
519
            http_url = base[len('bzr+'):]
 
520
            self._http_transport = transport.get_transport(http_url)
 
521
        else:
 
522
            self._http_transport = http_transport
 
523
        super(RemoteHTTPTransport, self).__init__(
 
524
            base, _from_transport=_from_transport)
 
525
 
 
526
    def _build_medium(self):
 
527
        # We let http_transport take care of the credentials
 
528
        return self._http_transport.get_smart_medium(), None
 
529
 
 
530
    def _remote_path(self, relpath):
 
531
        """After connecting, HTTP Transport only deals in relative URLs."""
 
532
        # Adjust the relpath based on which URL this smart transport is
 
533
        # connected to.
 
534
        http_base = urlutils.normalize_url(self.get_smart_medium().base)
 
535
        url = urlutils.join(self.base[len('bzr+'):], relpath)
 
536
        url = urlutils.normalize_url(url)
 
537
        return urlutils.relative_url(http_base, url)
 
538
 
 
539
    def clone(self, relative_url):
 
540
        """Make a new RemoteHTTPTransport related to me.
 
541
 
 
542
        This is re-implemented rather than using the default
 
543
        RemoteTransport.clone() because we must be careful about the underlying
 
544
        http transport.
 
545
 
 
546
        Also, the cloned smart transport will POST to the same .bzr/smart
 
547
        location as this transport (although obviously the relative paths in the
 
548
        smart requests may be different).  This is so that the server doesn't
 
549
        have to handle .bzr/smart requests at arbitrary places inside .bzr
 
550
        directories, just at the initial URL the user uses.
 
551
        """
 
552
        if relative_url:
 
553
            abs_url = self.abspath(relative_url)
 
554
        else:
 
555
            abs_url = self.base
 
556
        return RemoteHTTPTransport(abs_url,
 
557
                                   _from_transport=self,
 
558
                                   http_transport=self._http_transport)
 
559
 
 
560
 
 
561
def get_test_permutations():
 
562
    """Return (transport, server) permutations for testing."""
 
563
    ### We may need a little more test framework support to construct an
 
564
    ### appropriate RemoteTransport in the future.
 
565
    from bzrlib.smart import server
 
566
    return [(RemoteTCPTransport, server.SmartTCPServer_for_testing)]