Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
michael@0 | 1 | #!/usr/bin/env python |
michael@0 | 2 | |
michael@0 | 3 | # This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 5 | # You can obtain one at http://mozilla.org/MPL/2.0/. |
michael@0 | 6 | |
michael@0 | 7 | import ConfigParser |
michael@0 | 8 | import mozinfo |
michael@0 | 9 | import optparse |
michael@0 | 10 | import os |
michael@0 | 11 | import platform |
michael@0 | 12 | import subprocess |
michael@0 | 13 | import sys |
michael@0 | 14 | |
michael@0 | 15 | if mozinfo.isMac: |
michael@0 | 16 | from plistlib import readPlist |
michael@0 | 17 | |
michael@0 | 18 | from mozprofile import Profile, FirefoxProfile, MetroFirefoxProfile, ThunderbirdProfile, MozProfileCLI |
michael@0 | 19 | |
michael@0 | 20 | from .base import Runner |
michael@0 | 21 | from .utils import findInPath, get_metadata_from_egg |
michael@0 | 22 | |
michael@0 | 23 | |
michael@0 | 24 | __all__ = ['CLI', |
michael@0 | 25 | 'cli', |
michael@0 | 26 | 'LocalRunner', |
michael@0 | 27 | 'local_runners', |
michael@0 | 28 | 'package_metadata', |
michael@0 | 29 | 'FirefoxRunner', |
michael@0 | 30 | 'MetroFirefoxRunner', |
michael@0 | 31 | 'ThunderbirdRunner'] |
michael@0 | 32 | |
michael@0 | 33 | |
michael@0 | 34 | package_metadata = get_metadata_from_egg('mozrunner') |
michael@0 | 35 | |
michael@0 | 36 | |
michael@0 | 37 | # Map of debugging programs to information about them |
michael@0 | 38 | # from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59 |
michael@0 | 39 | debuggers = {'gdb': {'interactive': True, |
michael@0 | 40 | 'args': ['-q', '--args'],}, |
michael@0 | 41 | 'valgrind': {'interactive': False, |
michael@0 | 42 | 'args': ['--leak-check=full']} |
michael@0 | 43 | } |
michael@0 | 44 | |
michael@0 | 45 | |
michael@0 | 46 | def debugger_arguments(debugger, arguments=None, interactive=None): |
michael@0 | 47 | """Finds debugger arguments from debugger given and defaults |
michael@0 | 48 | |
michael@0 | 49 | :param debugger: name or path to debugger |
michael@0 | 50 | :param arguments: arguments for the debugger, or None to use defaults |
michael@0 | 51 | :param interactive: whether the debugger should run in interactive mode |
michael@0 | 52 | |
michael@0 | 53 | """ |
michael@0 | 54 | # find debugger executable if not a file |
michael@0 | 55 | executable = debugger |
michael@0 | 56 | if not os.path.exists(executable): |
michael@0 | 57 | executable = findInPath(debugger) |
michael@0 | 58 | if executable is None: |
michael@0 | 59 | raise Exception("Path to '%s' not found" % debugger) |
michael@0 | 60 | |
michael@0 | 61 | # if debugger not in dictionary of knowns return defaults |
michael@0 | 62 | dirname, debugger = os.path.split(debugger) |
michael@0 | 63 | if debugger not in debuggers: |
michael@0 | 64 | return ([executable] + (arguments or []), bool(interactive)) |
michael@0 | 65 | |
michael@0 | 66 | # otherwise use the dictionary values for arguments unless specified |
michael@0 | 67 | if arguments is None: |
michael@0 | 68 | arguments = debuggers[debugger].get('args', []) |
michael@0 | 69 | if interactive is None: |
michael@0 | 70 | interactive = debuggers[debugger].get('interactive', False) |
michael@0 | 71 | return ([executable] + arguments, interactive) |
michael@0 | 72 | |
michael@0 | 73 | |
michael@0 | 74 | class LocalRunner(Runner): |
michael@0 | 75 | """Handles all running operations. Finds bins, runs and kills the process""" |
michael@0 | 76 | |
michael@0 | 77 | profile_class = Profile # profile class to use by default |
michael@0 | 78 | |
michael@0 | 79 | @classmethod |
michael@0 | 80 | def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None, |
michael@0 | 81 | clean_profile=True, process_class=None, **kwargs): |
michael@0 | 82 | profile = cls.profile_class(**(profile_args or {})) |
michael@0 | 83 | return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs, |
michael@0 | 84 | clean_profile=clean_profile, process_class=process_class, **kwargs) |
michael@0 | 85 | |
michael@0 | 86 | def __init__(self, profile, binary, cmdargs=None, env=None, |
michael@0 | 87 | kp_kwargs=None, clean_profile=None, process_class=None, **kwargs): |
michael@0 | 88 | |
michael@0 | 89 | Runner.__init__(self, profile, clean_profile=clean_profile, kp_kwargs=kp_kwargs, |
michael@0 | 90 | process_class=process_class, env=env, **kwargs) |
michael@0 | 91 | |
michael@0 | 92 | # find the binary |
michael@0 | 93 | self.binary = binary |
michael@0 | 94 | if not self.binary: |
michael@0 | 95 | raise Exception("Binary not specified") |
michael@0 | 96 | if not os.path.exists(self.binary): |
michael@0 | 97 | raise OSError("Binary path does not exist: %s" % self.binary) |
michael@0 | 98 | |
michael@0 | 99 | # To be safe the absolute path of the binary should be used |
michael@0 | 100 | self.binary = os.path.abspath(self.binary) |
michael@0 | 101 | |
michael@0 | 102 | # allow Mac binaries to be specified as an app bundle |
michael@0 | 103 | plist = '%s/Contents/Info.plist' % self.binary |
michael@0 | 104 | if mozinfo.isMac and os.path.exists(plist): |
michael@0 | 105 | info = readPlist(plist) |
michael@0 | 106 | self.binary = os.path.join(self.binary, "Contents/MacOS/", |
michael@0 | 107 | info['CFBundleExecutable']) |
michael@0 | 108 | |
michael@0 | 109 | self.cmdargs = cmdargs or [] |
michael@0 | 110 | _cmdargs = [i for i in self.cmdargs |
michael@0 | 111 | if i != '-foreground'] |
michael@0 | 112 | if len(_cmdargs) != len(self.cmdargs): |
michael@0 | 113 | # foreground should be last; see |
michael@0 | 114 | # https://bugzilla.mozilla.org/show_bug.cgi?id=625614 |
michael@0 | 115 | self.cmdargs = _cmdargs |
michael@0 | 116 | self.cmdargs.append('-foreground') |
michael@0 | 117 | if mozinfo.isMac and '-foreground' not in self.cmdargs: |
michael@0 | 118 | # runner should specify '-foreground' on Mac; see |
michael@0 | 119 | # https://bugzilla.mozilla.org/show_bug.cgi?id=916512 |
michael@0 | 120 | self.cmdargs.append('-foreground') |
michael@0 | 121 | |
michael@0 | 122 | # process environment |
michael@0 | 123 | if env is None: |
michael@0 | 124 | self.env = os.environ.copy() |
michael@0 | 125 | else: |
michael@0 | 126 | self.env = env.copy() |
michael@0 | 127 | # allows you to run an instance of Firefox separately from any other instances |
michael@0 | 128 | self.env['MOZ_NO_REMOTE'] = '1' |
michael@0 | 129 | # keeps Firefox attached to the terminal window after it starts |
michael@0 | 130 | self.env['NO_EM_RESTART'] = '1' |
michael@0 | 131 | |
michael@0 | 132 | # set the library path if needed on linux |
michael@0 | 133 | if sys.platform == 'linux2' and self.binary.endswith('-bin'): |
michael@0 | 134 | dirname = os.path.dirname(self.binary) |
michael@0 | 135 | if os.environ.get('LD_LIBRARY_PATH', None): |
michael@0 | 136 | self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) |
michael@0 | 137 | else: |
michael@0 | 138 | self.env['LD_LIBRARY_PATH'] = dirname |
michael@0 | 139 | |
michael@0 | 140 | @property |
michael@0 | 141 | def command(self): |
michael@0 | 142 | """Returns the command list to run""" |
michael@0 | 143 | commands = [self.binary, '-profile', self.profile.profile] |
michael@0 | 144 | |
michael@0 | 145 | # Bug 775416 - Ensure that binary options are passed in first |
michael@0 | 146 | commands[1:1] = self.cmdargs |
michael@0 | 147 | |
michael@0 | 148 | return commands |
michael@0 | 149 | |
michael@0 | 150 | def get_repositoryInfo(self): |
michael@0 | 151 | """Read repository information from application.ini and platform.ini""" |
michael@0 | 152 | config = ConfigParser.RawConfigParser() |
michael@0 | 153 | dirname = os.path.dirname(self.binary) |
michael@0 | 154 | repository = { } |
michael@0 | 155 | |
michael@0 | 156 | for file, section in [('application', 'App'), ('platform', 'Build')]: |
michael@0 | 157 | config.read(os.path.join(dirname, '%s.ini' % file)) |
michael@0 | 158 | |
michael@0 | 159 | for key, id in [('SourceRepository', 'repository'), |
michael@0 | 160 | ('SourceStamp', 'changeset')]: |
michael@0 | 161 | try: |
michael@0 | 162 | repository['%s_%s' % (file, id)] = config.get(section, key); |
michael@0 | 163 | except: |
michael@0 | 164 | repository['%s_%s' % (file, id)] = None |
michael@0 | 165 | |
michael@0 | 166 | return repository |
michael@0 | 167 | |
michael@0 | 168 | |
michael@0 | 169 | class FirefoxRunner(LocalRunner): |
michael@0 | 170 | """Specialized LocalRunner subclass for running Firefox.""" |
michael@0 | 171 | |
michael@0 | 172 | profile_class = FirefoxProfile |
michael@0 | 173 | |
michael@0 | 174 | def __init__(self, profile, binary=None, **kwargs): |
michael@0 | 175 | |
michael@0 | 176 | # if no binary given take it from the BROWSER_PATH environment variable |
michael@0 | 177 | binary = binary or os.environ.get('BROWSER_PATH') |
michael@0 | 178 | LocalRunner.__init__(self, profile, binary, **kwargs) |
michael@0 | 179 | |
michael@0 | 180 | |
michael@0 | 181 | class MetroFirefoxRunner(LocalRunner): |
michael@0 | 182 | """Specialized LocalRunner subclass for running Firefox Metro""" |
michael@0 | 183 | |
michael@0 | 184 | profile_class = MetroFirefoxProfile |
michael@0 | 185 | |
michael@0 | 186 | # helper application to launch Firefox in Metro mode |
michael@0 | 187 | here = os.path.dirname(os.path.abspath(__file__)) |
michael@0 | 188 | immersiveHelperPath = os.path.sep.join([here, |
michael@0 | 189 | 'resources', |
michael@0 | 190 | 'metrotestharness.exe']) |
michael@0 | 191 | |
michael@0 | 192 | def __init__(self, profile, binary=None, **kwargs): |
michael@0 | 193 | |
michael@0 | 194 | # if no binary given take it from the BROWSER_PATH environment variable |
michael@0 | 195 | binary = binary or os.environ.get('BROWSER_PATH') |
michael@0 | 196 | LocalRunner.__init__(self, profile, binary, **kwargs) |
michael@0 | 197 | |
michael@0 | 198 | if not os.path.exists(self.immersiveHelperPath): |
michael@0 | 199 | raise OSError('Can not find Metro launcher: %s' % self.immersiveHelperPath) |
michael@0 | 200 | |
michael@0 | 201 | if not mozinfo.isWin: |
michael@0 | 202 | raise Exception('Firefox Metro mode is only supported on Windows 8 and onwards') |
michael@0 | 203 | |
michael@0 | 204 | @property |
michael@0 | 205 | def command(self): |
michael@0 | 206 | command = LocalRunner.command.fget(self) |
michael@0 | 207 | command[:0] = [self.immersiveHelperPath, '-firefoxpath'] |
michael@0 | 208 | |
michael@0 | 209 | return command |
michael@0 | 210 | |
michael@0 | 211 | |
michael@0 | 212 | class ThunderbirdRunner(LocalRunner): |
michael@0 | 213 | """Specialized LocalRunner subclass for running Thunderbird""" |
michael@0 | 214 | profile_class = ThunderbirdProfile |
michael@0 | 215 | |
michael@0 | 216 | |
michael@0 | 217 | local_runners = {'firefox': FirefoxRunner, |
michael@0 | 218 | 'metrofirefox' : MetroFirefoxRunner, |
michael@0 | 219 | 'thunderbird': ThunderbirdRunner} |
michael@0 | 220 | |
michael@0 | 221 | |
michael@0 | 222 | class CLI(MozProfileCLI): |
michael@0 | 223 | """Command line interface""" |
michael@0 | 224 | |
michael@0 | 225 | module = "mozrunner" |
michael@0 | 226 | |
michael@0 | 227 | def __init__(self, args=sys.argv[1:]): |
michael@0 | 228 | self.metadata = getattr(sys.modules[self.module], |
michael@0 | 229 | 'package_metadata', |
michael@0 | 230 | {}) |
michael@0 | 231 | version = self.metadata.get('Version') |
michael@0 | 232 | parser_args = {'description': self.metadata.get('Summary')} |
michael@0 | 233 | if version: |
michael@0 | 234 | parser_args['version'] = "%prog " + version |
michael@0 | 235 | self.parser = optparse.OptionParser(**parser_args) |
michael@0 | 236 | self.add_options(self.parser) |
michael@0 | 237 | (self.options, self.args) = self.parser.parse_args(args) |
michael@0 | 238 | |
michael@0 | 239 | if getattr(self.options, 'info', None): |
michael@0 | 240 | self.print_metadata() |
michael@0 | 241 | sys.exit(0) |
michael@0 | 242 | |
michael@0 | 243 | # choose appropriate runner and profile classes |
michael@0 | 244 | try: |
michael@0 | 245 | self.runner_class = local_runners[self.options.app] |
michael@0 | 246 | except KeyError: |
michael@0 | 247 | self.parser.error('Application "%s" unknown (should be one of "%s")' % |
michael@0 | 248 | (self.options.app, ', '.join(local_runners.keys()))) |
michael@0 | 249 | |
michael@0 | 250 | def add_options(self, parser): |
michael@0 | 251 | """add options to the parser""" |
michael@0 | 252 | |
michael@0 | 253 | # add profile options |
michael@0 | 254 | MozProfileCLI.add_options(self, parser) |
michael@0 | 255 | |
michael@0 | 256 | # add runner options |
michael@0 | 257 | parser.add_option('-b', "--binary", |
michael@0 | 258 | dest="binary", help="Binary path.", |
michael@0 | 259 | metavar=None, default=None) |
michael@0 | 260 | parser.add_option('--app', dest='app', default='firefox', |
michael@0 | 261 | help="Application to use [DEFAULT: %default]") |
michael@0 | 262 | parser.add_option('--app-arg', dest='appArgs', |
michael@0 | 263 | default=[], action='append', |
michael@0 | 264 | help="provides an argument to the test application") |
michael@0 | 265 | parser.add_option('--debugger', dest='debugger', |
michael@0 | 266 | help="run under a debugger, e.g. gdb or valgrind") |
michael@0 | 267 | parser.add_option('--debugger-args', dest='debugger_args', |
michael@0 | 268 | action='store', |
michael@0 | 269 | help="arguments to the debugger") |
michael@0 | 270 | parser.add_option('--interactive', dest='interactive', |
michael@0 | 271 | action='store_true', |
michael@0 | 272 | help="run the program interactively") |
michael@0 | 273 | if self.metadata: |
michael@0 | 274 | parser.add_option("--info", dest="info", default=False, |
michael@0 | 275 | action="store_true", |
michael@0 | 276 | help="Print module information") |
michael@0 | 277 | |
michael@0 | 278 | ### methods for introspecting data |
michael@0 | 279 | |
michael@0 | 280 | def get_metadata_from_egg(self): |
michael@0 | 281 | import pkg_resources |
michael@0 | 282 | ret = {} |
michael@0 | 283 | dist = pkg_resources.get_distribution(self.module) |
michael@0 | 284 | if dist.has_metadata("PKG-INFO"): |
michael@0 | 285 | for line in dist.get_metadata_lines("PKG-INFO"): |
michael@0 | 286 | key, value = line.split(':', 1) |
michael@0 | 287 | ret[key] = value |
michael@0 | 288 | if dist.has_metadata("requires.txt"): |
michael@0 | 289 | ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") |
michael@0 | 290 | return ret |
michael@0 | 291 | |
michael@0 | 292 | def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", |
michael@0 | 293 | "Author", "Author-email", "License", "Platform", "Dependencies")): |
michael@0 | 294 | for key in data: |
michael@0 | 295 | if key in self.metadata: |
michael@0 | 296 | print key + ": " + self.metadata[key] |
michael@0 | 297 | |
michael@0 | 298 | ### methods for running |
michael@0 | 299 | |
michael@0 | 300 | def command_args(self): |
michael@0 | 301 | """additional arguments for the mozilla application""" |
michael@0 | 302 | return map(os.path.expanduser, self.options.appArgs) |
michael@0 | 303 | |
michael@0 | 304 | def runner_args(self): |
michael@0 | 305 | """arguments to instantiate the runner class""" |
michael@0 | 306 | return dict(cmdargs=self.command_args(), |
michael@0 | 307 | binary=self.options.binary, |
michael@0 | 308 | profile_args=self.profile_args()) |
michael@0 | 309 | |
michael@0 | 310 | def create_runner(self): |
michael@0 | 311 | return self.runner_class.create(**self.runner_args()) |
michael@0 | 312 | |
michael@0 | 313 | def run(self): |
michael@0 | 314 | runner = self.create_runner() |
michael@0 | 315 | self.start(runner) |
michael@0 | 316 | runner.cleanup() |
michael@0 | 317 | |
michael@0 | 318 | def debugger_arguments(self): |
michael@0 | 319 | """Get the debugger arguments |
michael@0 | 320 | |
michael@0 | 321 | returns a 2-tuple of debugger arguments: |
michael@0 | 322 | (debugger_arguments, interactive) |
michael@0 | 323 | |
michael@0 | 324 | """ |
michael@0 | 325 | debug_args = self.options.debugger_args |
michael@0 | 326 | if debug_args is not None: |
michael@0 | 327 | debug_args = debug_args.split() |
michael@0 | 328 | interactive = self.options.interactive |
michael@0 | 329 | if self.options.debugger: |
michael@0 | 330 | debug_args, interactive = debugger_arguments(self.options.debugger, debug_args, interactive) |
michael@0 | 331 | return debug_args, interactive |
michael@0 | 332 | |
michael@0 | 333 | def start(self, runner): |
michael@0 | 334 | """Starts the runner and waits for the application to exit |
michael@0 | 335 | |
michael@0 | 336 | It can also happen via a keyboard interrupt. It should be |
michael@0 | 337 | overwritten to provide custom running of the runner instance. |
michael@0 | 338 | |
michael@0 | 339 | """ |
michael@0 | 340 | # attach a debugger if specified |
michael@0 | 341 | debug_args, interactive = self.debugger_arguments() |
michael@0 | 342 | runner.start(debug_args=debug_args, interactive=interactive) |
michael@0 | 343 | print 'Starting: ' + ' '.join(runner.command) |
michael@0 | 344 | try: |
michael@0 | 345 | runner.wait() |
michael@0 | 346 | except KeyboardInterrupt: |
michael@0 | 347 | runner.stop() |
michael@0 | 348 | |
michael@0 | 349 | |
michael@0 | 350 | def cli(args=sys.argv[1:]): |
michael@0 | 351 | CLI(args).run() |
michael@0 | 352 | |
michael@0 | 353 | |
michael@0 | 354 | if __name__ == '__main__': |
michael@0 | 355 | cli() |