3
# This Source Code Form is subject to the terms of the Mozilla Public
4
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
5
# You can obtain one at http://mozilla.org/MPL/2.0/.
7
__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cli', 'package_metadata']
17
from utils import get_metadata_from_egg
18
from utils import findInPath
19
from mozprofile import *
20
from mozprocess.processhandler import ProcessHandler
23
from plistlib import readPlist
25
package_metadata = get_metadata_from_egg('mozrunner')
27
# Map of debugging programs to information about them
28
# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
29
debuggers = {'gdb': {'interactive': True,
30
'args': ['-q', '--args'],},
31
'valgrind': {'interactive': False,
32
'args': ['--leak-check=full']}
35
def debugger_arguments(debugger, arguments=None, interactive=None):
37
finds debugger arguments from debugger given and defaults
38
* debugger : debugger name or path to debugger
39
* arguments : arguments to the debugger, or None to use defaults
40
* interactive : whether the debugger should be run in interactive mode, or None to use default
43
# find debugger executable if not a file
45
if not os.path.exists(executable):
46
executable = findInPath(debugger)
47
if executable is None:
48
raise Exception("Path to '%s' not found" % debugger)
50
# if debugger not in dictionary of knowns return defaults
51
dirname, debugger = os.path.split(debugger)
52
if debugger not in debuggers:
53
return ([executable] + (arguments or []), bool(interactive))
55
# otherwise use the dictionary values for arguments unless specified
57
arguments = debuggers[debugger].get('args', [])
58
if interactive is None:
59
interactive = debuggers[debugger].get('interactive', False)
60
return ([executable] + arguments, interactive)
63
"""Handles all running operations. Finds bins, runs and kills the process."""
65
profile_class = Profile # profile class to use by default
68
def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
69
clean_profile=True, process_class=ProcessHandler):
70
profile = cls.profile_class(**(profile_args or {}))
71
return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
72
clean_profile=clean_profile, process_class=process_class)
74
def __init__(self, profile, binary, cmdargs=None, env=None,
75
kp_kwargs=None, clean_profile=True, process_class=ProcessHandler):
76
self.process_handler = None
77
self.process_class = process_class
78
self.profile = profile
79
self.clean_profile = clean_profile
84
raise Exception("Binary not specified")
85
if not os.path.exists(self.binary):
86
raise OSError("Binary path does not exist: %s" % self.binary)
88
# allow Mac binaries to be specified as an app bundle
89
plist = '%s/Contents/Info.plist' % self.binary
90
if mozinfo.isMac and os.path.exists(plist):
91
info = readPlist(plist)
92
self.binary = os.path.join(self.binary, "Contents/MacOS/",
93
info['CFBundleExecutable'])
95
self.cmdargs = cmdargs or []
96
_cmdargs = [i for i in self.cmdargs
97
if i != '-foreground']
98
if len(_cmdargs) != len(self.cmdargs):
99
# foreground should be last; see
100
# - https://bugzilla.mozilla.org/show_bug.cgi?id=625614
101
# - https://bugzilla.mozilla.org/show_bug.cgi?id=626826
102
self.cmdargs = _cmdargs
103
self.cmdargs.append('-foreground')
105
# process environment
107
self.env = os.environ.copy()
109
self.env = env.copy()
110
# allows you to run an instance of Firefox separately from any other instances
111
self.env['MOZ_NO_REMOTE'] = '1'
112
# keeps Firefox attached to the terminal window after it starts
113
self.env['NO_EM_RESTART'] = '1'
115
# set the library path if needed on linux
116
if sys.platform == 'linux2' and self.binary.endswith('-bin'):
117
dirname = os.path.dirname(self.binary)
118
if os.environ.get('LD_LIBRARY_PATH', None):
119
self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
121
self.env['LD_LIBRARY_PATH'] = dirname
123
# arguments for ProfessHandler.Process
124
self.kp_kwargs = kp_kwargs or {}
128
"""Returns the command list to run."""
129
return [self.binary, '-profile', self.profile.profile]
131
def get_repositoryInfo(self):
132
"""Read repository information from application.ini and platform.ini."""
134
config = ConfigParser.RawConfigParser()
135
dirname = os.path.dirname(self.binary)
138
for file, section in [('application', 'App'), ('platform', 'Build')]:
139
config.read(os.path.join(dirname, '%s.ini' % file))
141
for key, id in [('SourceRepository', 'repository'),
142
('SourceStamp', 'changeset')]:
144
repository['%s_%s' % (file, id)] = config.get(section, key);
146
repository['%s_%s' % (file, id)] = None
150
def is_running(self):
151
return self.process_handler is not None
153
def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None):
155
Run self.command in the proper environment.
156
- debug_args: arguments for the debugger
157
- interactive: uses subprocess.Popen directly
158
- read_output: sends program output to stdout [default=False]
159
- timeout: see process_handler.waitForFinish
160
- outputTimeout: see process_handler.waitForFinish
163
# ensure you are stopped
166
# ensure the profile exists
167
if not self.profile.exists():
169
assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__
171
cmd = self._wrap_command(self.command+self.cmdargs)
173
# attach a debugger, if specified
175
cmd = list(debug_args) + cmd
178
self.process_handler = subprocess.Popen(cmd, env=self.env)
179
# TODO: other arguments
181
# this run uses the managed processhandler
182
self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs)
183
self.process_handler.run()
185
# start processing output from the process
186
self.process_handler.processOutput(timeout, outputTimeout)
188
def wait(self, timeout=None):
190
Wait for the app to exit.
192
If timeout is not None, will return after timeout seconds.
193
Use is_running() to determine whether or not a timeout occured.
194
Timeout is ignored if interactive was set to True.
196
if self.process_handler is None:
198
if isinstance(self.process_handler, subprocess.Popen):
199
self.process_handler.wait()
201
self.process_handler.waitForFinish(timeout)
202
if not getattr(self.process_handler.proc, 'returncode', False):
203
# waitForFinish timed out
205
self.process_handler = None
209
if self.process_handler is None:
211
self.process_handler.kill()
212
self.process_handler = None
216
reset the runner between runs
217
currently, only resets the profile, but probably should do more
223
if self.clean_profile:
224
self.profile.cleanup()
226
def _wrap_command(self, cmd):
228
If running on OS X 10.5 or older, wrap |cmd| so that it will
229
be executed as an i386 binary, in case it's a 32-bit/64-bit universal
232
if mozinfo.isMac and hasattr(platform, 'mac_ver') and \
233
platform.mac_ver()[0][:4] < '10.6':
234
return ["arch", "-arch", "i386"] + cmd
240
class FirefoxRunner(Runner):
241
"""Specialized Runner subclass for running Firefox."""
243
profile_class = FirefoxProfile
245
def __init__(self, profile, binary=None, **kwargs):
247
# take the binary from BROWSER_PATH environment variable
248
if (not binary) and 'BROWSER_PATH' in os.environ:
249
binary = os.environ['BROWSER_PATH']
251
Runner.__init__(self, profile, binary, **kwargs)
253
class ThunderbirdRunner(Runner):
254
"""Specialized Runner subclass for running Thunderbird"""
255
profile_class = ThunderbirdProfile
257
runners = {'firefox': FirefoxRunner,
258
'thunderbird': ThunderbirdRunner}
260
class CLI(MozProfileCLI):
261
"""Command line interface."""
265
def __init__(self, args=sys.argv[1:]):
267
Setup command line parser and parse arguments
268
- args : command line arguments
271
self.metadata = getattr(sys.modules[self.module],
274
version = self.metadata.get('Version')
275
parser_args = {'description': self.metadata.get('Summary')}
277
parser_args['version'] = "%prog " + version
278
self.parser = optparse.OptionParser(**parser_args)
279
self.add_options(self.parser)
280
(self.options, self.args) = self.parser.parse_args(args)
282
if getattr(self.options, 'info', None):
283
self.print_metadata()
286
# choose appropriate runner and profile classes
288
self.runner_class = runners[self.options.app]
290
self.parser.error('Application "%s" unknown (should be one of "firefox" or "thunderbird")' % self.options.app)
292
def add_options(self, parser):
293
"""add options to the parser"""
295
# add profile options
296
MozProfileCLI.add_options(self, parser)
299
parser.add_option('-b', "--binary",
300
dest="binary", help="Binary path.",
301
metavar=None, default=None)
302
parser.add_option('--app', dest='app', default='firefox',
303
help="Application to use [DEFAULT: %default]")
304
parser.add_option('--app-arg', dest='appArgs',
305
default=[], action='append',
306
help="provides an argument to the test application")
307
parser.add_option('--debugger', dest='debugger',
308
help="run under a debugger, e.g. gdb or valgrind")
309
parser.add_option('--debugger-args', dest='debugger_args',
310
action='append', default=None,
311
help="arguments to the debugger")
312
parser.add_option('--interactive', dest='interactive',
314
help="run the program interactively")
316
parser.add_option("--info", dest="info", default=False,
318
help="Print module information")
320
### methods for introspecting data
322
def get_metadata_from_egg(self):
325
dist = pkg_resources.get_distribution(self.module)
326
if dist.has_metadata("PKG-INFO"):
327
for line in dist.get_metadata_lines("PKG-INFO"):
328
key, value = line.split(':', 1)
330
if dist.has_metadata("requires.txt"):
331
ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
334
def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
335
"Author", "Author-email", "License", "Platform", "Dependencies")):
337
if key in self.metadata:
338
print key + ": " + self.metadata[key]
340
### methods for running
342
def command_args(self):
343
"""additional arguments for the mozilla application"""
344
return self.options.appArgs
346
def runner_args(self):
347
"""arguments to instantiate the runner class"""
348
return dict(cmdargs=self.command_args(),
349
binary=self.options.binary,
350
profile_args=self.profile_args())
352
def create_runner(self):
353
return self.runner_class.create(**self.runner_args())
356
runner = self.create_runner()
360
def debugger_arguments(self):
362
returns a 2-tuple of debugger arguments:
363
(debugger_arguments, interactive)
365
debug_args = self.options.debugger_args
366
interactive = self.options.interactive
367
if self.options.debugger:
368
debug_args, interactive = debugger_arguments(self.options.debugger)
369
return debug_args, interactive
371
def start(self, runner):
372
"""Starts the runner and waits for Firefox to exit or Keyboard Interrupt.
373
Shoule be overwritten to provide custom running of the runner instance."""
375
# attach a debugger if specified
376
debug_args, interactive = self.debugger_arguments()
377
runner.start(debug_args=debug_args, interactive=interactive)
378
print 'Starting:', ' '.join(runner.command)
381
except KeyboardInterrupt:
385
def cli(args=sys.argv[1:]):
388
if __name__ == '__main__':