1
# Copyright (C) 2006 Canonical Ltd
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.
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.
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
17
"""Smart-server protocol, client and server.
19
Requests are sent as a command and list of arguments, followed by optional
20
bulk body data. Responses are similarly a response and list of arguments,
21
followed by bulk body data. ::
24
Fields are separated by Ctrl-A.
25
BULK_DATA := CHUNK+ TRAILER
26
Chunks can be repeated as many times as necessary.
27
CHUNK := CHUNK_LEN CHUNK_BODY
28
CHUNK_LEN := DIGIT+ NEWLINE
29
Gives the number of bytes in the following chunk.
30
CHUNK_BODY := BYTE[chunk_len]
31
TRAILER := SUCCESS_TRAILER | ERROR_TRAILER
32
SUCCESS_TRAILER := 'done' NEWLINE
35
Paths are passed across the network. The client needs to see a namespace that
36
includes any repository that might need to be referenced, and the client needs
37
to know about a root directory beyond which it cannot ascend.
39
Servers run over ssh will typically want to be able to access any path the user
40
can access. Public servers on the other hand (which might be over http, ssh
41
or tcp) will typically want to restrict access to only a particular directory
42
and its children, so will want to do a software virtual root at that level.
43
In other words they'll want to rewrite incoming paths to be under that level
44
(and prevent escaping using ../ tricks.)
46
URLs that include ~ should probably be passed across to the server verbatim
47
and the server can expand them. This will proably not be meaningful when
48
limited to a directory?
53
# TODO: A plain integer from query_version is too simple; should give some
56
# TODO: Server should probably catch exceptions within itself and send them
57
# back across the network. (But shouldn't catch KeyboardInterrupt etc)
58
# Also needs to somehow report protocol errors like bad requests. Need to
59
# consider how we'll handle error reporting, e.g. if we get halfway through a
60
# bulk transfer and then something goes wrong.
62
# TODO: Standard marker at start of request/response lines?
64
# TODO: Make each request and response self-validatable, e.g. with checksums.
66
# TODO: get/put objects could be changed to gradually read back the data as it
67
# comes across the network
69
# TODO: What should the server do if it hits an error and has to terminate?
71
# TODO: is it useful to allow multiple chunks in the bulk data?
73
# TODO: If we get an exception during transmission of bulk data we can't just
74
# emit the exception because it won't be seen.
75
# John proposes: I think it would be worthwhile to have a header on each
76
# chunk, that indicates it is another chunk. Then you can send an 'error'
77
# chunk as long as you finish the previous chunk.
79
# TODO: Clone method on Transport; should work up towards parent directory;
80
# unclear how this should be stored or communicated to the server... maybe
81
# just pass it on all relevant requests?
83
# TODO: Better name than clone() for changing between directories. How about
84
# open_dir or change_dir or chdir?
86
# TODO: Is it really good to have the notion of current directory within the
87
# connection? Perhaps all Transports should factor out a common connection
88
# from the thing that has the directory context?
90
# TODO: Pull more things common to sftp and ssh to a higher level.
92
# TODO: The server that manages a connection should be quite small and retain
93
# minimum state because each of the requests are supposed to be stateless.
94
# Then we can write another implementation that maps to http.
96
# TODO: What to do when a client connection is garbage collected? Maybe just
97
# abruptly drop the connection?
99
# TODO: Server in some cases will need to restrict access to files outside of
100
# a particular root directory. LocalTransport doesn't do anything to stop you
101
# ascending above the base directory, so we need to prevent paths
102
# containing '..' in either the server or transport layers. (Also need to
103
# consider what happens if someone creates a symlink pointing outside the
106
# TODO: Server should rebase absolute paths coming across the network to put
107
# them under the virtual root, if one is in use. LocalTransport currently
108
# doesn't do that; if you give it an absolute path it just uses it.
110
# XXX: Arguments can't contain newlines or ascii; possibly we should e.g.
111
# urlescape them instead. Indeed possibly this should just literally be
114
# FIXME: This transport, with several others, has imperfect handling of paths
115
# within urls. It'd probably be better for ".." from a root to raise an error
116
# rather than return the same directory as we do at present.
118
# TODO: Rather than working at the Transport layer we want a Branch,
119
# Repository or BzrDir objects that talk to a server.
121
# TODO: Probably want some way for server commands to gradually produce body
122
# data rather than passing it as a string; they could perhaps pass an
123
# iterator-like callback that will gradually yield data; it probably needs a
124
# close() method that will always be closed to do any necessary cleanup.
126
# TODO: Split the actual smart server from the ssh encoding of it.
128
# TODO: Perhaps support file-level readwrite operations over the transport
131
# TODO: SmartBzrDir class, proxying all Branch etc methods across to another
132
# branch doing file-level operations.
134
# TODO: jam 20060915 _decode_tuple is acting directly on input over
135
# the socket, and it assumes everything is UTF8 sections separated
136
# by \001. Which means a request like '\002' Will abort the connection
137
# because of a UnicodeDecodeError. It does look like invalid data will
138
# kill the SmartStreamServer, but only with an abort + exception, and
139
# the overall server shouldn't die.
141
from cStringIO import StringIO
159
from bzrlib.bundle.serializer import write_bundle
160
from bzrlib.trace import mutter
161
from bzrlib.transport import local
163
# must do this otherwise urllib can't parse the urls properly :(
164
for scheme in ['ssh', 'bzr', 'bzr+loopback', 'bzr+ssh']:
165
transport.register_urlparse_netloc_protocol(scheme)
169
def _recv_tuple(from_file):
170
req_line = from_file.readline()
171
return _decode_tuple(req_line)
174
def _decode_tuple(req_line):
175
if req_line == None or req_line == '':
177
if req_line[-1] != '\n':
178
raise errors.SmartProtocolError("request %r not terminated" % req_line)
179
return tuple((a.decode('utf-8') for a in req_line[:-1].split('\x01')))
182
def _send_tuple(to_file, args):
183
# XXX: this will be inefficient. Just ask Robert.
184
to_file.write('\x01'.join((a.encode('utf-8') for a in args)) + '\n')
188
class SmartProtocolBase(object):
189
"""Methods common to client and server"""
191
def _send_bulk_data(self, body):
192
"""Send chunked body data"""
193
assert isinstance(body, str)
194
self._out.write('%d\n' % len(body))
195
self._out.write(body)
196
self._out.write('done\n')
199
# TODO: this only actually accomodates a single block; possibly should support
201
def _recv_bulk(self):
202
chunk_len = self._in.readline()
204
chunk_len = int(chunk_len)
206
raise errors.SmartProtocolError("bad chunk length line %r" % chunk_len)
207
bulk = self._in.read(chunk_len)
208
if len(bulk) != chunk_len:
209
raise errors.SmartProtocolError("short read fetching bulk data chunk")
213
def _recv_tuple(self):
214
return _recv_tuple(self._in)
216
def _recv_trailer(self):
217
resp = self._recv_tuple()
218
if resp == ('done', ):
221
self._translate_error(resp)
224
class SmartStreamServer(SmartProtocolBase):
225
"""Handles smart commands coming over a stream.
227
The stream may be a pipe connected to sshd, or a tcp socket, or an
228
in-process fifo for testing.
230
One instance is created for each connected client; it can serve multiple
231
requests in the lifetime of the connection.
233
The server passes requests through to an underlying backing transport,
234
which will typically be a LocalTransport looking at the server's filesystem.
237
def __init__(self, in_file, out_file, backing_transport):
238
"""Construct new server.
240
:param in_file: Python file from which requests can be read.
241
:param out_file: Python file to write responses.
242
:param backing_transport: Transport for the directory served.
246
self.smart_server = SmartServer(backing_transport)
247
# server can call back to us to get bulk data - this is not really
248
# ideal, they should get it per request instead
249
self.smart_server._recv_body = self._recv_bulk
251
def _recv_tuple(self):
252
"""Read a request from the client and return as a tuple.
254
Returns None at end of file (if the client closed the connection.)
256
return _recv_tuple(self._in)
258
def _send_tuple(self, args):
259
"""Send response header"""
260
return _send_tuple(self._out, args)
262
def _send_error_and_disconnect(self, exception):
263
self._send_tuple(('error', str(exception)))
268
def _serve_one_request(self):
269
"""Read one request from input, process, send back a response.
271
:return: False if the server should terminate, otherwise None.
273
req_args = self._recv_tuple()
274
mutter('server received %r' % (req_args,))
276
# client closed connection
277
return False # shutdown server
279
response = self.smart_server.dispatch_command(req_args[0], req_args[1:])
280
mutter('server sending %r' % (response.args,))
281
self._send_tuple(response.args)
282
if response.body is not None:
283
self._send_bulk_data(response.body)
284
except KeyboardInterrupt:
287
# everything else: pass to client, flush, and quit
288
self._send_error_and_disconnect(e)
292
"""Serve requests until the client disconnects."""
293
# Keep a reference to stderr because the sys module's globals get set to
294
# None during interpreter shutdown.
295
from sys import stderr
297
while self._serve_one_request() != False:
300
stderr.write("%s terminating on exception %s\n" % (self, e))
304
class SmartServerResponse(object):
305
"""Response generated by SmartServer."""
307
def __init__(self, args, body=None):
312
class SmartServer(object):
313
"""Protocol logic for smart server.
315
This doesn't handle serialization at all, it just processes requests and
319
# TODO: Better way of representing the body for commands that take it,
320
# and allow it to be streamed into the server.
322
def __init__(self, backing_transport):
323
self._backing_transport = backing_transport
326
"""Answer a version request with my version."""
327
return SmartServerResponse(('ok', '1'))
329
def do_has(self, relpath):
330
r = self._backing_transport.has(relpath) and 'yes' or 'no'
331
return SmartServerResponse((r,))
333
def do_get(self, relpath):
334
backing_bytes = self._backing_transport.get_bytes(relpath)
335
return SmartServerResponse(('ok',), backing_bytes)
337
def _deserialise_optional_mode(self, mode):
343
def do_append(self, relpath, mode):
344
old_length = self._backing_transport.append_bytes(
345
relpath, self._recv_body(), self._deserialise_optional_mode(mode))
346
return SmartServerResponse(('appended', '%d' % old_length))
348
def do_delete(self, relpath):
349
self._backing_transport.delete(relpath)
351
def do_iter_files_recursive(self, abspath):
352
# XXX: the path handling needs some thought.
353
#relpath = self._backing_transport.relpath(abspath)
354
transport = self._backing_transport.clone(abspath)
355
filenames = transport.iter_files_recursive()
356
return SmartServerResponse(('names',) + tuple(filenames))
358
def do_list_dir(self, relpath):
359
filenames = self._backing_transport.list_dir(relpath)
360
return SmartServerResponse(('names',) + tuple(filenames))
362
def do_mkdir(self, relpath, mode):
363
self._backing_transport.mkdir(relpath,
364
self._deserialise_optional_mode(mode))
366
def do_move(self, rel_from, rel_to):
367
self._backing_transport.move(rel_from, rel_to)
369
def do_put(self, relpath, mode):
370
self._backing_transport.put_bytes(relpath,
372
self._deserialise_optional_mode(mode))
374
def do_rename(self, rel_from, rel_to):
375
self._backing_transport.rename(rel_from, rel_to)
377
def do_rmdir(self, relpath):
378
self._backing_transport.rmdir(relpath)
380
def do_stat(self, relpath):
381
stat = self._backing_transport.stat(relpath)
382
return SmartServerResponse(('stat', str(stat.st_size), oct(stat.st_mode)))
384
def do_get_bundle(self, path, revision_id):
385
# open transport relative to our base
386
t = self._backing_transport.clone(path)
387
control, extra_path = bzrdir.BzrDir.open_containing_from_transport(t)
388
repo = control.open_repository()
389
tmpf = tempfile.TemporaryFile()
390
base_revision = revision.NULL_REVISION
391
write_bundle(repo, revision_id, base_revision, tmpf)
393
return SmartServerResponse((), tmpf.read())
395
def dispatch_command(self, cmd, args):
396
func = getattr(self, 'do_' + cmd, None)
398
raise errors.SmartProtocolError("bad request %r" % (cmd,))
402
result = SmartServerResponse(('ok',))
404
except errors.NoSuchFile, e:
405
return SmartServerResponse(('NoSuchFile', e.path))
406
except errors.FileExists, e:
407
return SmartServerResponse(('FileExists', e.path))
408
except errors.DirectoryNotEmpty, e:
409
return SmartServerResponse(('DirectoryNotEmpty', e.path))
410
except UnicodeError, e:
411
# If it is a DecodeError, than most likely we are starting
412
# with a plain string
413
str_or_unicode = e.object
414
if isinstance(str_or_unicode, unicode):
415
val = u'u:' + str_or_unicode
417
val = u's:' + str_or_unicode.encode('base64')
418
# This handles UnicodeEncodeError or UnicodeDecodeError
419
return SmartServerResponse((e.__class__.__name__,
420
e.encoding, val, str(e.start), str(e.end), e.reason))
423
class SmartTCPServer(object):
424
"""Listens on a TCP socket and accepts connections from smart clients"""
426
def __init__(self, backing_transport=None, host='127.0.0.1', port=0):
427
"""Construct a new server.
429
To actually start it running, call either start_background_thread or
432
:param host: Name of the interface to listen on.
433
:param port: TCP port to listen on, or 0 to allocate a transient port.
435
if backing_transport is None:
436
backing_transport = memory.MemoryTransport()
437
self._server_socket = socket.socket()
438
self._server_socket.bind((host, port))
439
self.port = self._server_socket.getsockname()[1]
440
self._server_socket.listen(1)
441
self._server_socket.settimeout(1)
442
self.backing_transport = backing_transport
445
# let connections timeout so that we get a chance to terminate
446
# Keep a reference to the exceptions we want to catch because the socket
447
# module's globals get set to None during interpreter shutdown.
448
from socket import timeout as socket_timeout
449
from socket import error as socket_error
450
self._should_terminate = False
451
while not self._should_terminate:
453
self.accept_and_serve()
454
except socket_timeout:
455
# just check if we're asked to stop
457
except socket_error, e:
458
trace.warning("client disconnected: %s", e)
462
"""Return the url of the server"""
463
return "bzr://%s:%d/" % self._server_socket.getsockname()
465
def accept_and_serve(self):
466
conn, client_addr = self._server_socket.accept()
467
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
468
from_client = conn.makefile('r')
469
to_client = conn.makefile('w')
470
handler = SmartStreamServer(from_client, to_client,
471
self.backing_transport)
472
connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
473
connection_thread.setDaemon(True)
474
connection_thread.start()
476
def start_background_thread(self):
477
self._server_thread = threading.Thread(None,
479
name='server-' + self.get_url())
480
self._server_thread.setDaemon(True)
481
self._server_thread.start()
483
def stop_background_thread(self):
484
self._should_terminate = True
485
# self._server_socket.close()
486
# we used to join the thread, but it's not really necessary; it will
488
## self._server_thread.join()
491
class SmartTCPServer_for_testing(SmartTCPServer):
492
"""Server suitable for use by transport tests.
494
This server is backed by the process's cwd.
498
self._homedir = os.getcwd()
499
# The server is set up by default like for ssh access: the client
500
# passes filesystem-absolute paths; therefore the server must look
501
# them up relative to the root directory. it might be better to act
502
# a public server and have the server rewrite paths into the test
504
SmartTCPServer.__init__(self, transport.get_transport("file:///"))
507
"""Set up server for testing"""
508
self.start_background_thread()
511
self.stop_background_thread()
514
"""Return the url of the server"""
515
host, port = self._server_socket.getsockname()
516
# XXX: I think this is likely to break on windows -- self._homedir will
517
# have backslashes (and maybe a drive letter?).
518
# -- Andrew Bennetts, 2006-08-29
519
return "bzr://%s:%d%s" % (host, port, urlutils.escape(self._homedir))
521
def get_bogus_url(self):
522
"""Return a URL which will fail to connect"""
523
return 'bzr://127.0.0.1:1/'
526
class SmartStat(object):
528
def __init__(self, size, mode):
533
class SmartTransport(transport.Transport):
534
"""Connection to a smart server.
536
The connection holds references to pipes that can be used to send requests
539
The connection has a notion of the current directory to which it's
540
connected; this is incorporated in filenames passed to the server.
542
This supports some higher-level RPC operations and can also be treated
543
like a Transport to do file-like operations.
545
The connection can be made over a tcp socket, or (in future) an ssh pipe
546
or a series of http requests. There are concrete subclasses for each
547
type: SmartTCPTransport, etc.
550
def __init__(self, url, clone_from=None, client=None):
553
:param client: ignored when clone_from is not None.
555
### Technically super() here is faulty because Transport's __init__
556
### fails to take 2 parameters, and if super were to choose a silly
557
### initialisation order things would blow up.
558
if not url.endswith('/'):
560
super(SmartTransport, self).__init__(url)
561
self._scheme, self._username, self._password, self._host, self._port, self._path = \
562
transport.split_url(url)
563
if clone_from is None:
565
self._client = SmartStreamClient(self._connect_to_server)
567
self._client = client
569
# credentials may be stripped from the base in some circumstances
570
# as yet to be clearly defined or documented, so copy them.
571
self._username = clone_from._username
572
# reuse same connection
573
self._client = clone_from._client
575
def abspath(self, relpath):
576
"""Return the full url to the given relative path.
578
@param relpath: the relative path or path components
579
@type relpath: str or list
581
return self._unparse_url(self._remote_path(relpath))
583
def clone(self, relative_url):
584
"""Make a new SmartTransport related to me, sharing the same connection.
586
This essentially opens a handle on a different remote directory.
588
if relative_url is None:
589
return self.__class__(self.base, self)
591
return self.__class__(self.abspath(relative_url), self)
593
def is_readonly(self):
594
"""Smart server transport can do read/write file operations."""
597
def get_smart_client(self):
600
def _unparse_url(self, path):
601
"""Return URL for a path.
603
:see: SFTPUrlHandling._unparse_url
605
# TODO: Eventually it should be possible to unify this with
606
# SFTPUrlHandling._unparse_url?
609
path = urllib.quote(path)
610
netloc = urllib.quote(self._host)
611
if self._username is not None:
612
netloc = '%s@%s' % (urllib.quote(self._username), netloc)
613
if self._port is not None:
614
netloc = '%s:%d' % (netloc, self._port)
615
return urlparse.urlunparse((self._scheme, netloc, path, '', '', ''))
617
def _remote_path(self, relpath):
618
"""Returns the Unicode version of the absolute path for relpath."""
619
return self._combine_paths(self._path, relpath)
621
def has(self, relpath):
622
"""Indicate whether a remote file of the given name exists or not.
624
:see: Transport.has()
626
resp = self._client._call('has', self._remote_path(relpath))
627
if resp == ('yes', ):
629
elif resp == ('no', ):
632
self._translate_error(resp)
634
def get(self, relpath):
635
"""Return file-like object reading the contents of a remote file.
637
:see: Transport.get_bytes()/get_file()
639
remote = self._remote_path(relpath)
640
resp = self._client._call('get', remote)
642
self._translate_error(resp, relpath)
643
return StringIO(self._client._recv_bulk())
645
def _serialise_optional_mode(self, mode):
651
def mkdir(self, relpath, mode=None):
652
resp = self._client._call('mkdir',
653
self._remote_path(relpath),
654
self._serialise_optional_mode(mode))
655
self._translate_error(resp)
657
def put_file(self, relpath, upload_file, mode=None):
658
# its not ideal to seek back, but currently put_non_atomic_file depends
659
# on transports not reading before failing - which is a faulty
660
# assumption I think - RBC 20060915
661
pos = upload_file.tell()
663
return self.put_bytes(relpath, upload_file.read(), mode)
665
upload_file.seek(pos)
668
def put_bytes(self, relpath, upload_contents, mode=None):
669
# FIXME: upload_file is probably not safe for non-ascii characters -
670
# should probably just pass all parameters as length-delimited
672
resp = self._client._call_with_upload(
674
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
676
self._translate_error(resp)
678
def append_file(self, relpath, from_file, mode=None):
679
return self.append_bytes(relpath, from_file.read(), mode)
681
def append_bytes(self, relpath, bytes, mode=None):
682
resp = self._client._call_with_upload(
684
(self._remote_path(relpath), self._serialise_optional_mode(mode)),
686
if resp[0] == 'appended':
688
self._translate_error(resp)
690
def delete(self, relpath):
691
resp = self._client._call('delete', self._remote_path(relpath))
692
self._translate_error(resp)
694
def rename(self, rel_from, rel_to):
696
self._remote_path(rel_from),
697
self._remote_path(rel_to))
699
def move(self, rel_from, rel_to):
701
self._remote_path(rel_from),
702
self._remote_path(rel_to))
704
def rmdir(self, relpath):
705
resp = self._call('rmdir', self._remote_path(relpath))
707
def _call(self, method, *args):
708
resp = self._client._call(method, *args)
709
self._translate_error(resp)
711
def _translate_error(self, resp, orig_path=None):
712
"""Raise an exception from a response"""
719
elif what == 'NoSuchFile':
720
if orig_path is not None:
721
error_path = orig_path
724
raise errors.NoSuchFile(error_path)
725
elif what == 'error':
726
raise errors.SmartProtocolError(unicode(resp[1]))
727
elif what == 'FileExists':
728
raise errors.FileExists(resp[1])
729
elif what == 'DirectoryNotEmpty':
730
raise errors.DirectoryNotEmpty(resp[1])
731
elif what in ('UnicodeEncodeError', 'UnicodeDecodeError'):
732
encoding = str(resp[1]) # encoding must always be a string
736
reason = str(resp[5]) # reason must always be a string
737
if val.startswith('u:'):
739
elif val.startswith('s:'):
740
val = val[2:].decode('base64')
741
if what == 'UnicodeDecodeError':
742
raise UnicodeDecodeError(encoding, val, start, end, reason)
743
elif what == 'UnicodeEncodeError':
744
raise UnicodeEncodeError(encoding, val, start, end, reason)
746
raise errors.SmartProtocolError('unexpected smart server error: %r' % (resp,))
748
def _send_tuple(self, args):
749
self._client._send_tuple(args)
751
def _recv_tuple(self):
752
return self._client._recv_tuple()
754
def disconnect(self):
755
self._client.disconnect()
757
def delete_tree(self, relpath):
758
raise errors.TransportNotPossible('readonly transport')
760
def stat(self, relpath):
761
resp = self._client._call('stat', self._remote_path(relpath))
762
if resp[0] == 'stat':
763
return SmartStat(int(resp[1]), int(resp[2], 8))
765
self._translate_error(resp)
767
## def lock_read(self, relpath):
768
## """Lock the given file for shared (read) access.
769
## :return: A lock object, which should be passed to Transport.unlock()
771
## # The old RemoteBranch ignore lock for reading, so we will
772
## # continue that tradition and return a bogus lock object.
773
## class BogusLock(object):
774
## def __init__(self, path):
778
## return BogusLock(relpath)
783
def list_dir(self, relpath):
784
resp = self._client._call('list_dir',
785
self._remote_path(relpath))
786
if resp[0] == 'names':
787
return [name.encode('ascii') for name in resp[1:]]
789
self._translate_error(resp)
791
def iter_files_recursive(self):
792
resp = self._client._call('iter_files_recursive',
793
self._remote_path(''))
794
if resp[0] == 'names':
797
self._translate_error(resp)
800
class SmartStreamClient(SmartProtocolBase):
801
"""Connection to smart server over two streams"""
803
def __init__(self, connect_func):
804
self._connect_func = connect_func
805
self._connected = False
810
def _ensure_connection(self):
811
if not self._connected:
812
self._in, self._out = self._connect_func()
813
self._connected = True
815
def _send_tuple(self, args):
816
self._ensure_connection()
817
_send_tuple(self._out, args)
819
def _send_bulk_data(self, body):
820
self._ensure_connection()
821
SmartProtocolBase._send_bulk_data(self, body)
823
def _recv_bulk(self):
824
self._ensure_connection()
825
return SmartProtocolBase._recv_bulk(self)
827
def _recv_tuple(self):
828
self._ensure_connection()
829
return SmartProtocolBase._recv_tuple(self)
831
def _recv_trailer(self):
832
self._ensure_connection()
833
return SmartProtocolBase._recv_trailer(self)
835
def disconnect(self):
836
"""Close connection to the server"""
841
def _call(self, *args):
842
self._send_tuple(args)
843
return self._recv_tuple()
845
def _call_with_upload(self, method, args, body):
846
"""Call an rpc, supplying bulk upload data.
848
:param method: method name to call
849
:param args: parameter args tuple
850
:param body: upload body as a byte string
852
self._send_tuple((method,) + args)
853
self._send_bulk_data(body)
854
return self._recv_tuple()
856
def query_version(self):
857
"""Return protocol version number of the server."""
858
# XXX: should make sure it's empty
859
self._send_tuple(('hello',))
860
resp = self._recv_tuple()
861
if resp == ('ok', '1'):
864
raise errors.SmartProtocolError("bad response %r" % (resp,))
867
class SmartTCPTransport(SmartTransport):
868
"""Connection to smart server over plain tcp"""
870
def __init__(self, url, clone_from=None):
871
super(SmartTCPTransport, self).__init__(url, clone_from)
873
self._port = int(self._port)
874
except (ValueError, TypeError), e:
875
raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
878
def _connect_to_server(self):
879
self._socket = socket.socket()
880
self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
881
result = self._socket.connect_ex((self._host, int(self._port)))
883
raise errors.ConnectionError("failed to connect to %s:%d: %s" %
884
(self._host, self._port, os.strerror(result)))
885
# TODO: May be more efficient to just treat them as sockets
886
# throughout? But what about pipes to ssh?...
887
to_server = self._socket.makefile('w')
888
from_server = self._socket.makefile('r')
889
return from_server, to_server
891
def disconnect(self):
892
super(SmartTCPTransport, self).disconnect()
893
# XXX: Is closing the socket as well as closing the files really
895
if self._socket is not None:
899
from bzrlib.transport import sftp, ssh
900
except errors.ParamikoNotPresent:
901
# no paramiko, no SSHTransport.
904
class SmartSSHTransport(SmartTransport):
905
"""Connection to smart server over SSH."""
907
def __init__(self, url, clone_from=None):
908
# TODO: all this probably belongs in the parent class.
909
super(SmartSSHTransport, self).__init__(url, clone_from)
911
if self._port is not None:
912
self._port = int(self._port)
913
except (ValueError, TypeError), e:
914
raise errors.InvalidURL(path=url, extra="invalid port %s" % self._port)
916
def _connect_to_server(self):
917
# XXX: don't hardcode vendor
918
# XXX: cannot pass password to SSHSubprocess yet
919
if self._password is not None:
920
raise errors.InvalidURL("SSH smart transport doesn't handle passwords")
921
executable = os.environ.get('BZR_REMOTE_PATH', 'bzr')
922
vendor = ssh._get_ssh_vendor()
923
self._ssh_connection = vendor.connect_ssh(self._username, None,
924
self._host, self._port,
925
command=[executable, 'serve', '--inet',
927
return self._ssh_connection.get_filelike_channels()
929
def disconnect(self):
930
super(SmartSSHTransport, self).disconnect()
931
self._ssh_connection.close()
934
def get_test_permutations():
935
"""Return (transport, server) permutations for testing"""
936
return [(SmartTCPTransport, SmartTCPServer_for_testing)]