~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/smart/server.py

  • Committer: Danny van Heumen
  • Date: 2010-03-09 21:42:11 UTC
  • mto: (4634.139.5 2.0)
  • mto: This revision was merged to the branch mainline in revision 5160.
  • Revision ID: danny@dannyvanheumen.nl-20100309214211-iqh42x6qcikgd9p3
Reverted now-useless TODO list.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006, 2007 Canonical Ltd
 
1
# Copyright (C) 2006, 2007, 2008 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
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
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
"""Server for smart-server protocol."""
18
18
 
19
19
import errno
20
20
import socket
 
21
import sys
21
22
import threading
22
23
 
23
 
from bzrlib.hooks import Hooks
 
24
from bzrlib.hooks import HookPoint, Hooks
24
25
from bzrlib import (
25
26
    errors,
26
27
    trace,
27
28
    transport,
28
29
)
29
 
from bzrlib.smart.medium import SmartServerSocketStreamMedium
 
30
from bzrlib.lazy_import import lazy_import
 
31
lazy_import(globals(), """
 
32
from bzrlib.smart import medium
 
33
""")
30
34
 
31
35
 
32
36
class SmartTCPServer(object):
38
42
    hooks: An instance of SmartServerHooks.
39
43
    """
40
44
 
41
 
    def __init__(self, backing_transport, host='127.0.0.1', port=0):
 
45
    def __init__(self, backing_transport, host='127.0.0.1', port=0,
 
46
                 root_client_path='/'):
42
47
        """Construct a new server.
43
48
 
44
49
        To actually start it running, call either start_background_thread or
45
50
        serve.
46
51
 
 
52
        :param backing_transport: The transport to serve.
47
53
        :param host: Name of the interface to listen on.
48
54
        :param port: TCP port to listen on, or 0 to allocate a transient port.
 
55
        :param root_client_path: The client path that will correspond to root
 
56
            of backing_transport.
49
57
        """
50
58
        # let connections timeout so that we get a chance to terminate
51
59
        # Keep a reference to the exceptions we want to catch because the socket
54
62
        from socket import error as socket_error
55
63
        self._socket_error = socket_error
56
64
        self._socket_timeout = socket_timeout
57
 
        self._server_socket = socket.socket()
58
 
        self._server_socket.bind((host, port))
 
65
        addrs = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
 
66
            socket.SOCK_STREAM, 0, socket.AI_PASSIVE)[0]
 
67
 
 
68
        (family, socktype, proto, canonname, sockaddr) = addrs
 
69
 
 
70
        self._server_socket = socket.socket(family, socktype, proto)
 
71
        # SO_REUSERADDR has a different meaning on Windows
 
72
        if sys.platform != 'win32':
 
73
            self._server_socket.setsockopt(socket.SOL_SOCKET,
 
74
                socket.SO_REUSEADDR, 1)
 
75
        try:
 
76
            self._server_socket.bind(sockaddr)
 
77
        except self._socket_error, message:
 
78
            raise errors.CannotBindAddress(host, port, message)
59
79
        self._sockname = self._server_socket.getsockname()
60
80
        self.port = self._sockname[1]
61
81
        self._server_socket.listen(1)
63
83
        self.backing_transport = backing_transport
64
84
        self._started = threading.Event()
65
85
        self._stopped = threading.Event()
 
86
        self.root_client_path = root_client_path
66
87
 
67
 
    def serve(self):
 
88
    def serve(self, thread_name_suffix=''):
68
89
        self._should_terminate = False
69
90
        # for hooks we are letting code know that a server has started (and
70
91
        # later stopped).
77
98
        # We need all three because:
78
99
        #  * other machines see the first
79
100
        #  * local commits on this machine should be able to be mapped to
80
 
        #    this server 
 
101
        #    this server
81
102
        #  * commits the server does itself need to be mapped across to this
82
103
        #    server.
83
104
        # The latter two urls are different aliases to the servers url,
84
 
        # so we group those in a list - as there might be more aliases 
 
105
        # so we group those in a list - as there might be more aliases
85
106
        # in the future.
86
107
        backing_urls = [self.backing_transport.base]
87
108
        try:
90
111
            pass
91
112
        for hook in SmartTCPServer.hooks['server_started']:
92
113
            hook(backing_urls, self.get_url())
 
114
        for hook in SmartTCPServer.hooks['server_started_ex']:
 
115
            hook(backing_urls, self)
93
116
        self._started.set()
94
117
        try:
95
118
            try:
106
129
                        if e.args[0] != errno.EBADF:
107
130
                            trace.warning("listening socket error: %s", e)
108
131
                    else:
109
 
                        self.serve_conn(conn)
 
132
                        self.serve_conn(conn, thread_name_suffix)
110
133
            except KeyboardInterrupt:
111
134
                # dont log when CTRL-C'd.
112
135
                raise
129
152
        """Return the url of the server"""
130
153
        return "bzr://%s:%d/" % self._sockname
131
154
 
132
 
    def serve_conn(self, conn):
 
155
    def serve_conn(self, conn, thread_name_suffix):
133
156
        # For WIN32, where the timeout value from the listening socket
134
 
        # propogates to the newly accepted socket.
 
157
        # propagates to the newly accepted socket.
135
158
        conn.setblocking(True)
136
159
        conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
137
 
        handler = SmartServerSocketStreamMedium(conn, self.backing_transport)
138
 
        connection_thread = threading.Thread(None, handler.serve, name='smart-server-child')
 
160
        handler = medium.SmartServerSocketStreamMedium(
 
161
            conn, self.backing_transport, self.root_client_path)
 
162
        thread_name = 'smart-server-child' + thread_name_suffix
 
163
        connection_thread = threading.Thread(
 
164
            None, handler.serve, name=thread_name)
139
165
        connection_thread.setDaemon(True)
140
166
        connection_thread.start()
141
167
 
142
 
    def start_background_thread(self):
 
168
    def start_background_thread(self, thread_name_suffix=''):
143
169
        self._started.clear()
144
170
        self._server_thread = threading.Thread(None,
145
 
                self.serve,
 
171
                self.serve, args=(thread_name_suffix,),
146
172
                name='server-' + self.get_url())
147
173
        self._server_thread.setDaemon(True)
148
174
        self._server_thread.start()
154
180
        self._should_terminate = True
155
181
        # close the socket - gives error to connections from here on in,
156
182
        # rather than a connection reset error to connections made during
157
 
        # the period between setting _should_terminate = True and 
 
183
        # the period between setting _should_terminate = True and
158
184
        # the current request completing/aborting. It may also break out the
159
185
        # main loop if it was currently in accept() (on some platforms).
160
186
        try:
184
210
        notified.
185
211
        """
186
212
        Hooks.__init__(self)
187
 
        # Introduced in 0.16:
188
 
        # invoked whenever the server starts serving a directory.
189
 
        # The api signature is (backing urls, public url).
190
 
        self['server_started'] = []
191
 
        # Introduced in 0.16:
192
 
        # invoked whenever the server stops serving a directory.
193
 
        # The api signature is (backing urls, public url).
194
 
        self['server_stopped'] = []
 
213
        self.create_hook(HookPoint('server_started',
 
214
            "Called by the bzr server when it starts serving a directory. "
 
215
            "server_started is called with (backing urls, public url), "
 
216
            "where backing_url is a list of URLs giving the "
 
217
            "server-specific directory locations, and public_url is the "
 
218
            "public URL for the directory being served.", (0, 16), None))
 
219
        self.create_hook(HookPoint('server_started_ex',
 
220
            "Called by the bzr server when it starts serving a directory. "
 
221
            "server_started is called with (backing_urls, server_obj).",
 
222
            (1, 17), None))
 
223
        self.create_hook(HookPoint('server_stopped',
 
224
            "Called by the bzr server when it stops serving a directory. "
 
225
            "server_stopped is called with the same parameters as the "
 
226
            "server_started hook: (backing_urls, public_url).", (0, 16), None))
195
227
 
196
228
SmartTCPServer.hooks = SmartServerHooks()
197
229
 
198
230
 
199
231
class SmartTCPServer_for_testing(SmartTCPServer):
200
232
    """Server suitable for use by transport tests.
201
 
    
 
233
 
202
234
    This server is backed by the process's cwd.
203
235
    """
204
236
 
205
 
    def __init__(self):
 
237
    def __init__(self, thread_name_suffix=''):
206
238
        SmartTCPServer.__init__(self, None)
207
 
        
 
239
        self.client_path_extra = None
 
240
        self.thread_name_suffix = thread_name_suffix
 
241
 
208
242
    def get_backing_transport(self, backing_transport_server):
209
243
        """Get a backing transport from a server we are decorating."""
210
244
        return transport.get_transport(backing_transport_server.get_url())
211
245
 
212
 
    def setUp(self, backing_transport_server=None):
213
 
        """Set up server for testing"""
 
246
    def setUp(self, backing_transport_server=None,
 
247
              client_path_extra='/extra/'):
 
248
        """Set up server for testing.
 
249
 
 
250
        :param backing_transport_server: backing server to use.  If not
 
251
            specified, a LocalURLServer at the current working directory will
 
252
            be used.
 
253
        :param client_path_extra: a path segment starting with '/' to append to
 
254
            the root URL for this server.  For instance, a value of '/foo/bar/'
 
255
            will mean the root of the backing transport will be published at a
 
256
            URL like `bzr://127.0.0.1:nnnn/foo/bar/`, rather than
 
257
            `bzr://127.0.0.1:nnnn/`.  Default value is `extra`, so that tests
 
258
            by default will fail unless they do the necessary path translation.
 
259
        """
 
260
        if not client_path_extra.startswith('/'):
 
261
            raise ValueError(client_path_extra)
214
262
        from bzrlib.transport.chroot import ChrootServer
215
263
        if backing_transport_server is None:
216
264
            from bzrlib.transport.local import LocalURLServer
220
268
        self.chroot_server.setUp()
221
269
        self.backing_transport = transport.get_transport(
222
270
            self.chroot_server.get_url())
223
 
        self.start_background_thread()
 
271
        self.root_client_path = self.client_path_extra = client_path_extra
 
272
        self.start_background_thread(self.thread_name_suffix)
224
273
 
225
274
    def tearDown(self):
226
275
        self.stop_background_thread()
227
276
        self.chroot_server.tearDown()
228
277
 
 
278
    def get_url(self):
 
279
        url = super(SmartTCPServer_for_testing, self).get_url()
 
280
        return url[:-1] + self.client_path_extra
 
281
 
229
282
    def get_bogus_url(self):
230
283
        """Return a URL which will fail to connect"""
231
284
        return 'bzr://127.0.0.1:1/'
239
292
        url = 'readonly+' + backing_transport_server.get_url()
240
293
        return transport.get_transport(url)
241
294
 
 
295
 
 
296
class SmartTCPServer_for_testing_v2_only(SmartTCPServer_for_testing):
 
297
    """A variation of SmartTCPServer_for_testing that limits the client to
 
298
    using RPCs in protocol v2 (i.e. bzr <= 1.5).
 
299
    """
 
300
 
 
301
    def get_url(self):
 
302
        url = super(SmartTCPServer_for_testing_v2_only, self).get_url()
 
303
        url = 'bzr-v2://' + url[len('bzr://'):]
 
304
        return url
 
305
 
 
306
 
 
307
class ReadonlySmartTCPServer_for_testing_v2_only(SmartTCPServer_for_testing_v2_only):
 
308
    """Get a readonly server for testing."""
 
309
 
 
310
    def get_backing_transport(self, backing_transport_server):
 
311
        """Get a backing transport from a server we are decorating."""
 
312
        url = 'readonly+' + backing_transport_server.get_url()
 
313
        return transport.get_transport(url)
 
314
 
 
315
 
 
316
def serve_bzr(transport, host=None, port=None, inet=False):
 
317
    from bzrlib import lockdir, ui
 
318
    from bzrlib.transport import get_transport
 
319
    from bzrlib.transport.chroot import ChrootServer
 
320
    chroot_server = ChrootServer(transport)
 
321
    chroot_server.setUp()
 
322
    transport = get_transport(chroot_server.get_url())
 
323
    if inet:
 
324
        smart_server = medium.SmartServerPipeStreamMedium(
 
325
            sys.stdin, sys.stdout, transport)
 
326
    else:
 
327
        if host is None:
 
328
            host = medium.BZR_DEFAULT_INTERFACE
 
329
        if port is None:
 
330
            port = medium.BZR_DEFAULT_PORT
 
331
        smart_server = SmartTCPServer(transport, host=host, port=port)
 
332
        trace.note('listening on port: %s' % smart_server.port)
 
333
    # For the duration of this server, no UI output is permitted. note
 
334
    # that this may cause problems with blackbox tests. This should be
 
335
    # changed with care though, as we dont want to use bandwidth sending
 
336
    # progress over stderr to smart server clients!
 
337
    old_factory = ui.ui_factory
 
338
    old_lockdir_timeout = lockdir._DEFAULT_TIMEOUT_SECONDS
 
339
    try:
 
340
        ui.ui_factory = ui.SilentUIFactory()
 
341
        lockdir._DEFAULT_TIMEOUT_SECONDS = 0
 
342
        smart_server.serve()
 
343
    finally:
 
344
        ui.ui_factory = old_factory
 
345
        lockdir._DEFAULT_TIMEOUT_SECONDS = old_lockdir_timeout
 
346