1
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
4
# pylint: disable-msg=W0603
7
__all__ = ['start_launchpad']
10
from contextlib import nested
17
from lazr.config import as_host_port
18
from rabbitfixture.server import RabbitServerResources
19
from testtools.testresult.real import _details_to_str
20
from zope.app.server.main import main
22
from canonical.config import config
23
from canonical.launchpad.daemons import tachandler
24
from canonical.lazr.pidfile import (
28
from lp.services.googlesearch import googletestservice
29
from lp.services.mailman import runmailman
30
from lp.services.osutils import ensure_directory_exists
31
from lp.services.rabbit.server import RabbitServer
32
from lp.services.txlongpoll.server import TxLongPollServer
35
def make_abspath(path):
36
return os.path.abspath(os.path.join(config.root, *path.split('/')))
39
class Service(fixtures.Fixture):
42
def should_launch(self):
43
"""Return true if this service should be launched by default."""
47
"""Run the service in a thread or external process.
49
May block long enough to kick it off, but must return control to
50
the caller without waiting for it to shutdown.
52
raise NotImplementedError
55
super(Service, self).setUp()
59
class TacFile(Service):
61
def __init__(self, name, tac_filename, section_name, pre_launch=None):
62
"""Create a TacFile object.
64
:param name: A short name for the service. Used to name the pid file.
65
:param tac_filename: The location of the TAC file, relative to this
67
:param section_name: The config section name that provides the
68
launch, logfile and spew options.
69
:param pre_launch: A callable that is called before the launch
72
super(TacFile, self).__init__()
74
self.tac_filename = tac_filename
75
self.section_name = section_name
76
if pre_launch is None:
77
self.pre_launch = lambda: None
79
self.pre_launch = pre_launch
82
def should_launch(self):
83
return (self.section_name is not None
84
and config[self.section_name].launch)
88
"""Return the log file to use.
90
Default to the value of the configuration key logfile.
92
return config[self.section_name].logfile
97
pidfile = pidfile_path(self.name)
98
logfile = config[self.section_name].logfile
99
tacfile = make_abspath(self.tac_filename)
102
tachandler.twistd_script,
106
"--pidfile", pidfile,
107
"--prefix", self.name.capitalize(),
108
"--logfile", logfile,
111
if config[self.section_name].spew:
112
args.append("--spew")
114
# Note that startup tracebacks and evil programmers using 'print' will
115
# cause output to our stdout. However, we don't want to have twisted
116
# log to stdout and redirect it ourselves because we then lose the
117
# ability to cycle the log files by sending a signal to the twisted
119
process = subprocess.Popen(args, stdin=subprocess.PIPE)
120
self.addCleanup(stop_process, process)
121
process.stdin.close()
124
class MailmanService(Service):
127
def should_launch(self):
128
return config.mailman.launch
131
runmailman.start_mailman()
132
self.addCleanup(runmailman.stop_mailman)
135
class CodebrowseService(Service):
138
def should_launch(self):
142
process = subprocess.Popen(
143
['make', 'run_codebrowse'],
144
stdin=subprocess.PIPE)
145
self.addCleanup(stop_process, process)
146
process.stdin.close()
149
class GoogleWebService(Service):
152
def should_launch(self):
153
return config.google_test_service.launch
156
self.addCleanup(stop_process, googletestservice.start_as_process())
159
class MemcachedService(Service):
160
"""A local memcached service for developer environments."""
163
def should_launch(self):
164
return config.memcached.launch
169
'-m', str(config.memcached.memory_size),
170
'-l', str(config.memcached.address),
171
'-p', str(config.memcached.port),
172
'-U', str(config.memcached.port),
174
if config.memcached.verbose:
178
process = subprocess.Popen(cmd, stdin=subprocess.PIPE)
179
self.addCleanup(stop_process, process)
180
process.stdin.close()
183
class ForkingSessionService(Service):
184
"""A lp-forking-service for handling codehosting access."""
186
# TODO: The "sftp" (aka codehosting) server depends fairly heavily on this
187
# service. It would seem reasonable to make one always start if the
188
# other one is started. Though this might be a way to "FeatureFlag"
189
# whether this is active or not.
191
def should_launch(self):
192
return (config.codehosting.launch and
193
config.codehosting.use_forking_daemon)
197
"""Return the log file to use.
199
Default to the value of the configuration key logfile.
201
return config.codehosting.forker_logfile
204
# Following the logic in TacFile. Specifically, if you configure sftp
205
# to not run (and thus bzr+ssh) then we don't want to run the forking
207
if not self.should_launch:
209
from lp.codehosting import get_bzr_path
210
command = [config.root + '/bin/py', get_bzr_path(),
211
'launchpad-forking-service',
212
'--path', config.codehosting.forking_daemon_socket,
214
env = dict(os.environ)
215
env['BZR_PLUGIN_PATH'] = config.root + '/bzrplugins'
216
logfile = self.logfile
218
# This process uses a different logging infrastructure from the
219
# rest of the Launchpad code. As such, it cannot trivially use '-'
220
# as the logfile. So we just ignore this setting.
223
env['BZR_LOG'] = logfile
224
process = subprocess.Popen(command, env=env, stdin=subprocess.PIPE)
225
self.addCleanup(stop_process, process)
226
process.stdin.close()
229
class RabbitService(Service):
230
"""A RabbitMQ service."""
233
def should_launch(self):
234
return config.rabbitmq.launch
237
hostname, port = as_host_port(config.rabbitmq.host, None, None)
238
self.server = RabbitServer(
239
RabbitServerResources(hostname=hostname, port=port))
240
self.useFixture(self.server)
243
class TxLongPollService(Service):
244
"""A TxLongPoll service."""
247
def should_launch(self):
248
return config.txlongpoll.launch
251
twistd_bin = os.path.join(
252
config.root, 'bin', 'twistd-for-txlongpoll')
253
broker_hostname, broker_port = as_host_port(
254
config.rabbitmq.host, None, None)
255
self.server = TxLongPollServer(
256
twistd_bin=twistd_bin,
257
frontend_port=config.txlongpoll.frontend_port,
258
broker_user=config.rabbitmq.userid,
259
broker_password=config.rabbitmq.password,
260
broker_vhost=config.rabbitmq.virtual_host,
261
broker_host=broker_hostname,
262
broker_port=broker_port)
263
self.useFixture(self.server)
266
def stop_process(process):
267
"""kill process and BLOCK until process dies.
269
:param process: An instance of subprocess.Popen.
271
if process.poll() is None:
272
os.kill(process.pid, signal.SIGTERM)
276
def prepare_for_librarian():
277
if not os.path.isdir(config.librarian_server.root):
278
os.makedirs(config.librarian_server.root, 0700)
282
'librarian': TacFile('librarian', 'daemons/librarian.tac',
283
'librarian_server', prepare_for_librarian),
284
'sftp': TacFile('sftp', 'daemons/sftp.tac', 'codehosting'),
285
'forker': ForkingSessionService(),
286
'mailman': MailmanService(),
287
'codebrowse': CodebrowseService(),
288
'google-webservice': GoogleWebService(),
289
'memcached': MemcachedService(),
290
'rabbitmq': RabbitService(),
291
'txlongpoll': TxLongPollService(),
295
def get_services_to_run(requested_services):
296
"""Return a list of services (TacFiles) given a list of service names.
298
If no names are given, then the list of services to run comes from the
299
launchpad configuration.
301
If names are given, then only run the services matching those names.
303
if len(requested_services) == 0:
304
return [svc for svc in SERVICES.values() if svc.should_launch]
305
return [SERVICES[name] for name in requested_services]
308
def split_out_runlaunchpad_arguments(args):
309
"""Split the given command-line arguments into services to start and Zope
312
The runlaunchpad script can take an optional '-r services,...' argument.
313
If this argument is present, then the value is returned as the first
314
element of the return tuple. The rest of the arguments are returned as the
315
second element of the return tuple.
317
Returns a tuple of the form ([service_name, ...], remaining_argv).
319
if len(args) > 1 and args[0] == '-r':
320
return args[1].split(','), args[2:]
324
def process_config_arguments(args):
325
"""Process the arguments related to the config.
327
-i Will set the instance name aka LPCONFIG env.
329
If there is no ZConfig file passed, one will add to the argument
330
based on the selected instance.
333
index = args.index('-i')
334
config.setInstance(args[index + 1])
335
del args[index:index + 2]
338
zope_config_file = config.zope_config_file
339
if not os.path.isfile(zope_config_file):
341
"Cannot find ZConfig file for instance %s: %s" % (
342
config.instance_name, zope_config_file))
343
args.extend(['-C', zope_config_file])
347
def start_testapp(argv=list(sys.argv)):
348
from canonical.config.fixture import ConfigUseFixture
349
from canonical.testing.layers import (
352
LayerProcessController,
356
from lp.testing.pgsql import (
358
uninstallFakeConnect,
360
assert config.instance_name.startswith('testrunner-appserver'), (
361
'%r does not start with "testrunner-appserver"' %
362
config.instance_name)
363
interactive_tests = 'INTERACTIVE_TESTS' in os.environ
367
# This code needs to be run after other zcml setup happens in
368
# runlaunchpad, so it is passed in as a callable. We set up layers
369
# here because we need to control fixtures within this process, and
370
# because we want interactive tests to be as similar as possible to
371
# tests run in the testrunner.
372
# Note that this changes the config instance-name, with the result
373
# that the configuration of utilities may become invalidated.
374
# XXX Robert Collins, bug=883980: In short, we should derive the
375
# other services from the test runner, rather than duplicating
376
# the work of test setup within the slave appserver. That will
377
# permit reuse of the librarian, DB, rabbit etc, and
378
# correspondingly easier assertions and inspection of interactions
379
# with other services. That would mean we do not need to set up rabbit
380
# or the librarian here: the test runner would control and take care
383
teardowns.append(BaseLayer.tearDown)
384
RabbitMQLayer.setUp()
385
teardowns.append(RabbitMQLayer.tearDown)
386
# We set up the database here even for the test suite because we want
387
# to be able to control the database here in the subprocess. It is
388
# possible to do that when setting the database up in the parent
389
# process, but it is messier. This is simple.
391
teardowns.append(uninstallFakeConnect)
392
DatabaseLayer.setUp()
393
teardowns.append(DatabaseLayer.tearDown)
394
# The Librarian needs access to the database, so setting it up here
395
# where we are setting up the database makes the most sense.
396
LibrarianLayer.setUp()
397
teardowns.append(LibrarianLayer.tearDown)
398
# Switch to the appserver config.
399
fixture = ConfigUseFixture(BaseLayer.appserver_config_name)
401
teardowns.append(fixture.cleanUp)
402
# Interactive tests always need this. We let functional tests use
403
# a local one too because of simplicity.
404
LayerProcessController.startSMTPServer()
405
teardowns.append(LayerProcessController.stopSMTPServer)
406
if interactive_tests:
407
root_url = config.appserver_root_url()
409
print 'In a few seconds, go to ' + root_url + '/+yuitest'
412
start_launchpad(argv, setup)
415
for teardown in teardowns:
418
except NotImplementedError:
419
# We are in a separate process anyway. Bah.
423
def start_launchpad(argv=list(sys.argv), setup=None):
424
# We really want to replace this with a generic startup harness.
425
# However, this should last us until this is developed
426
services, argv = split_out_runlaunchpad_arguments(argv[1:])
427
argv = process_config_arguments(argv)
428
services = get_services_to_run(services)
429
# Create the ZCML override file based on the instance.
430
config.generate_overrides()
431
# Many things rely on a directory called 'logs' existing in the current
433
ensure_directory_exists('logs')
434
if setup is not None:
435
# This is the setup from start_testapp, above.
438
with nested(*services):
439
# Store our process id somewhere
440
make_pidfile('launchpad')
441
if config.launchpad.launch:
444
# We just need the foreground process to sit around forever
445
# waiting for the signal to shut everything down. Normally,
446
# Zope itself would be this master process, but we're not
447
# starting that up, so we need to do something else.
450
except KeyboardInterrupt:
453
print >> sys.stderr, "stopping services on exception %r" % e
454
for service in services:
455
print >> sys.stderr, service, "fixture details:"
456
# There may be no details on some services if they haven't been
458
if getattr(service, '_details', None) is None:
459
print >> sys.stderr, "(not ready yet?)"
461
details_str = _details_to_str(service.getDetails())
463
print >> sys.stderr, details_str
465
print >> sys.stderr, "(no details present)"
469
def start_librarian():
470
"""Start the Librarian in the background."""
471
# Create the ZCML override file based on the instance.
472
config.generate_overrides()
473
# Create the Librarian storage directory if it doesn't already exist.
474
prepare_for_librarian()
475
pidfile = pidfile_path('librarian')
477
tachandler.twistd_script,
478
"--python", 'daemons/librarian.tac',
479
"--pidfile", pidfile,
480
"--prefix", 'Librarian',
481
"--logfile", config.librarian_server.logfile,
483
return subprocess.call(cmd)