Thu, 15 Jan 2015 15:59:08 +0100
Implement a real Private Browsing Mode condition by changing the API/ABI;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
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() |