addon-sdk/source/python-lib/mozrunner/__init__.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 4
michael@0 5 import os
michael@0 6 import sys
michael@0 7 import copy
michael@0 8 import tempfile
michael@0 9 import signal
michael@0 10 import commands
michael@0 11 import zipfile
michael@0 12 import optparse
michael@0 13 import killableprocess
michael@0 14 import subprocess
michael@0 15 import platform
michael@0 16 import shutil
michael@0 17 from StringIO import StringIO
michael@0 18 from xml.dom import minidom
michael@0 19
michael@0 20 from distutils import dir_util
michael@0 21 from time import sleep
michael@0 22
michael@0 23 # conditional (version-dependent) imports
michael@0 24 try:
michael@0 25 import simplejson
michael@0 26 except ImportError:
michael@0 27 import json as simplejson
michael@0 28
michael@0 29 import logging
michael@0 30 logger = logging.getLogger(__name__)
michael@0 31
michael@0 32 # Use dir_util for copy/rm operations because shutil is all kinds of broken
michael@0 33 copytree = dir_util.copy_tree
michael@0 34 rmtree = dir_util.remove_tree
michael@0 35
michael@0 36 def findInPath(fileName, path=os.environ['PATH']):
michael@0 37 dirs = path.split(os.pathsep)
michael@0 38 for dir in dirs:
michael@0 39 if os.path.isfile(os.path.join(dir, fileName)):
michael@0 40 return os.path.join(dir, fileName)
michael@0 41 if os.name == 'nt' or sys.platform == 'cygwin':
michael@0 42 if os.path.isfile(os.path.join(dir, fileName + ".exe")):
michael@0 43 return os.path.join(dir, fileName + ".exe")
michael@0 44 return None
michael@0 45
michael@0 46 stdout = sys.stdout
michael@0 47 stderr = sys.stderr
michael@0 48 stdin = sys.stdin
michael@0 49
michael@0 50 def run_command(cmd, env=None, **kwargs):
michael@0 51 """Run the given command in killable process."""
michael@0 52 killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin}
michael@0 53 killable_kwargs.update(kwargs)
michael@0 54
michael@0 55 if sys.platform != "win32":
michael@0 56 return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0),
michael@0 57 env=env, **killable_kwargs)
michael@0 58 else:
michael@0 59 return killableprocess.Popen(cmd, env=env, **killable_kwargs)
michael@0 60
michael@0 61 def getoutput(l):
michael@0 62 tmp = tempfile.mktemp()
michael@0 63 x = open(tmp, 'w')
michael@0 64 subprocess.call(l, stdout=x, stderr=x)
michael@0 65 x.close(); x = open(tmp, 'r')
michael@0 66 r = x.read() ; x.close()
michael@0 67 os.remove(tmp)
michael@0 68 return r
michael@0 69
michael@0 70 def get_pids(name, minimun_pid=0):
michael@0 71 """Get all the pids matching name, exclude any pids below minimum_pid."""
michael@0 72 if os.name == 'nt' or sys.platform == 'cygwin':
michael@0 73 import wpk
michael@0 74
michael@0 75 pids = wpk.get_pids(name)
michael@0 76
michael@0 77 else:
michael@0 78 data = getoutput(['ps', 'ax']).splitlines()
michael@0 79 pids = [int(line.split()[0]) for line in data if line.find(name) is not -1]
michael@0 80
michael@0 81 matching_pids = [m for m in pids if m > minimun_pid]
michael@0 82 return matching_pids
michael@0 83
michael@0 84 def makedirs(name):
michael@0 85
michael@0 86 head, tail = os.path.split(name)
michael@0 87 if not tail:
michael@0 88 head, tail = os.path.split(head)
michael@0 89 if head and tail and not os.path.exists(head):
michael@0 90 try:
michael@0 91 makedirs(head)
michael@0 92 except OSError, e:
michael@0 93 pass
michael@0 94 if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists
michael@0 95 return
michael@0 96 try:
michael@0 97 os.mkdir(name)
michael@0 98 except:
michael@0 99 pass
michael@0 100
michael@0 101 # addon_details() copied from mozprofile
michael@0 102 def addon_details(install_rdf_fh):
michael@0 103 """
michael@0 104 returns a dictionary of details about the addon
michael@0 105 - addon_path : path to the addon directory
michael@0 106 Returns:
michael@0 107 {'id': u'rainbow@colors.org', # id of the addon
michael@0 108 'version': u'1.4', # version of the addon
michael@0 109 'name': u'Rainbow', # name of the addon
michael@0 110 'unpack': # whether to unpack the addon
michael@0 111 """
michael@0 112
michael@0 113 details = {
michael@0 114 'id': None,
michael@0 115 'unpack': False,
michael@0 116 'name': None,
michael@0 117 'version': None
michael@0 118 }
michael@0 119
michael@0 120 def get_namespace_id(doc, url):
michael@0 121 attributes = doc.documentElement.attributes
michael@0 122 namespace = ""
michael@0 123 for i in range(attributes.length):
michael@0 124 if attributes.item(i).value == url:
michael@0 125 if ":" in attributes.item(i).name:
michael@0 126 # If the namespace is not the default one remove 'xlmns:'
michael@0 127 namespace = attributes.item(i).name.split(':')[1] + ":"
michael@0 128 break
michael@0 129 return namespace
michael@0 130
michael@0 131 def get_text(element):
michael@0 132 """Retrieve the text value of a given node"""
michael@0 133 rc = []
michael@0 134 for node in element.childNodes:
michael@0 135 if node.nodeType == node.TEXT_NODE:
michael@0 136 rc.append(node.data)
michael@0 137 return ''.join(rc).strip()
michael@0 138
michael@0 139 doc = minidom.parse(install_rdf_fh)
michael@0 140
michael@0 141 # Get the namespaces abbreviations
michael@0 142 em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
michael@0 143 rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
michael@0 144
michael@0 145 description = doc.getElementsByTagName(rdf + "Description").item(0)
michael@0 146 for node in description.childNodes:
michael@0 147 # Remove the namespace prefix from the tag for comparison
michael@0 148 entry = node.nodeName.replace(em, "")
michael@0 149 if entry in details.keys():
michael@0 150 details.update({ entry: get_text(node) })
michael@0 151
michael@0 152 # turn unpack into a true/false value
michael@0 153 if isinstance(details['unpack'], basestring):
michael@0 154 details['unpack'] = details['unpack'].lower() == 'true'
michael@0 155
michael@0 156 return details
michael@0 157
michael@0 158 class Profile(object):
michael@0 159 """Handles all operations regarding profile. Created new profiles, installs extensions,
michael@0 160 sets preferences and handles cleanup."""
michael@0 161
michael@0 162 def __init__(self, binary=None, profile=None, addons=None,
michael@0 163 preferences=None):
michael@0 164
michael@0 165 self.binary = binary
michael@0 166
michael@0 167 self.create_new = not(bool(profile))
michael@0 168 if profile:
michael@0 169 self.profile = profile
michael@0 170 else:
michael@0 171 self.profile = self.create_new_profile(self.binary)
michael@0 172
michael@0 173 self.addons_installed = []
michael@0 174 self.addons = addons or []
michael@0 175
michael@0 176 ### set preferences from class preferences
michael@0 177 preferences = preferences or {}
michael@0 178 if hasattr(self.__class__, 'preferences'):
michael@0 179 self.preferences = self.__class__.preferences.copy()
michael@0 180 else:
michael@0 181 self.preferences = {}
michael@0 182 self.preferences.update(preferences)
michael@0 183
michael@0 184 for addon in self.addons:
michael@0 185 self.install_addon(addon)
michael@0 186
michael@0 187 self.set_preferences(self.preferences)
michael@0 188
michael@0 189 def create_new_profile(self, binary):
michael@0 190 """Create a new clean profile in tmp which is a simple empty folder"""
michael@0 191 profile = tempfile.mkdtemp(suffix='.mozrunner')
michael@0 192 return profile
michael@0 193
michael@0 194 def unpack_addon(self, xpi_zipfile, addon_path):
michael@0 195 for name in xpi_zipfile.namelist():
michael@0 196 if name.endswith('/'):
michael@0 197 makedirs(os.path.join(addon_path, name))
michael@0 198 else:
michael@0 199 if not os.path.isdir(os.path.dirname(os.path.join(addon_path, name))):
michael@0 200 makedirs(os.path.dirname(os.path.join(addon_path, name)))
michael@0 201 data = xpi_zipfile.read(name)
michael@0 202 f = open(os.path.join(addon_path, name), 'wb')
michael@0 203 f.write(data) ; f.close()
michael@0 204 zi = xpi_zipfile.getinfo(name)
michael@0 205 os.chmod(os.path.join(addon_path,name), (zi.external_attr>>16))
michael@0 206
michael@0 207 def install_addon(self, path):
michael@0 208 """Installs the given addon or directory of addons in the profile."""
michael@0 209
michael@0 210 extensions_path = os.path.join(self.profile, 'extensions')
michael@0 211 if not os.path.exists(extensions_path):
michael@0 212 os.makedirs(extensions_path)
michael@0 213
michael@0 214 addons = [path]
michael@0 215 if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')):
michael@0 216 addons = [os.path.join(path, x) for x in os.listdir(path)]
michael@0 217
michael@0 218 for addon in addons:
michael@0 219 if addon.endswith('.xpi'):
michael@0 220 xpi_zipfile = zipfile.ZipFile(addon, "r")
michael@0 221 details = addon_details(StringIO(xpi_zipfile.read('install.rdf')))
michael@0 222 addon_path = os.path.join(extensions_path, details["id"])
michael@0 223 if details.get("unpack", True):
michael@0 224 self.unpack_addon(xpi_zipfile, addon_path)
michael@0 225 self.addons_installed.append(addon_path)
michael@0 226 else:
michael@0 227 shutil.copy(addon, addon_path + '.xpi')
michael@0 228 else:
michael@0 229 # it's already unpacked, but we need to extract the id so we
michael@0 230 # can copy it
michael@0 231 details = addon_details(open(os.path.join(addon, "install.rdf"), "rb"))
michael@0 232 addon_path = os.path.join(extensions_path, details["id"])
michael@0 233 shutil.copytree(addon, addon_path, symlinks=True)
michael@0 234
michael@0 235 def set_preferences(self, preferences):
michael@0 236 """Adds preferences dict to profile preferences"""
michael@0 237 prefs_file = os.path.join(self.profile, 'user.js')
michael@0 238 # Ensure that the file exists first otherwise create an empty file
michael@0 239 if os.path.isfile(prefs_file):
michael@0 240 f = open(prefs_file, 'a+')
michael@0 241 else:
michael@0 242 f = open(prefs_file, 'w')
michael@0 243
michael@0 244 f.write('\n#MozRunner Prefs Start\n')
michael@0 245
michael@0 246 pref_lines = ['user_pref(%s, %s);' %
michael@0 247 (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in
michael@0 248 preferences.items()]
michael@0 249 for line in pref_lines:
michael@0 250 f.write(line+'\n')
michael@0 251 f.write('#MozRunner Prefs End\n')
michael@0 252 f.flush() ; f.close()
michael@0 253
michael@0 254 def pop_preferences(self):
michael@0 255 """
michael@0 256 pop the last set of preferences added
michael@0 257 returns True if popped
michael@0 258 """
michael@0 259
michael@0 260 # our magic markers
michael@0 261 delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End')
michael@0 262
michael@0 263 lines = file(os.path.join(self.profile, 'user.js')).read().splitlines()
michael@0 264 def last_index(_list, value):
michael@0 265 """
michael@0 266 returns the last index of an item;
michael@0 267 this should actually be part of python code but it isn't
michael@0 268 """
michael@0 269 for index in reversed(range(len(_list))):
michael@0 270 if _list[index] == value:
michael@0 271 return index
michael@0 272 s = last_index(lines, delimeters[0])
michael@0 273 e = last_index(lines, delimeters[1])
michael@0 274
michael@0 275 # ensure both markers are found
michael@0 276 if s is None:
michael@0 277 assert e is None, '%s found without %s' % (delimeters[1], delimeters[0])
michael@0 278 return False # no preferences found
michael@0 279 elif e is None:
michael@0 280 assert e is None, '%s found without %s' % (delimeters[0], delimeters[1])
michael@0 281
michael@0 282 # ensure the markers are in the proper order
michael@0 283 assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s)
michael@0 284
michael@0 285 # write the prefs
michael@0 286 cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
michael@0 287 f = file(os.path.join(self.profile, 'user.js'), 'w')
michael@0 288 f.write(cleaned_prefs)
michael@0 289 f.close()
michael@0 290 return True
michael@0 291
michael@0 292 def clean_preferences(self):
michael@0 293 """Removed preferences added by mozrunner."""
michael@0 294 while True:
michael@0 295 if not self.pop_preferences():
michael@0 296 break
michael@0 297
michael@0 298 def clean_addons(self):
michael@0 299 """Cleans up addons in the profile."""
michael@0 300 for addon in self.addons_installed:
michael@0 301 if os.path.isdir(addon):
michael@0 302 rmtree(addon)
michael@0 303
michael@0 304 def cleanup(self):
michael@0 305 """Cleanup operations on the profile."""
michael@0 306 def oncleanup_error(function, path, excinfo):
michael@0 307 #TODO: How should we handle this?
michael@0 308 print "Error Cleaning up: " + str(excinfo[1])
michael@0 309 if self.create_new:
michael@0 310 shutil.rmtree(self.profile, False, oncleanup_error)
michael@0 311 else:
michael@0 312 self.clean_preferences()
michael@0 313 self.clean_addons()
michael@0 314
michael@0 315 class FirefoxProfile(Profile):
michael@0 316 """Specialized Profile subclass for Firefox"""
michael@0 317 preferences = {# Don't automatically update the application
michael@0 318 'app.update.enabled' : False,
michael@0 319 # Don't restore the last open set of tabs if the browser has crashed
michael@0 320 'browser.sessionstore.resume_from_crash': False,
michael@0 321 # Don't check for the default web browser
michael@0 322 'browser.shell.checkDefaultBrowser' : False,
michael@0 323 # Don't warn on exit when multiple tabs are open
michael@0 324 'browser.tabs.warnOnClose' : False,
michael@0 325 # Don't warn when exiting the browser
michael@0 326 'browser.warnOnQuit': False,
michael@0 327 # Only install add-ons from the profile and the app folder
michael@0 328 'extensions.enabledScopes' : 5,
michael@0 329 # Don't automatically update add-ons
michael@0 330 'extensions.update.enabled' : False,
michael@0 331 # Don't open a dialog to show available add-on updates
michael@0 332 'extensions.update.notifyUser' : False,
michael@0 333 }
michael@0 334
michael@0 335 # The possible names of application bundles on Mac OS X, in order of
michael@0 336 # preference from most to least preferred.
michael@0 337 # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly,
michael@0 338 # but it will still be present if users update an older nightly build
michael@0 339 # via the app update service.
michael@0 340 bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
michael@0 341
michael@0 342 # The possible names of binaries, in order of preference from most to least
michael@0 343 # preferred.
michael@0 344 @property
michael@0 345 def names(self):
michael@0 346 if sys.platform == 'darwin':
michael@0 347 return ['firefox', 'nightly', 'shiretoko']
michael@0 348 if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
michael@0 349 return ['firefox', 'mozilla-firefox', 'iceweasel']
michael@0 350 if os.name == 'nt' or sys.platform == 'cygwin':
michael@0 351 return ['firefox']
michael@0 352
michael@0 353 class ThunderbirdProfile(Profile):
michael@0 354 preferences = {'extensions.update.enabled' : False,
michael@0 355 'extensions.update.notifyUser' : False,
michael@0 356 'browser.shell.checkDefaultBrowser' : False,
michael@0 357 'browser.tabs.warnOnClose' : False,
michael@0 358 'browser.warnOnQuit': False,
michael@0 359 'browser.sessionstore.resume_from_crash': False,
michael@0 360 }
michael@0 361
michael@0 362 # The possible names of application bundles on Mac OS X, in order of
michael@0 363 # preference from most to least preferred.
michael@0 364 bundle_names = ["Thunderbird", "Shredder"]
michael@0 365
michael@0 366 # The possible names of binaries, in order of preference from most to least
michael@0 367 # preferred.
michael@0 368 names = ["thunderbird", "shredder"]
michael@0 369
michael@0 370
michael@0 371 class Runner(object):
michael@0 372 """Handles all running operations. Finds bins, runs and kills the process."""
michael@0 373
michael@0 374 def __init__(self, binary=None, profile=None, cmdargs=[], env=None,
michael@0 375 kp_kwargs={}):
michael@0 376 if binary is None:
michael@0 377 self.binary = self.find_binary()
michael@0 378 elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1:
michael@0 379 self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.names[0])
michael@0 380 else:
michael@0 381 self.binary = binary
michael@0 382
michael@0 383 if not os.path.exists(self.binary):
michael@0 384 raise Exception("Binary path does not exist "+self.binary)
michael@0 385
michael@0 386 if sys.platform == 'linux2' and self.binary.endswith('-bin'):
michael@0 387 dirname = os.path.dirname(self.binary)
michael@0 388 if os.environ.get('LD_LIBRARY_PATH', None):
michael@0 389 os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
michael@0 390 else:
michael@0 391 os.environ['LD_LIBRARY_PATH'] = dirname
michael@0 392
michael@0 393 # Disable the crash reporter by default
michael@0 394 os.environ['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
michael@0 395
michael@0 396 self.profile = profile
michael@0 397
michael@0 398 self.cmdargs = cmdargs
michael@0 399 if env is None:
michael@0 400 self.env = copy.copy(os.environ)
michael@0 401 self.env.update({'MOZ_NO_REMOTE':"1",})
michael@0 402 else:
michael@0 403 self.env = env
michael@0 404 self.kp_kwargs = kp_kwargs or {}
michael@0 405
michael@0 406 def find_binary(self):
michael@0 407 """Finds the binary for self.names if one was not provided."""
michael@0 408 binary = None
michael@0 409 if sys.platform in ('linux2', 'sunos5', 'solaris') \
michael@0 410 or sys.platform.startswith('freebsd'):
michael@0 411 for name in reversed(self.names):
michael@0 412 binary = findInPath(name)
michael@0 413 elif os.name == 'nt' or sys.platform == 'cygwin':
michael@0 414
michael@0 415 # find the default executable from the windows registry
michael@0 416 try:
michael@0 417 import _winreg
michael@0 418 except ImportError:
michael@0 419 pass
michael@0 420 else:
michael@0 421 sam_flags = [0]
michael@0 422 # KEY_WOW64_32KEY etc only appeared in 2.6+, but that's OK as
michael@0 423 # only 2.6+ has functioning 64bit builds.
michael@0 424 if hasattr(_winreg, "KEY_WOW64_32KEY"):
michael@0 425 if "64 bit" in sys.version:
michael@0 426 # a 64bit Python should also look in the 32bit registry
michael@0 427 sam_flags.append(_winreg.KEY_WOW64_32KEY)
michael@0 428 else:
michael@0 429 # possibly a 32bit Python on 64bit Windows, so look in
michael@0 430 # the 64bit registry incase there is a 64bit app.
michael@0 431 sam_flags.append(_winreg.KEY_WOW64_64KEY)
michael@0 432 for sam_flag in sam_flags:
michael@0 433 try:
michael@0 434 # assumes self.app_name is defined, as it should be for
michael@0 435 # implementors
michael@0 436 keyname = r"Software\Mozilla\Mozilla %s" % self.app_name
michael@0 437 sam = _winreg.KEY_READ | sam_flag
michael@0 438 app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keyname, 0, sam)
michael@0 439 version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion")
michael@0 440 version_key = _winreg.OpenKey(app_key, version + r"\Main")
michael@0 441 path, _ = _winreg.QueryValueEx(version_key, "PathToExe")
michael@0 442 return path
michael@0 443 except _winreg.error:
michael@0 444 pass
michael@0 445
michael@0 446 # search for the binary in the path
michael@0 447 for name in reversed(self.names):
michael@0 448 binary = findInPath(name)
michael@0 449 if sys.platform == 'cygwin':
michael@0 450 program_files = os.environ['PROGRAMFILES']
michael@0 451 else:
michael@0 452 program_files = os.environ['ProgramFiles']
michael@0 453
michael@0 454 if binary is None:
michael@0 455 for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'),
michael@0 456 (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'),
michael@0 457 (program_files, 'Nightly', 'firefox.exe'),
michael@0 458 (os.environ.get("ProgramFiles(x86)"),'Nightly', 'firefox.exe'),
michael@0 459 (program_files, 'Aurora', 'firefox.exe'),
michael@0 460 (os.environ.get("ProgramFiles(x86)"),'Aurora', 'firefox.exe')
michael@0 461 ]:
michael@0 462 path = os.path.join(*bin)
michael@0 463 if os.path.isfile(path):
michael@0 464 binary = path
michael@0 465 break
michael@0 466 elif sys.platform == 'darwin':
michael@0 467 for bundle_name in self.bundle_names:
michael@0 468 # Look for the application bundle in the user's home directory
michael@0 469 # or the system-wide /Applications directory. If we don't find
michael@0 470 # it in one of those locations, we move on to the next possible
michael@0 471 # bundle name.
michael@0 472 appdir = os.path.join("~/Applications/%s.app" % bundle_name)
michael@0 473 if not os.path.isdir(appdir):
michael@0 474 appdir = "/Applications/%s.app" % bundle_name
michael@0 475 if not os.path.isdir(appdir):
michael@0 476 continue
michael@0 477
michael@0 478 # Look for a binary with any of the possible binary names
michael@0 479 # inside the application bundle.
michael@0 480 for binname in self.names:
michael@0 481 binpath = os.path.join(appdir,
michael@0 482 "Contents/MacOS/%s-bin" % binname)
michael@0 483 if (os.path.isfile(binpath)):
michael@0 484 binary = binpath
michael@0 485 break
michael@0 486
michael@0 487 if binary:
michael@0 488 break
michael@0 489
michael@0 490 if binary is None:
michael@0 491 raise Exception('Mozrunner could not locate your binary, you will need to set it.')
michael@0 492 return binary
michael@0 493
michael@0 494 @property
michael@0 495 def command(self):
michael@0 496 """Returns the command list to run."""
michael@0 497 cmd = [self.binary, '-profile', self.profile.profile]
michael@0 498 # On i386 OS X machines, i386+x86_64 universal binaries need to be told
michael@0 499 # to run as i386 binaries. If we're not running a i386+x86_64 universal
michael@0 500 # binary, then this command modification is harmless.
michael@0 501 if sys.platform == 'darwin':
michael@0 502 if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit':
michael@0 503 cmd = ['arch', '-i386'] + cmd
michael@0 504 return cmd
michael@0 505
michael@0 506 def get_repositoryInfo(self):
michael@0 507 """Read repository information from application.ini and platform.ini."""
michael@0 508 import ConfigParser
michael@0 509
michael@0 510 config = ConfigParser.RawConfigParser()
michael@0 511 dirname = os.path.dirname(self.binary)
michael@0 512 repository = { }
michael@0 513
michael@0 514 for entry in [['application', 'App'], ['platform', 'Build']]:
michael@0 515 (file, section) = entry
michael@0 516 config.read(os.path.join(dirname, '%s.ini' % file))
michael@0 517
michael@0 518 for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]:
michael@0 519 (key, id) = entry
michael@0 520
michael@0 521 try:
michael@0 522 repository['%s_%s' % (file, id)] = config.get(section, key);
michael@0 523 except:
michael@0 524 repository['%s_%s' % (file, id)] = None
michael@0 525
michael@0 526 return repository
michael@0 527
michael@0 528 def start(self):
michael@0 529 """Run self.command in the proper environment."""
michael@0 530 if self.profile is None:
michael@0 531 self.profile = self.profile_class()
michael@0 532 self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs)
michael@0 533
michael@0 534 def wait(self, timeout=None):
michael@0 535 """Wait for the browser to exit."""
michael@0 536 self.process_handler.wait(timeout=timeout)
michael@0 537
michael@0 538 if sys.platform != 'win32':
michael@0 539 for name in self.names:
michael@0 540 for pid in get_pids(name, self.process_handler.pid):
michael@0 541 self.process_handler.pid = pid
michael@0 542 self.process_handler.wait(timeout=timeout)
michael@0 543
michael@0 544 def kill(self, kill_signal=signal.SIGTERM):
michael@0 545 """Kill the browser"""
michael@0 546 if sys.platform != 'win32':
michael@0 547 self.process_handler.kill()
michael@0 548 for name in self.names:
michael@0 549 for pid in get_pids(name, self.process_handler.pid):
michael@0 550 self.process_handler.pid = pid
michael@0 551 self.process_handler.kill()
michael@0 552 else:
michael@0 553 try:
michael@0 554 self.process_handler.kill(group=True)
michael@0 555 # On windows, it sometimes behooves one to wait for dust to settle
michael@0 556 # after killing processes. Let's try that.
michael@0 557 # TODO: Bug 640047 is invesitgating the correct way to handle this case
michael@0 558 self.process_handler.wait(timeout=10)
michael@0 559 except Exception, e:
michael@0 560 logger.error('Cannot kill process, '+type(e).__name__+' '+e.message)
michael@0 561
michael@0 562 def stop(self):
michael@0 563 self.kill()
michael@0 564
michael@0 565 class FirefoxRunner(Runner):
michael@0 566 """Specialized Runner subclass for running Firefox."""
michael@0 567
michael@0 568 app_name = 'Firefox'
michael@0 569 profile_class = FirefoxProfile
michael@0 570
michael@0 571 # The possible names of application bundles on Mac OS X, in order of
michael@0 572 # preference from most to least preferred.
michael@0 573 # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly,
michael@0 574 # but it will still be present if users update an older nightly build
michael@0 575 # only via the app update service.
michael@0 576 bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
michael@0 577
michael@0 578 @property
michael@0 579 def names(self):
michael@0 580 if sys.platform == 'darwin':
michael@0 581 return ['firefox', 'nightly', 'shiretoko']
michael@0 582 if sys.platform in ('linux2', 'sunos5', 'solaris') \
michael@0 583 or sys.platform.startswith('freebsd'):
michael@0 584 return ['firefox', 'mozilla-firefox', 'iceweasel']
michael@0 585 if os.name == 'nt' or sys.platform == 'cygwin':
michael@0 586 return ['firefox']
michael@0 587
michael@0 588 class ThunderbirdRunner(Runner):
michael@0 589 """Specialized Runner subclass for running Thunderbird"""
michael@0 590
michael@0 591 app_name = 'Thunderbird'
michael@0 592 profile_class = ThunderbirdProfile
michael@0 593
michael@0 594 # The possible names of application bundles on Mac OS X, in order of
michael@0 595 # preference from most to least preferred.
michael@0 596 bundle_names = ["Thunderbird", "Shredder"]
michael@0 597
michael@0 598 # The possible names of binaries, in order of preference from most to least
michael@0 599 # preferred.
michael@0 600 names = ["thunderbird", "shredder"]
michael@0 601
michael@0 602 class CLI(object):
michael@0 603 """Command line interface."""
michael@0 604
michael@0 605 runner_class = FirefoxRunner
michael@0 606 profile_class = FirefoxProfile
michael@0 607 module = "mozrunner"
michael@0 608
michael@0 609 parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path.",
michael@0 610 metavar=None, default=None),
michael@0 611 ('-p', "--profile",): dict(dest="profile", help="Profile path.",
michael@0 612 metavar=None, default=None),
michael@0 613 ('-a', "--addons",): dict(dest="addons",
michael@0 614 help="Addons paths to install.",
michael@0 615 metavar=None, default=None),
michael@0 616 ("--info",): dict(dest="info", default=False,
michael@0 617 action="store_true",
michael@0 618 help="Print module information")
michael@0 619 }
michael@0 620
michael@0 621 def __init__(self):
michael@0 622 """ Setup command line parser and parse arguments """
michael@0 623 self.metadata = self.get_metadata_from_egg()
michael@0 624 self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"])
michael@0 625 for names, opts in self.parser_options.items():
michael@0 626 self.parser.add_option(*names, **opts)
michael@0 627 (self.options, self.args) = self.parser.parse_args()
michael@0 628
michael@0 629 if self.options.info:
michael@0 630 self.print_metadata()
michael@0 631 sys.exit(0)
michael@0 632
michael@0 633 # XXX should use action='append' instead of rolling our own
michael@0 634 try:
michael@0 635 self.addons = self.options.addons.split(',')
michael@0 636 except:
michael@0 637 self.addons = []
michael@0 638
michael@0 639 def get_metadata_from_egg(self):
michael@0 640 import pkg_resources
michael@0 641 ret = {}
michael@0 642 dist = pkg_resources.get_distribution(self.module)
michael@0 643 if dist.has_metadata("PKG-INFO"):
michael@0 644 for line in dist.get_metadata_lines("PKG-INFO"):
michael@0 645 key, value = line.split(':', 1)
michael@0 646 ret[key] = value
michael@0 647 if dist.has_metadata("requires.txt"):
michael@0 648 ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
michael@0 649 return ret
michael@0 650
michael@0 651 def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
michael@0 652 "Author", "Author-email", "License", "Platform", "Dependencies")):
michael@0 653 for key in data:
michael@0 654 if key in self.metadata:
michael@0 655 print key + ": " + self.metadata[key]
michael@0 656
michael@0 657 def create_runner(self):
michael@0 658 """ Get the runner object """
michael@0 659 runner = self.get_runner(binary=self.options.binary)
michael@0 660 profile = self.get_profile(binary=runner.binary,
michael@0 661 profile=self.options.profile,
michael@0 662 addons=self.addons)
michael@0 663 runner.profile = profile
michael@0 664 return runner
michael@0 665
michael@0 666 def get_runner(self, binary=None, profile=None):
michael@0 667 """Returns the runner instance for the given command line binary argument
michael@0 668 the profile instance returned from self.get_profile()."""
michael@0 669 return self.runner_class(binary, profile)
michael@0 670
michael@0 671 def get_profile(self, binary=None, profile=None, addons=None, preferences=None):
michael@0 672 """Returns the profile instance for the given command line arguments."""
michael@0 673 addons = addons or []
michael@0 674 preferences = preferences or {}
michael@0 675 return self.profile_class(binary, profile, addons, preferences)
michael@0 676
michael@0 677 def run(self):
michael@0 678 runner = self.create_runner()
michael@0 679 self.start(runner)
michael@0 680 runner.profile.cleanup()
michael@0 681
michael@0 682 def start(self, runner):
michael@0 683 """Starts the runner and waits for Firefox to exitor Keyboard Interrupt.
michael@0 684 Shoule be overwritten to provide custom running of the runner instance."""
michael@0 685 runner.start()
michael@0 686 print 'Started:', ' '.join(runner.command)
michael@0 687 try:
michael@0 688 runner.wait()
michael@0 689 except KeyboardInterrupt:
michael@0 690 runner.stop()
michael@0 691
michael@0 692
michael@0 693 def cli():
michael@0 694 CLI().run()

mercurial