1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/addon-sdk/source/python-lib/mozrunner/__init__.py Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,694 @@ 1.4 +# This Source Code Form is subject to the terms of the Mozilla Public 1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/. 1.7 + 1.8 +import os 1.9 +import sys 1.10 +import copy 1.11 +import tempfile 1.12 +import signal 1.13 +import commands 1.14 +import zipfile 1.15 +import optparse 1.16 +import killableprocess 1.17 +import subprocess 1.18 +import platform 1.19 +import shutil 1.20 +from StringIO import StringIO 1.21 +from xml.dom import minidom 1.22 + 1.23 +from distutils import dir_util 1.24 +from time import sleep 1.25 + 1.26 +# conditional (version-dependent) imports 1.27 +try: 1.28 + import simplejson 1.29 +except ImportError: 1.30 + import json as simplejson 1.31 + 1.32 +import logging 1.33 +logger = logging.getLogger(__name__) 1.34 + 1.35 +# Use dir_util for copy/rm operations because shutil is all kinds of broken 1.36 +copytree = dir_util.copy_tree 1.37 +rmtree = dir_util.remove_tree 1.38 + 1.39 +def findInPath(fileName, path=os.environ['PATH']): 1.40 + dirs = path.split(os.pathsep) 1.41 + for dir in dirs: 1.42 + if os.path.isfile(os.path.join(dir, fileName)): 1.43 + return os.path.join(dir, fileName) 1.44 + if os.name == 'nt' or sys.platform == 'cygwin': 1.45 + if os.path.isfile(os.path.join(dir, fileName + ".exe")): 1.46 + return os.path.join(dir, fileName + ".exe") 1.47 + return None 1.48 + 1.49 +stdout = sys.stdout 1.50 +stderr = sys.stderr 1.51 +stdin = sys.stdin 1.52 + 1.53 +def run_command(cmd, env=None, **kwargs): 1.54 + """Run the given command in killable process.""" 1.55 + killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin} 1.56 + killable_kwargs.update(kwargs) 1.57 + 1.58 + if sys.platform != "win32": 1.59 + return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0), 1.60 + env=env, **killable_kwargs) 1.61 + else: 1.62 + return killableprocess.Popen(cmd, env=env, **killable_kwargs) 1.63 + 1.64 +def getoutput(l): 1.65 + tmp = tempfile.mktemp() 1.66 + x = open(tmp, 'w') 1.67 + subprocess.call(l, stdout=x, stderr=x) 1.68 + x.close(); x = open(tmp, 'r') 1.69 + r = x.read() ; x.close() 1.70 + os.remove(tmp) 1.71 + return r 1.72 + 1.73 +def get_pids(name, minimun_pid=0): 1.74 + """Get all the pids matching name, exclude any pids below minimum_pid.""" 1.75 + if os.name == 'nt' or sys.platform == 'cygwin': 1.76 + import wpk 1.77 + 1.78 + pids = wpk.get_pids(name) 1.79 + 1.80 + else: 1.81 + data = getoutput(['ps', 'ax']).splitlines() 1.82 + pids = [int(line.split()[0]) for line in data if line.find(name) is not -1] 1.83 + 1.84 + matching_pids = [m for m in pids if m > minimun_pid] 1.85 + return matching_pids 1.86 + 1.87 +def makedirs(name): 1.88 + 1.89 + head, tail = os.path.split(name) 1.90 + if not tail: 1.91 + head, tail = os.path.split(head) 1.92 + if head and tail and not os.path.exists(head): 1.93 + try: 1.94 + makedirs(head) 1.95 + except OSError, e: 1.96 + pass 1.97 + if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists 1.98 + return 1.99 + try: 1.100 + os.mkdir(name) 1.101 + except: 1.102 + pass 1.103 + 1.104 +# addon_details() copied from mozprofile 1.105 +def addon_details(install_rdf_fh): 1.106 + """ 1.107 + returns a dictionary of details about the addon 1.108 + - addon_path : path to the addon directory 1.109 + Returns: 1.110 + {'id': u'rainbow@colors.org', # id of the addon 1.111 + 'version': u'1.4', # version of the addon 1.112 + 'name': u'Rainbow', # name of the addon 1.113 + 'unpack': # whether to unpack the addon 1.114 + """ 1.115 + 1.116 + details = { 1.117 + 'id': None, 1.118 + 'unpack': False, 1.119 + 'name': None, 1.120 + 'version': None 1.121 + } 1.122 + 1.123 + def get_namespace_id(doc, url): 1.124 + attributes = doc.documentElement.attributes 1.125 + namespace = "" 1.126 + for i in range(attributes.length): 1.127 + if attributes.item(i).value == url: 1.128 + if ":" in attributes.item(i).name: 1.129 + # If the namespace is not the default one remove 'xlmns:' 1.130 + namespace = attributes.item(i).name.split(':')[1] + ":" 1.131 + break 1.132 + return namespace 1.133 + 1.134 + def get_text(element): 1.135 + """Retrieve the text value of a given node""" 1.136 + rc = [] 1.137 + for node in element.childNodes: 1.138 + if node.nodeType == node.TEXT_NODE: 1.139 + rc.append(node.data) 1.140 + return ''.join(rc).strip() 1.141 + 1.142 + doc = minidom.parse(install_rdf_fh) 1.143 + 1.144 + # Get the namespaces abbreviations 1.145 + em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#") 1.146 + rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#") 1.147 + 1.148 + description = doc.getElementsByTagName(rdf + "Description").item(0) 1.149 + for node in description.childNodes: 1.150 + # Remove the namespace prefix from the tag for comparison 1.151 + entry = node.nodeName.replace(em, "") 1.152 + if entry in details.keys(): 1.153 + details.update({ entry: get_text(node) }) 1.154 + 1.155 + # turn unpack into a true/false value 1.156 + if isinstance(details['unpack'], basestring): 1.157 + details['unpack'] = details['unpack'].lower() == 'true' 1.158 + 1.159 + return details 1.160 + 1.161 +class Profile(object): 1.162 + """Handles all operations regarding profile. Created new profiles, installs extensions, 1.163 + sets preferences and handles cleanup.""" 1.164 + 1.165 + def __init__(self, binary=None, profile=None, addons=None, 1.166 + preferences=None): 1.167 + 1.168 + self.binary = binary 1.169 + 1.170 + self.create_new = not(bool(profile)) 1.171 + if profile: 1.172 + self.profile = profile 1.173 + else: 1.174 + self.profile = self.create_new_profile(self.binary) 1.175 + 1.176 + self.addons_installed = [] 1.177 + self.addons = addons or [] 1.178 + 1.179 + ### set preferences from class preferences 1.180 + preferences = preferences or {} 1.181 + if hasattr(self.__class__, 'preferences'): 1.182 + self.preferences = self.__class__.preferences.copy() 1.183 + else: 1.184 + self.preferences = {} 1.185 + self.preferences.update(preferences) 1.186 + 1.187 + for addon in self.addons: 1.188 + self.install_addon(addon) 1.189 + 1.190 + self.set_preferences(self.preferences) 1.191 + 1.192 + def create_new_profile(self, binary): 1.193 + """Create a new clean profile in tmp which is a simple empty folder""" 1.194 + profile = tempfile.mkdtemp(suffix='.mozrunner') 1.195 + return profile 1.196 + 1.197 + def unpack_addon(self, xpi_zipfile, addon_path): 1.198 + for name in xpi_zipfile.namelist(): 1.199 + if name.endswith('/'): 1.200 + makedirs(os.path.join(addon_path, name)) 1.201 + else: 1.202 + if not os.path.isdir(os.path.dirname(os.path.join(addon_path, name))): 1.203 + makedirs(os.path.dirname(os.path.join(addon_path, name))) 1.204 + data = xpi_zipfile.read(name) 1.205 + f = open(os.path.join(addon_path, name), 'wb') 1.206 + f.write(data) ; f.close() 1.207 + zi = xpi_zipfile.getinfo(name) 1.208 + os.chmod(os.path.join(addon_path,name), (zi.external_attr>>16)) 1.209 + 1.210 + def install_addon(self, path): 1.211 + """Installs the given addon or directory of addons in the profile.""" 1.212 + 1.213 + extensions_path = os.path.join(self.profile, 'extensions') 1.214 + if not os.path.exists(extensions_path): 1.215 + os.makedirs(extensions_path) 1.216 + 1.217 + addons = [path] 1.218 + if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')): 1.219 + addons = [os.path.join(path, x) for x in os.listdir(path)] 1.220 + 1.221 + for addon in addons: 1.222 + if addon.endswith('.xpi'): 1.223 + xpi_zipfile = zipfile.ZipFile(addon, "r") 1.224 + details = addon_details(StringIO(xpi_zipfile.read('install.rdf'))) 1.225 + addon_path = os.path.join(extensions_path, details["id"]) 1.226 + if details.get("unpack", True): 1.227 + self.unpack_addon(xpi_zipfile, addon_path) 1.228 + self.addons_installed.append(addon_path) 1.229 + else: 1.230 + shutil.copy(addon, addon_path + '.xpi') 1.231 + else: 1.232 + # it's already unpacked, but we need to extract the id so we 1.233 + # can copy it 1.234 + details = addon_details(open(os.path.join(addon, "install.rdf"), "rb")) 1.235 + addon_path = os.path.join(extensions_path, details["id"]) 1.236 + shutil.copytree(addon, addon_path, symlinks=True) 1.237 + 1.238 + def set_preferences(self, preferences): 1.239 + """Adds preferences dict to profile preferences""" 1.240 + prefs_file = os.path.join(self.profile, 'user.js') 1.241 + # Ensure that the file exists first otherwise create an empty file 1.242 + if os.path.isfile(prefs_file): 1.243 + f = open(prefs_file, 'a+') 1.244 + else: 1.245 + f = open(prefs_file, 'w') 1.246 + 1.247 + f.write('\n#MozRunner Prefs Start\n') 1.248 + 1.249 + pref_lines = ['user_pref(%s, %s);' % 1.250 + (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in 1.251 + preferences.items()] 1.252 + for line in pref_lines: 1.253 + f.write(line+'\n') 1.254 + f.write('#MozRunner Prefs End\n') 1.255 + f.flush() ; f.close() 1.256 + 1.257 + def pop_preferences(self): 1.258 + """ 1.259 + pop the last set of preferences added 1.260 + returns True if popped 1.261 + """ 1.262 + 1.263 + # our magic markers 1.264 + delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End') 1.265 + 1.266 + lines = file(os.path.join(self.profile, 'user.js')).read().splitlines() 1.267 + def last_index(_list, value): 1.268 + """ 1.269 + returns the last index of an item; 1.270 + this should actually be part of python code but it isn't 1.271 + """ 1.272 + for index in reversed(range(len(_list))): 1.273 + if _list[index] == value: 1.274 + return index 1.275 + s = last_index(lines, delimeters[0]) 1.276 + e = last_index(lines, delimeters[1]) 1.277 + 1.278 + # ensure both markers are found 1.279 + if s is None: 1.280 + assert e is None, '%s found without %s' % (delimeters[1], delimeters[0]) 1.281 + return False # no preferences found 1.282 + elif e is None: 1.283 + assert e is None, '%s found without %s' % (delimeters[0], delimeters[1]) 1.284 + 1.285 + # ensure the markers are in the proper order 1.286 + assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s) 1.287 + 1.288 + # write the prefs 1.289 + cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:]) 1.290 + f = file(os.path.join(self.profile, 'user.js'), 'w') 1.291 + f.write(cleaned_prefs) 1.292 + f.close() 1.293 + return True 1.294 + 1.295 + def clean_preferences(self): 1.296 + """Removed preferences added by mozrunner.""" 1.297 + while True: 1.298 + if not self.pop_preferences(): 1.299 + break 1.300 + 1.301 + def clean_addons(self): 1.302 + """Cleans up addons in the profile.""" 1.303 + for addon in self.addons_installed: 1.304 + if os.path.isdir(addon): 1.305 + rmtree(addon) 1.306 + 1.307 + def cleanup(self): 1.308 + """Cleanup operations on the profile.""" 1.309 + def oncleanup_error(function, path, excinfo): 1.310 + #TODO: How should we handle this? 1.311 + print "Error Cleaning up: " + str(excinfo[1]) 1.312 + if self.create_new: 1.313 + shutil.rmtree(self.profile, False, oncleanup_error) 1.314 + else: 1.315 + self.clean_preferences() 1.316 + self.clean_addons() 1.317 + 1.318 +class FirefoxProfile(Profile): 1.319 + """Specialized Profile subclass for Firefox""" 1.320 + preferences = {# Don't automatically update the application 1.321 + 'app.update.enabled' : False, 1.322 + # Don't restore the last open set of tabs if the browser has crashed 1.323 + 'browser.sessionstore.resume_from_crash': False, 1.324 + # Don't check for the default web browser 1.325 + 'browser.shell.checkDefaultBrowser' : False, 1.326 + # Don't warn on exit when multiple tabs are open 1.327 + 'browser.tabs.warnOnClose' : False, 1.328 + # Don't warn when exiting the browser 1.329 + 'browser.warnOnQuit': False, 1.330 + # Only install add-ons from the profile and the app folder 1.331 + 'extensions.enabledScopes' : 5, 1.332 + # Don't automatically update add-ons 1.333 + 'extensions.update.enabled' : False, 1.334 + # Don't open a dialog to show available add-on updates 1.335 + 'extensions.update.notifyUser' : False, 1.336 + } 1.337 + 1.338 + # The possible names of application bundles on Mac OS X, in order of 1.339 + # preference from most to least preferred. 1.340 + # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly, 1.341 + # but it will still be present if users update an older nightly build 1.342 + # via the app update service. 1.343 + bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly'] 1.344 + 1.345 + # The possible names of binaries, in order of preference from most to least 1.346 + # preferred. 1.347 + @property 1.348 + def names(self): 1.349 + if sys.platform == 'darwin': 1.350 + return ['firefox', 'nightly', 'shiretoko'] 1.351 + if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): 1.352 + return ['firefox', 'mozilla-firefox', 'iceweasel'] 1.353 + if os.name == 'nt' or sys.platform == 'cygwin': 1.354 + return ['firefox'] 1.355 + 1.356 +class ThunderbirdProfile(Profile): 1.357 + preferences = {'extensions.update.enabled' : False, 1.358 + 'extensions.update.notifyUser' : False, 1.359 + 'browser.shell.checkDefaultBrowser' : False, 1.360 + 'browser.tabs.warnOnClose' : False, 1.361 + 'browser.warnOnQuit': False, 1.362 + 'browser.sessionstore.resume_from_crash': False, 1.363 + } 1.364 + 1.365 + # The possible names of application bundles on Mac OS X, in order of 1.366 + # preference from most to least preferred. 1.367 + bundle_names = ["Thunderbird", "Shredder"] 1.368 + 1.369 + # The possible names of binaries, in order of preference from most to least 1.370 + # preferred. 1.371 + names = ["thunderbird", "shredder"] 1.372 + 1.373 + 1.374 +class Runner(object): 1.375 + """Handles all running operations. Finds bins, runs and kills the process.""" 1.376 + 1.377 + def __init__(self, binary=None, profile=None, cmdargs=[], env=None, 1.378 + kp_kwargs={}): 1.379 + if binary is None: 1.380 + self.binary = self.find_binary() 1.381 + elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1: 1.382 + self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.names[0]) 1.383 + else: 1.384 + self.binary = binary 1.385 + 1.386 + if not os.path.exists(self.binary): 1.387 + raise Exception("Binary path does not exist "+self.binary) 1.388 + 1.389 + if sys.platform == 'linux2' and self.binary.endswith('-bin'): 1.390 + dirname = os.path.dirname(self.binary) 1.391 + if os.environ.get('LD_LIBRARY_PATH', None): 1.392 + os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) 1.393 + else: 1.394 + os.environ['LD_LIBRARY_PATH'] = dirname 1.395 + 1.396 + # Disable the crash reporter by default 1.397 + os.environ['MOZ_CRASHREPORTER_NO_REPORT'] = '1' 1.398 + 1.399 + self.profile = profile 1.400 + 1.401 + self.cmdargs = cmdargs 1.402 + if env is None: 1.403 + self.env = copy.copy(os.environ) 1.404 + self.env.update({'MOZ_NO_REMOTE':"1",}) 1.405 + else: 1.406 + self.env = env 1.407 + self.kp_kwargs = kp_kwargs or {} 1.408 + 1.409 + def find_binary(self): 1.410 + """Finds the binary for self.names if one was not provided.""" 1.411 + binary = None 1.412 + if sys.platform in ('linux2', 'sunos5', 'solaris') \ 1.413 + or sys.platform.startswith('freebsd'): 1.414 + for name in reversed(self.names): 1.415 + binary = findInPath(name) 1.416 + elif os.name == 'nt' or sys.platform == 'cygwin': 1.417 + 1.418 + # find the default executable from the windows registry 1.419 + try: 1.420 + import _winreg 1.421 + except ImportError: 1.422 + pass 1.423 + else: 1.424 + sam_flags = [0] 1.425 + # KEY_WOW64_32KEY etc only appeared in 2.6+, but that's OK as 1.426 + # only 2.6+ has functioning 64bit builds. 1.427 + if hasattr(_winreg, "KEY_WOW64_32KEY"): 1.428 + if "64 bit" in sys.version: 1.429 + # a 64bit Python should also look in the 32bit registry 1.430 + sam_flags.append(_winreg.KEY_WOW64_32KEY) 1.431 + else: 1.432 + # possibly a 32bit Python on 64bit Windows, so look in 1.433 + # the 64bit registry incase there is a 64bit app. 1.434 + sam_flags.append(_winreg.KEY_WOW64_64KEY) 1.435 + for sam_flag in sam_flags: 1.436 + try: 1.437 + # assumes self.app_name is defined, as it should be for 1.438 + # implementors 1.439 + keyname = r"Software\Mozilla\Mozilla %s" % self.app_name 1.440 + sam = _winreg.KEY_READ | sam_flag 1.441 + app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keyname, 0, sam) 1.442 + version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion") 1.443 + version_key = _winreg.OpenKey(app_key, version + r"\Main") 1.444 + path, _ = _winreg.QueryValueEx(version_key, "PathToExe") 1.445 + return path 1.446 + except _winreg.error: 1.447 + pass 1.448 + 1.449 + # search for the binary in the path 1.450 + for name in reversed(self.names): 1.451 + binary = findInPath(name) 1.452 + if sys.platform == 'cygwin': 1.453 + program_files = os.environ['PROGRAMFILES'] 1.454 + else: 1.455 + program_files = os.environ['ProgramFiles'] 1.456 + 1.457 + if binary is None: 1.458 + for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'), 1.459 + (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'), 1.460 + (program_files, 'Nightly', 'firefox.exe'), 1.461 + (os.environ.get("ProgramFiles(x86)"),'Nightly', 'firefox.exe'), 1.462 + (program_files, 'Aurora', 'firefox.exe'), 1.463 + (os.environ.get("ProgramFiles(x86)"),'Aurora', 'firefox.exe') 1.464 + ]: 1.465 + path = os.path.join(*bin) 1.466 + if os.path.isfile(path): 1.467 + binary = path 1.468 + break 1.469 + elif sys.platform == 'darwin': 1.470 + for bundle_name in self.bundle_names: 1.471 + # Look for the application bundle in the user's home directory 1.472 + # or the system-wide /Applications directory. If we don't find 1.473 + # it in one of those locations, we move on to the next possible 1.474 + # bundle name. 1.475 + appdir = os.path.join("~/Applications/%s.app" % bundle_name) 1.476 + if not os.path.isdir(appdir): 1.477 + appdir = "/Applications/%s.app" % bundle_name 1.478 + if not os.path.isdir(appdir): 1.479 + continue 1.480 + 1.481 + # Look for a binary with any of the possible binary names 1.482 + # inside the application bundle. 1.483 + for binname in self.names: 1.484 + binpath = os.path.join(appdir, 1.485 + "Contents/MacOS/%s-bin" % binname) 1.486 + if (os.path.isfile(binpath)): 1.487 + binary = binpath 1.488 + break 1.489 + 1.490 + if binary: 1.491 + break 1.492 + 1.493 + if binary is None: 1.494 + raise Exception('Mozrunner could not locate your binary, you will need to set it.') 1.495 + return binary 1.496 + 1.497 + @property 1.498 + def command(self): 1.499 + """Returns the command list to run.""" 1.500 + cmd = [self.binary, '-profile', self.profile.profile] 1.501 + # On i386 OS X machines, i386+x86_64 universal binaries need to be told 1.502 + # to run as i386 binaries. If we're not running a i386+x86_64 universal 1.503 + # binary, then this command modification is harmless. 1.504 + if sys.platform == 'darwin': 1.505 + if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit': 1.506 + cmd = ['arch', '-i386'] + cmd 1.507 + return cmd 1.508 + 1.509 + def get_repositoryInfo(self): 1.510 + """Read repository information from application.ini and platform.ini.""" 1.511 + import ConfigParser 1.512 + 1.513 + config = ConfigParser.RawConfigParser() 1.514 + dirname = os.path.dirname(self.binary) 1.515 + repository = { } 1.516 + 1.517 + for entry in [['application', 'App'], ['platform', 'Build']]: 1.518 + (file, section) = entry 1.519 + config.read(os.path.join(dirname, '%s.ini' % file)) 1.520 + 1.521 + for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]: 1.522 + (key, id) = entry 1.523 + 1.524 + try: 1.525 + repository['%s_%s' % (file, id)] = config.get(section, key); 1.526 + except: 1.527 + repository['%s_%s' % (file, id)] = None 1.528 + 1.529 + return repository 1.530 + 1.531 + def start(self): 1.532 + """Run self.command in the proper environment.""" 1.533 + if self.profile is None: 1.534 + self.profile = self.profile_class() 1.535 + self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs) 1.536 + 1.537 + def wait(self, timeout=None): 1.538 + """Wait for the browser to exit.""" 1.539 + self.process_handler.wait(timeout=timeout) 1.540 + 1.541 + if sys.platform != 'win32': 1.542 + for name in self.names: 1.543 + for pid in get_pids(name, self.process_handler.pid): 1.544 + self.process_handler.pid = pid 1.545 + self.process_handler.wait(timeout=timeout) 1.546 + 1.547 + def kill(self, kill_signal=signal.SIGTERM): 1.548 + """Kill the browser""" 1.549 + if sys.platform != 'win32': 1.550 + self.process_handler.kill() 1.551 + for name in self.names: 1.552 + for pid in get_pids(name, self.process_handler.pid): 1.553 + self.process_handler.pid = pid 1.554 + self.process_handler.kill() 1.555 + else: 1.556 + try: 1.557 + self.process_handler.kill(group=True) 1.558 + # On windows, it sometimes behooves one to wait for dust to settle 1.559 + # after killing processes. Let's try that. 1.560 + # TODO: Bug 640047 is invesitgating the correct way to handle this case 1.561 + self.process_handler.wait(timeout=10) 1.562 + except Exception, e: 1.563 + logger.error('Cannot kill process, '+type(e).__name__+' '+e.message) 1.564 + 1.565 + def stop(self): 1.566 + self.kill() 1.567 + 1.568 +class FirefoxRunner(Runner): 1.569 + """Specialized Runner subclass for running Firefox.""" 1.570 + 1.571 + app_name = 'Firefox' 1.572 + profile_class = FirefoxProfile 1.573 + 1.574 + # The possible names of application bundles on Mac OS X, in order of 1.575 + # preference from most to least preferred. 1.576 + # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly, 1.577 + # but it will still be present if users update an older nightly build 1.578 + # only via the app update service. 1.579 + bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly'] 1.580 + 1.581 + @property 1.582 + def names(self): 1.583 + if sys.platform == 'darwin': 1.584 + return ['firefox', 'nightly', 'shiretoko'] 1.585 + if sys.platform in ('linux2', 'sunos5', 'solaris') \ 1.586 + or sys.platform.startswith('freebsd'): 1.587 + return ['firefox', 'mozilla-firefox', 'iceweasel'] 1.588 + if os.name == 'nt' or sys.platform == 'cygwin': 1.589 + return ['firefox'] 1.590 + 1.591 +class ThunderbirdRunner(Runner): 1.592 + """Specialized Runner subclass for running Thunderbird""" 1.593 + 1.594 + app_name = 'Thunderbird' 1.595 + profile_class = ThunderbirdProfile 1.596 + 1.597 + # The possible names of application bundles on Mac OS X, in order of 1.598 + # preference from most to least preferred. 1.599 + bundle_names = ["Thunderbird", "Shredder"] 1.600 + 1.601 + # The possible names of binaries, in order of preference from most to least 1.602 + # preferred. 1.603 + names = ["thunderbird", "shredder"] 1.604 + 1.605 +class CLI(object): 1.606 + """Command line interface.""" 1.607 + 1.608 + runner_class = FirefoxRunner 1.609 + profile_class = FirefoxProfile 1.610 + module = "mozrunner" 1.611 + 1.612 + parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path.", 1.613 + metavar=None, default=None), 1.614 + ('-p', "--profile",): dict(dest="profile", help="Profile path.", 1.615 + metavar=None, default=None), 1.616 + ('-a', "--addons",): dict(dest="addons", 1.617 + help="Addons paths to install.", 1.618 + metavar=None, default=None), 1.619 + ("--info",): dict(dest="info", default=False, 1.620 + action="store_true", 1.621 + help="Print module information") 1.622 + } 1.623 + 1.624 + def __init__(self): 1.625 + """ Setup command line parser and parse arguments """ 1.626 + self.metadata = self.get_metadata_from_egg() 1.627 + self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"]) 1.628 + for names, opts in self.parser_options.items(): 1.629 + self.parser.add_option(*names, **opts) 1.630 + (self.options, self.args) = self.parser.parse_args() 1.631 + 1.632 + if self.options.info: 1.633 + self.print_metadata() 1.634 + sys.exit(0) 1.635 + 1.636 + # XXX should use action='append' instead of rolling our own 1.637 + try: 1.638 + self.addons = self.options.addons.split(',') 1.639 + except: 1.640 + self.addons = [] 1.641 + 1.642 + def get_metadata_from_egg(self): 1.643 + import pkg_resources 1.644 + ret = {} 1.645 + dist = pkg_resources.get_distribution(self.module) 1.646 + if dist.has_metadata("PKG-INFO"): 1.647 + for line in dist.get_metadata_lines("PKG-INFO"): 1.648 + key, value = line.split(':', 1) 1.649 + ret[key] = value 1.650 + if dist.has_metadata("requires.txt"): 1.651 + ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") 1.652 + return ret 1.653 + 1.654 + def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", 1.655 + "Author", "Author-email", "License", "Platform", "Dependencies")): 1.656 + for key in data: 1.657 + if key in self.metadata: 1.658 + print key + ": " + self.metadata[key] 1.659 + 1.660 + def create_runner(self): 1.661 + """ Get the runner object """ 1.662 + runner = self.get_runner(binary=self.options.binary) 1.663 + profile = self.get_profile(binary=runner.binary, 1.664 + profile=self.options.profile, 1.665 + addons=self.addons) 1.666 + runner.profile = profile 1.667 + return runner 1.668 + 1.669 + def get_runner(self, binary=None, profile=None): 1.670 + """Returns the runner instance for the given command line binary argument 1.671 + the profile instance returned from self.get_profile().""" 1.672 + return self.runner_class(binary, profile) 1.673 + 1.674 + def get_profile(self, binary=None, profile=None, addons=None, preferences=None): 1.675 + """Returns the profile instance for the given command line arguments.""" 1.676 + addons = addons or [] 1.677 + preferences = preferences or {} 1.678 + return self.profile_class(binary, profile, addons, preferences) 1.679 + 1.680 + def run(self): 1.681 + runner = self.create_runner() 1.682 + self.start(runner) 1.683 + runner.profile.cleanup() 1.684 + 1.685 + def start(self, runner): 1.686 + """Starts the runner and waits for Firefox to exitor Keyboard Interrupt. 1.687 + Shoule be overwritten to provide custom running of the runner instance.""" 1.688 + runner.start() 1.689 + print 'Started:', ' '.join(runner.command) 1.690 + try: 1.691 + runner.wait() 1.692 + except KeyboardInterrupt: 1.693 + runner.stop() 1.694 + 1.695 + 1.696 +def cli(): 1.697 + CLI().run()