27
26
from bzrlib import (
30
31
revision as _mod_revision,
33
36
from bzrlib.branch import Branch
34
from bzrlib.bzrdir import BzrDir
35
from bzrlib.errors import ParamikoNotPresent
37
from bzrlib.controldir import ControlDir
36
38
from bzrlib.smart import client, medium
37
from bzrlib.smart.server import SmartTCPServer
38
from bzrlib.tests import TestCaseWithTransport, TestSkipped
39
from bzrlib.trace import mutter
40
from bzrlib.transport import get_transport, remote
43
class TestBzrServe(TestCaseWithTransport):
39
from bzrlib.smart.server import (
43
from bzrlib.tests import (
44
TestCaseWithMemoryTransport,
45
TestCaseWithTransport,
47
from bzrlib.transport import remote
50
class TestBzrServeBase(TestCaseWithTransport):
52
def run_bzr_serve_then_func(self, serve_args, retcode=0, func=None,
53
*func_args, **func_kwargs):
54
"""Run 'bzr serve', and run the given func in a thread once the server
57
When 'func' terminates, the server will be terminated too.
59
Returns stdout and stderr.
61
def on_server_start_thread(tcp_server):
62
"""This runs concurrently with the server thread.
64
The server is interrupted as soon as ``func`` finishes, even if an
65
exception is encountered.
69
self.tcp_server = tcp_server
72
func(*func_args, **func_kwargs)
74
# Log errors to make some test failures a little less
76
trace.mutter('func broke: %r', e)
78
# Then stop the server
79
trace.mutter('interrupting...')
80
thread.interrupt_main()
81
# When the hook is fired, it just starts ``on_server_start_thread`` and
83
def on_server_start(backing_urls, tcp_server):
85
target=on_server_start_thread, args=(tcp_server,))
88
SmartTCPServer.hooks.install_named_hook(
89
'server_started_ex', on_server_start,
90
'run_bzr_serve_then_func hook')
91
# It seesm thread.interrupt_main() will not raise KeyboardInterrupt
92
# until after socket.accept returns. So we set the timeout low to make
94
self.overrideAttr(SmartTCPServer, '_ACCEPT_TIMEOUT', 0.1)
97
out, err = self.run_bzr(['serve'] + list(serve_args),
99
except KeyboardInterrupt, e:
104
class TestBzrServe(TestBzrServeBase):
107
super(TestBzrServe, self).setUp()
108
self.disable_missing_extensions_warning()
110
def test_server_exception_with_hook(self):
111
"""Catch exception from the server in the server_exception hook.
113
We use ``run_bzr_serve_then_func`` without a ``func`` so the server
114
will receive a KeyboardInterrupt exception we want to catch.
117
if exception[0] is KeyboardInterrupt:
118
sys.stderr.write('catching KeyboardInterrupt\n')
122
SmartTCPServer.hooks.install_named_hook(
123
'server_exception', hook,
124
'test_server_except_hook hook')
125
args = ['--listen', 'localhost', '--port', '0', '--quiet']
126
out, err = self.run_bzr_serve_then_func(args, retcode=0)
127
self.assertEqual('catching KeyboardInterrupt\n', err)
129
def test_server_exception_no_hook(self):
130
"""test exception without hook returns error"""
132
out, err = self.run_bzr_serve_then_func(args, retcode=3)
45
134
def assertInetServerShutsdownCleanly(self, process):
46
135
"""Shutdown the server process looking for errors."""
98
190
finish_bzr_subprocess, and the base url for the server.
100
192
# Serve from the current directory
101
args = ['serve', '--port', 'localhost:0']
193
args = ['serve', '--listen', 'localhost', '--port', '0']
102
194
args.extend(extra_options)
103
195
process = self.start_bzr_subprocess(args, skip_if_plan_to_signal=True)
104
196
port_line = process.stderr.readline()
105
197
prefix = 'listening on port: '
106
198
self.assertStartsWith(port_line, prefix)
107
199
port = int(port_line[len(prefix):])
108
return process,'bzr://localhost:%d/' % port
200
url = 'bzr://localhost:%d/' % port
204
def test_bzr_serve_quiet(self):
205
self.make_branch('.')
206
args = ['--listen', 'localhost', '--port', '0', '--quiet']
207
out, err = self.run_bzr_serve_then_func(args, retcode=3)
208
self.assertEqual('', out)
209
self.assertEqual('', err)
110
211
def test_bzr_serve_inet_readonly(self):
111
212
"""bzr server should provide a read only filesystem by default."""
154
256
self.make_read_requests(branch)
155
257
self.assertServerFinishesCleanly(process)
157
def test_bzr_connect_to_bzr_ssh(self):
158
"""User acceptance that get_transport of a bzr+ssh:// behaves correctly.
160
bzr+ssh:// should cause bzr to run a remote bzr smart server over SSH.
163
from bzrlib.transport.sftp import SFTPServer
164
except ParamikoNotPresent:
165
raise TestSkipped('Paramiko not installed')
166
from bzrlib.tests.stub_sftp import StubServer
169
self.make_branch('a_branch')
171
# Start an SSH server
172
self.command_executed = []
173
# XXX: This is horrible -- we define a really dumb SSH server that
174
# executes commands, and manage the hooking up of stdin/out/err to the
175
# SSH channel ourselves. Surely this has already been implemented
177
class StubSSHServer(StubServer):
181
def check_channel_exec_request(self, channel, command):
182
self.test.command_executed.append(command)
183
proc = subprocess.Popen(
184
command, shell=True, stdin=subprocess.PIPE,
185
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
187
# XXX: horribly inefficient, not to mention ugly.
188
# Start a thread for each of stdin/out/err, and relay bytes from
189
# the subprocess to channel and vice versa.
190
def ferry_bytes(read, write, close):
199
(channel.recv, proc.stdin.write, proc.stdin.close),
200
(proc.stdout.read, channel.sendall, channel.close),
201
(proc.stderr.read, channel.sendall_stderr, channel.close)]
202
for read, write, close in file_functions:
203
t = threading.Thread(
204
target=ferry_bytes, args=(read, write, close))
209
ssh_server = SFTPServer(StubSSHServer)
210
# XXX: We *don't* want to override the default SSH vendor, so we set
211
# _vendor to what _get_ssh_vendor returns.
213
self.addCleanup(ssh_server.tearDown)
214
port = ssh_server._listener.port
216
# Access the branch via a bzr+ssh URL. The BZR_REMOTE_PATH environment
217
# variable is used to tell bzr what command to run on the remote end.
218
path_to_branch = osutils.abspath('a_branch')
220
orig_bzr_remote_path = os.environ.get('BZR_REMOTE_PATH')
221
bzr_remote_path = self.get_bzr_path()
222
if sys.platform == 'win32':
223
bzr_remote_path = sys.executable + ' ' + self.get_bzr_path()
224
os.environ['BZR_REMOTE_PATH'] = bzr_remote_path
226
if sys.platform == 'win32':
227
path_to_branch = os.path.splitdrive(path_to_branch)[1]
228
branch = Branch.open(
229
'bzr+ssh://fred:secret@localhost:%d%s' % (port, path_to_branch))
230
self.make_read_requests(branch)
231
# Check we can perform write operations
232
branch.bzrdir.root_transport.mkdir('foo')
234
# Restore the BZR_REMOTE_PATH environment variable back to its
236
if orig_bzr_remote_path is None:
237
del os.environ['BZR_REMOTE_PATH']
239
os.environ['BZR_REMOTE_PATH'] = orig_bzr_remote_path
242
['%s serve --inet --directory=/ --allow-writes'
244
self.command_executed)
247
class TestCmdServeChrooting(TestCaseWithTransport):
259
def test_bzr_serve_dhpss(self):
260
# This is a smoke test that the server doesn't crash when run with
261
# -Dhpss, and does drop some hpss logging to the file.
262
self.make_branch('.')
263
log_fname = os.getcwd() + '/server.log'
264
self.overrideEnv('BZR_LOG', log_fname)
265
process, transport = self.start_server_inet(['-Dhpss'])
266
branch = ControlDir.open_from_transport(transport).open_branch()
267
self.make_read_requests(branch)
268
self.assertInetServerShutsdownCleanly(process)
269
f = open(log_fname, 'rb')
272
self.assertContainsRe(content, r'hpss request: \[[0-9-]+\]')
274
def test_bzr_serve_supports_configurable_timeout(self):
275
gs = config.GlobalStack()
276
gs.set('serve.client_timeout', 0.2)
277
# Save the config as the subprocess will use it
279
process, url = self.start_server_port()
280
self.build_tree_contents([('a_file', 'contents\n')])
281
# We can connect and issue a request
282
t = transport.get_transport_from_url(url)
283
self.assertEqual('contents\n', t.get_bytes('a_file'))
284
# However, if we just wait for more content from the server, it will
285
# eventually disconnect us.
286
m = t.get_smart_medium()
288
# Now, we wait for timeout to trigger
289
err = process.stderr.readline()
291
'Connection Timeout: disconnecting client after 0.2 seconds\n',
293
self.assertServerFinishesCleanly(process)
295
def test_bzr_serve_supports_client_timeout(self):
296
process, url = self.start_server_port(['--client-timeout=0.1'])
297
self.build_tree_contents([('a_file', 'contents\n')])
298
# We can connect and issue a request
299
t = transport.get_transport_from_url(url)
300
self.assertEqual('contents\n', t.get_bytes('a_file'))
301
# However, if we just wait for more content from the server, it will
302
# eventually disconnect us.
303
# TODO: Use something like signal.alarm() so that if the server doesn't
304
# properly handle the timeout, we end up failing the test instead
305
# of hanging forever.
306
m = t.get_smart_medium()
308
# Now, we wait for timeout to trigger
309
err = process.stderr.readline()
311
'Connection Timeout: disconnecting client after 0.1 seconds\n',
313
self.assertServerFinishesCleanly(process)
315
def test_bzr_serve_graceful_shutdown(self):
316
big_contents = 'a'*64*1024
317
self.build_tree_contents([('bigfile', big_contents)])
318
process, url = self.start_server_port(['--client-timeout=1.0'])
319
t = transport.get_transport_from_url(url)
320
m = t.get_smart_medium()
321
c = client._SmartClient(m)
322
# Start, but don't finish a response
323
resp, response_handler = c.call_expecting_body('get', 'bigfile')
324
self.assertEqual(('ok',), resp)
325
# Note: process.send_signal is a Python 2.6ism
326
process.send_signal(signal.SIGHUP)
327
# Wait for the server to notice the signal, and then read the actual
328
# body of the response. That way we know that it is waiting for the
330
self.assertEqual('Requested to stop gracefully\n',
331
process.stderr.readline())
332
self.assertEqual('Waiting for 1 client(s) to finish\n',
333
process.stderr.readline())
334
body = response_handler.read_body_bytes()
335
if body != big_contents:
336
self.fail('Failed to properly read the contents of "bigfile"')
337
# Now that our request is finished, the medium should notice it has
339
self.assertEqual('', m.read_bytes(1))
340
# And the server should be stopping
341
self.assertEqual(0, process.wait())
344
class TestCmdServeChrooting(TestBzrServeBase):
249
346
def test_serve_tcp(self):
250
347
"""'bzr serve' wraps the given --directory in a ChrootServer.
256
353
t = self.get_transport()
257
354
t.mkdir('server-root')
258
355
self.run_bzr_serve_then_func(
259
['--port', '0', '--directory', t.local_abspath('server-root'),
356
['--listen', '127.0.0.1', '--port', '0',
357
'--directory', t.local_abspath('server-root'),
260
358
'--allow-writes'],
261
self.when_server_started)
359
func=self.when_server_started)
262
360
# The when_server_started method issued a find_repositoryV3 that should
263
361
# fail with 'norepository' because there are no repositories inside the
265
363
self.assertEqual(('norepository',), self.client_resp)
267
def run_bzr_serve_then_func(self, serve_args, func, *func_args,
269
"""Run 'bzr serve', and run the given func in a thread once the server
272
When 'func' terminates, the server will be terminated too.
275
def on_server_start(backing_urls, tcp_server):
276
t = threading.Thread(
277
target=on_server_start_thread, args=(tcp_server,))
279
def on_server_start_thread(tcp_server):
282
self.tcp_server = tcp_server
284
func(*func_args, **func_kwargs)
286
# Log errors to make some test failures a little less
288
mutter('func broke: %r', e)
290
# Then stop the server
291
mutter('interrupting...')
292
thread.interrupt_main()
293
SmartTCPServer.hooks.install_named_hook(
294
'server_started_ex', on_server_start,
295
'run_bzr_serve_then_func hook')
298
self.run_bzr(['serve'] + list(serve_args))
299
except KeyboardInterrupt:
302
365
def when_server_started(self):
303
366
# Connect to the TCP server and issue some requests and see what comes
316
379
client_medium.disconnect()
382
class TestUserdirExpansion(TestCaseWithMemoryTransport):
385
def fake_expanduser(path):
386
"""A simple, environment-independent, function for the duration of this
389
Paths starting with a path segment of '~user' will expand to start with
390
'/home/user/'. Every other path will be unchanged.
392
if path.split('/', 1)[0] == '~user':
393
return '/home/user' + path[len('~user'):]
396
def make_test_server(self, base_path='/'):
397
"""Make and start a BzrServerFactory, backed by a memory transport, and
398
creat '/home/user' in that transport.
400
bzr_server = BzrServerFactory(
401
self.fake_expanduser, lambda t: base_path)
402
mem_transport = self.get_transport()
403
mem_transport.mkdir_multi(['home', 'home/user'])
404
bzr_server.set_up(mem_transport, None, None, inet=True, timeout=4.0)
405
self.addCleanup(bzr_server.tear_down)
408
def test_bzr_serve_expands_userdir(self):
409
bzr_server = self.make_test_server()
410
self.assertTrue(bzr_server.smart_server.backing_transport.has('~user'))
412
def test_bzr_serve_does_not_expand_userdir_outside_base(self):
413
bzr_server = self.make_test_server('/foo')
414
self.assertFalse(bzr_server.smart_server.backing_transport.has('~user'))
416
def test_get_base_path(self):
417
"""cmd_serve will turn the --directory option into a LocalTransport
418
(optionally decorated with 'readonly+'). BzrServerFactory can
419
determine the original --directory from that transport.
421
# URLs always include the trailing slash, and get_base_path returns it
422
base_dir = osutils.abspath('/a/b/c') + '/'
423
base_url = urlutils.local_path_to_url(base_dir) + '/'
424
# Define a fake 'protocol' to capture the transport that cmd_serve
425
# passes to serve_bzr.
426
def capture_transport(transport, host, port, inet, timeout):
427
self.bzr_serve_transport = transport
428
cmd = builtins.cmd_serve()
430
cmd.run(directory=base_dir, protocol=capture_transport)
431
server_maker = BzrServerFactory()
433
'readonly+%s' % base_url, self.bzr_serve_transport.base)
435
base_dir, server_maker.get_base_path(self.bzr_serve_transport))
437
cmd.run(directory=base_dir, protocol=capture_transport,
439
server_maker = BzrServerFactory()
440
self.assertEqual(base_url, self.bzr_serve_transport.base)
441
self.assertEqual(base_dir,
442
server_maker.get_base_path(self.bzr_serve_transport))
443
# Read-only, from a URL
444
cmd.run(directory=base_url, protocol=capture_transport)
445
server_maker = BzrServerFactory()
447
'readonly+%s' % base_url, self.bzr_serve_transport.base)
449
base_dir, server_maker.get_base_path(self.bzr_serve_transport))