OLD | NEW |
(Empty) | |
| 1 #!/usr/bin/env python |
| 2 |
| 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/. |
| 6 |
| 7 __all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cl
i', 'package_metadata'] |
| 8 |
| 9 import mozinfo |
| 10 import optparse |
| 11 import os |
| 12 import platform |
| 13 import subprocess |
| 14 import sys |
| 15 import ConfigParser |
| 16 |
| 17 from utils import get_metadata_from_egg |
| 18 from utils import findInPath |
| 19 from mozprofile import * |
| 20 from mozprocess.processhandler import ProcessHandler |
| 21 |
| 22 if mozinfo.isMac: |
| 23 from plistlib import readPlist |
| 24 |
| 25 package_metadata = get_metadata_from_egg('mozrunner') |
| 26 |
| 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']} |
| 33 } |
| 34 |
| 35 def debugger_arguments(debugger, arguments=None, interactive=None): |
| 36 """ |
| 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 N
one to use default |
| 41 """ |
| 42 |
| 43 # find debugger executable if not a file |
| 44 executable = debugger |
| 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) |
| 49 |
| 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)) |
| 54 |
| 55 # otherwise use the dictionary values for arguments unless specified |
| 56 if arguments is None: |
| 57 arguments = debuggers[debugger].get('args', []) |
| 58 if interactive is None: |
| 59 interactive = debuggers[debugger].get('interactive', False) |
| 60 return ([executable] + arguments, interactive) |
| 61 |
| 62 class Runner(object): |
| 63 """Handles all running operations. Finds bins, runs and kills the process.""
" |
| 64 |
| 65 profile_class = Profile # profile class to use by default |
| 66 |
| 67 @classmethod |
| 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=k
p_kwargs, |
| 72 clean_profile=clean_profile, process_
class=process_class) |
| 73 |
| 74 def __init__(self, profile, binary, cmdargs=None, env=None, |
| 75 kp_kwargs=None, clean_profile=True, process_class=ProcessHandle
r): |
| 76 self.process_handler = None |
| 77 self.process_class = process_class |
| 78 self.profile = profile |
| 79 self.clean_profile = clean_profile |
| 80 |
| 81 # find the binary |
| 82 self.binary = binary |
| 83 if not self.binary: |
| 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) |
| 87 |
| 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']) |
| 94 |
| 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') |
| 104 |
| 105 # process environment |
| 106 if env is None: |
| 107 self.env = os.environ.copy() |
| 108 else: |
| 109 self.env = env.copy() |
| 110 # allows you to run an instance of Firefox separately from any other ins
tances |
| 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' |
| 114 |
| 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) |
| 120 else: |
| 121 self.env['LD_LIBRARY_PATH'] = dirname |
| 122 |
| 123 # arguments for ProfessHandler.Process |
| 124 self.kp_kwargs = kp_kwargs or {} |
| 125 |
| 126 @property |
| 127 def command(self): |
| 128 """Returns the command list to run.""" |
| 129 return [self.binary, '-profile', self.profile.profile] |
| 130 |
| 131 def get_repositoryInfo(self): |
| 132 """Read repository information from application.ini and platform.ini.""" |
| 133 |
| 134 config = ConfigParser.RawConfigParser() |
| 135 dirname = os.path.dirname(self.binary) |
| 136 repository = { } |
| 137 |
| 138 for file, section in [('application', 'App'), ('platform', 'Build')]: |
| 139 config.read(os.path.join(dirname, '%s.ini' % file)) |
| 140 |
| 141 for key, id in [('SourceRepository', 'repository'), |
| 142 ('SourceStamp', 'changeset')]: |
| 143 try: |
| 144 repository['%s_%s' % (file, id)] = config.get(section, key); |
| 145 except: |
| 146 repository['%s_%s' % (file, id)] = None |
| 147 |
| 148 return repository |
| 149 |
| 150 def is_running(self): |
| 151 return self.process_handler is not None |
| 152 |
| 153 def start(self, debug_args=None, interactive=False, timeout=None, outputTime
out=None): |
| 154 """ |
| 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 |
| 161 """ |
| 162 |
| 163 # ensure you are stopped |
| 164 self.stop() |
| 165 |
| 166 # ensure the profile exists |
| 167 if not self.profile.exists(): |
| 168 self.profile.reset() |
| 169 assert self.profile.exists(), "%s : failure to reset profile" % self
.__class__.__name__ |
| 170 |
| 171 cmd = self._wrap_command(self.command+self.cmdargs) |
| 172 |
| 173 # attach a debugger, if specified |
| 174 if debug_args: |
| 175 cmd = list(debug_args) + cmd |
| 176 |
| 177 if interactive: |
| 178 self.process_handler = subprocess.Popen(cmd, env=self.env) |
| 179 # TODO: other arguments |
| 180 else: |
| 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(timeout, outputTimeout) |
| 184 |
| 185 def wait(self, timeout=None): |
| 186 """ |
| 187 Wait for the app to exit. |
| 188 |
| 189 If timeout is not None, will return after timeout seconds. |
| 190 Use is_running() to determine whether or not a timeout occured. |
| 191 Timeout is ignored if interactive was set to True. |
| 192 """ |
| 193 if self.process_handler is None: |
| 194 return |
| 195 |
| 196 if isinstance(self.process_handler, subprocess.Popen): |
| 197 self.process_handler.wait() |
| 198 else: |
| 199 self.process_handler.wait(timeout) |
| 200 if self.process_handler.proc.poll() is None: |
| 201 # waitForFinish timed out |
| 202 return |
| 203 |
| 204 self.process_handler = None |
| 205 |
| 206 def stop(self): |
| 207 """Kill the app""" |
| 208 if self.process_handler is None: |
| 209 return |
| 210 self.process_handler.kill() |
| 211 self.process_handler = None |
| 212 |
| 213 def reset(self): |
| 214 """ |
| 215 reset the runner between runs |
| 216 currently, only resets the profile, but probably should do more |
| 217 """ |
| 218 self.profile.reset() |
| 219 |
| 220 def cleanup(self): |
| 221 self.stop() |
| 222 if self.clean_profile: |
| 223 self.profile.cleanup() |
| 224 |
| 225 def _wrap_command(self, cmd): |
| 226 """ |
| 227 If running on OS X 10.5 or older, wrap |cmd| so that it will |
| 228 be executed as an i386 binary, in case it's a 32-bit/64-bit universal |
| 229 binary. |
| 230 """ |
| 231 if mozinfo.isMac and hasattr(platform, 'mac_ver') and \ |
| 232 platform.mac_ver()[0][:4] < '10.6': |
| 233 return ["arch", "-arch", "i386"] + cmd |
| 234 return cmd |
| 235 |
| 236 __del__ = cleanup |
| 237 |
| 238 |
| 239 class FirefoxRunner(Runner): |
| 240 """Specialized Runner subclass for running Firefox.""" |
| 241 |
| 242 profile_class = FirefoxProfile |
| 243 |
| 244 def __init__(self, profile, binary=None, **kwargs): |
| 245 |
| 246 # take the binary from BROWSER_PATH environment variable |
| 247 if (not binary) and 'BROWSER_PATH' in os.environ: |
| 248 binary = os.environ['BROWSER_PATH'] |
| 249 |
| 250 Runner.__init__(self, profile, binary, **kwargs) |
| 251 |
| 252 class ThunderbirdRunner(Runner): |
| 253 """Specialized Runner subclass for running Thunderbird""" |
| 254 profile_class = ThunderbirdProfile |
| 255 |
| 256 runners = {'firefox': FirefoxRunner, |
| 257 'thunderbird': ThunderbirdRunner} |
| 258 |
| 259 class CLI(MozProfileCLI): |
| 260 """Command line interface.""" |
| 261 |
| 262 module = "mozrunner" |
| 263 |
| 264 def __init__(self, args=sys.argv[1:]): |
| 265 """ |
| 266 Setup command line parser and parse arguments |
| 267 - args : command line arguments |
| 268 """ |
| 269 |
| 270 self.metadata = getattr(sys.modules[self.module], |
| 271 'package_metadata', |
| 272 {}) |
| 273 version = self.metadata.get('Version') |
| 274 parser_args = {'description': self.metadata.get('Summary')} |
| 275 if version: |
| 276 parser_args['version'] = "%prog " + version |
| 277 self.parser = optparse.OptionParser(**parser_args) |
| 278 self.add_options(self.parser) |
| 279 (self.options, self.args) = self.parser.parse_args(args) |
| 280 |
| 281 if getattr(self.options, 'info', None): |
| 282 self.print_metadata() |
| 283 sys.exit(0) |
| 284 |
| 285 # choose appropriate runner and profile classes |
| 286 try: |
| 287 self.runner_class = runners[self.options.app] |
| 288 except KeyError: |
| 289 self.parser.error('Application "%s" unknown (should be one of "firef
ox" or "thunderbird")' % self.options.app) |
| 290 |
| 291 def add_options(self, parser): |
| 292 """add options to the parser""" |
| 293 |
| 294 # add profile options |
| 295 MozProfileCLI.add_options(self, parser) |
| 296 |
| 297 # add runner options |
| 298 parser.add_option('-b', "--binary", |
| 299 dest="binary", help="Binary path.", |
| 300 metavar=None, default=None) |
| 301 parser.add_option('--app', dest='app', default='firefox', |
| 302 help="Application to use [DEFAULT: %default]") |
| 303 parser.add_option('--app-arg', dest='appArgs', |
| 304 default=[], action='append', |
| 305 help="provides an argument to the test application") |
| 306 parser.add_option('--debugger', dest='debugger', |
| 307 help="run under a debugger, e.g. gdb or valgrind") |
| 308 parser.add_option('--debugger-args', dest='debugger_args', |
| 309 action='append', default=None, |
| 310 help="arguments to the debugger") |
| 311 parser.add_option('--interactive', dest='interactive', |
| 312 action='store_true', |
| 313 help="run the program interactively") |
| 314 if self.metadata: |
| 315 parser.add_option("--info", dest="info", default=False, |
| 316 action="store_true", |
| 317 help="Print module information") |
| 318 |
| 319 ### methods for introspecting data |
| 320 |
| 321 def get_metadata_from_egg(self): |
| 322 import pkg_resources |
| 323 ret = {} |
| 324 dist = pkg_resources.get_distribution(self.module) |
| 325 if dist.has_metadata("PKG-INFO"): |
| 326 for line in dist.get_metadata_lines("PKG-INFO"): |
| 327 key, value = line.split(':', 1) |
| 328 ret[key] = value |
| 329 if dist.has_metadata("requires.txt"): |
| 330 ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") |
| 331 return ret |
| 332 |
| 333 def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", |
| 334 "Author", "Author-email", "License", "Platfor
m", "Dependencies")): |
| 335 for key in data: |
| 336 if key in self.metadata: |
| 337 print key + ": " + self.metadata[key] |
| 338 |
| 339 ### methods for running |
| 340 |
| 341 def command_args(self): |
| 342 """additional arguments for the mozilla application""" |
| 343 return self.options.appArgs |
| 344 |
| 345 def runner_args(self): |
| 346 """arguments to instantiate the runner class""" |
| 347 return dict(cmdargs=self.command_args(), |
| 348 binary=self.options.binary, |
| 349 profile_args=self.profile_args()) |
| 350 |
| 351 def create_runner(self): |
| 352 return self.runner_class.create(**self.runner_args()) |
| 353 |
| 354 def run(self): |
| 355 runner = self.create_runner() |
| 356 self.start(runner) |
| 357 runner.cleanup() |
| 358 |
| 359 def debugger_arguments(self): |
| 360 """ |
| 361 returns a 2-tuple of debugger arguments: |
| 362 (debugger_arguments, interactive) |
| 363 """ |
| 364 debug_args = self.options.debugger_args |
| 365 interactive = self.options.interactive |
| 366 if self.options.debugger: |
| 367 debug_args, interactive = debugger_arguments(self.options.debugger) |
| 368 return debug_args, interactive |
| 369 |
| 370 def start(self, runner): |
| 371 """Starts the runner and waits for Firefox to exit or Keyboard Interrupt
. |
| 372 Shoule be overwritten to provide custom running of the runner instance."
"" |
| 373 |
| 374 # attach a debugger if specified |
| 375 debug_args, interactive = self.debugger_arguments() |
| 376 runner.start(debug_args=debug_args, interactive=interactive) |
| 377 print 'Starting:', ' '.join(runner.command) |
| 378 try: |
| 379 runner.wait() |
| 380 except KeyboardInterrupt: |
| 381 runner.stop() |
| 382 |
| 383 |
| 384 def cli(args=sys.argv[1:]): |
| 385 CLI(args).run() |
| 386 |
| 387 if __name__ == '__main__': |
| 388 cli() |
OLD | NEW |