Index: src/third_party/pylib/mozrunner/__init__.py |
=================================================================== |
--- src/third_party/pylib/mozrunner/__init__.py (revision 9275) |
+++ src/third_party/pylib/mozrunner/__init__.py (working copy) |
@@ -1,639 +0,0 @@ |
-# ***** BEGIN LICENSE BLOCK ***** |
-# Version: MPL 1.1/GPL 2.0/LGPL 2.1 |
-# |
-# The contents of this file are subject to the Mozilla Public License Version |
-# 1.1 (the "License"); you may not use this file except in compliance with |
-# the License. You may obtain a copy of the License at |
-# http://www.mozilla.org/MPL/ |
-# |
-# Software distributed under the License is distributed on an "AS IS" basis, |
-# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License |
-# for the specific language governing rights and limitations under the |
-# License. |
-# |
-# The Original Code is Mozilla Corporation Code. |
-# |
-# The Initial Developer of the Original Code is |
-# Mikeal Rogers. |
-# Portions created by the Initial Developer are Copyright (C) 2008-2009 |
-# the Initial Developer. All Rights Reserved. |
-# |
-# Contributor(s): |
-# Mikeal Rogers <mikeal.rogers@gmail.com> |
-# Clint Talbert <ctalbert@mozilla.com> |
-# Henrik Skupin <hskupin@mozilla.com> |
-# |
-# Alternatively, the contents of this file may be used under the terms of |
-# either the GNU General Public License Version 2 or later (the "GPL"), or |
-# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), |
-# in which case the provisions of the GPL or the LGPL are applicable instead |
-# of those above. If you wish to allow use of your version of this file only |
-# under the terms of either the GPL or the LGPL, and not to allow others to |
-# use your version of this file under the terms of the MPL, indicate your |
-# decision by deleting the provisions above and replace them with the notice |
-# and other provisions required by the GPL or the LGPL. If you do not delete |
-# the provisions above, a recipient may use your version of this file under |
-# the terms of any one of the MPL, the GPL or the LGPL. |
-# |
-# ***** END LICENSE BLOCK ***** |
- |
-import os |
-import sys |
-import copy |
-import tempfile |
-import signal |
-import commands |
-import zipfile |
-import optparse |
-import killableprocess |
-import subprocess |
-import platform |
- |
-from distutils import dir_util |
-from time import sleep |
-from xml.dom import minidom |
- |
-# conditional (version-dependent) imports |
-try: |
- import simplejson |
-except ImportError: |
- import json as simplejson |
- |
-import logging |
-logger = logging.getLogger(__name__) |
- |
-# Use dir_util for copy/rm operations because shutil is all kinds of broken |
-copytree = dir_util.copy_tree |
-rmtree = dir_util.remove_tree |
- |
-def findInPath(fileName, path=os.environ['PATH']): |
- dirs = path.split(os.pathsep) |
- for dir in dirs: |
- if os.path.isfile(os.path.join(dir, fileName)): |
- return os.path.join(dir, fileName) |
- if os.name == 'nt' or sys.platform == 'cygwin': |
- if os.path.isfile(os.path.join(dir, fileName + ".exe")): |
- return os.path.join(dir, fileName + ".exe") |
- return None |
- |
-stdout = sys.stdout |
-stderr = sys.stderr |
-stdin = sys.stdin |
- |
-def run_command(cmd, env=None, **kwargs): |
- """Run the given command in killable process.""" |
- killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin} |
- killable_kwargs.update(kwargs) |
- |
- if sys.platform != "win32": |
- return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0), |
- env=env, **killable_kwargs) |
- else: |
- return killableprocess.Popen(cmd, env=env, **killable_kwargs) |
- |
-def getoutput(l): |
- tmp = tempfile.mktemp() |
- x = open(tmp, 'w') |
- subprocess.call(l, stdout=x, stderr=x) |
- x.close(); x = open(tmp, 'r') |
- r = x.read() ; x.close() |
- os.remove(tmp) |
- return r |
- |
-def get_pids(name, minimun_pid=0): |
- """Get all the pids matching name, exclude any pids below minimum_pid.""" |
- if os.name == 'nt' or sys.platform == 'cygwin': |
- import wpk |
- |
- pids = wpk.get_pids(name) |
- |
- else: |
- # get_pids_cmd = ['ps', 'ax'] |
- # h = killableprocess.runCommand(get_pids_cmd, stdout=subprocess.PIPE, universal_newlines=True) |
- # h.wait(group=False) |
- # data = h.stdout.readlines() |
- data = getoutput(['ps', 'ax']).splitlines() |
- pids = [int(line.split()[0]) for line in data if line.find(name) is not -1] |
- |
- matching_pids = [m for m in pids if m > minimun_pid] |
- return matching_pids |
- |
-def kill_process_by_name(name): |
- """Find and kill all processes containing a certain name""" |
- |
- pids = get_pids(name) |
- |
- if os.name == 'nt' or sys.platform == 'cygwin': |
- for p in pids: |
- import wpk |
- |
- wpk.kill_pid(p) |
- |
- else: |
- for pid in pids: |
- try: |
- os.kill(pid, signal.SIGTERM) |
- except OSError: pass |
- sleep(.5) |
- if len(get_pids(name)) is not 0: |
- try: |
- os.kill(pid, signal.SIGKILL) |
- except OSError: pass |
- sleep(.5) |
- if len(get_pids(name)) is not 0: |
- logger.error('Could not kill process') |
- |
-def makedirs(name): |
- |
- head, tail = os.path.split(name) |
- if not tail: |
- head, tail = os.path.split(head) |
- if head and tail and not os.path.exists(head): |
- try: |
- makedirs(head) |
- except OSError, e: |
- pass |
- if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists |
- return |
- try: |
- os.mkdir(name) |
- except: |
- pass |
- |
-class Profile(object): |
- """Handles all operations regarding profile. Created new profiles, installs extensions, |
- sets preferences and handles cleanup.""" |
- |
- def __init__(self, binary=None, profile=None, addons=None, |
- preferences=None): |
- |
- self.binary = binary |
- |
- self.create_new = not(bool(profile)) |
- if profile: |
- self.profile = profile |
- else: |
- self.profile = self.create_new_profile(self.binary) |
- |
- self.addons_installed = [] |
- self.addons = addons or [] |
- |
- ### set preferences from class preferences |
- preferences = preferences or {} |
- if hasattr(self.__class__, 'preferences'): |
- self.preferences = self.__class__.preferences.copy() |
- else: |
- self.preferences = {} |
- self.preferences.update(preferences) |
- |
- for addon in self.addons: |
- self.install_addon(addon) |
- |
- self.set_preferences(self.preferences) |
- |
- def create_new_profile(self, binary): |
- """Create a new clean profile in tmp which is a simple empty folder""" |
- profile = tempfile.mkdtemp(suffix='.mozrunner') |
- return profile |
- |
- ### methods related to addons |
- |
- @classmethod |
- def addon_id(self, addon_path): |
- """ |
- return the id for a given addon, or None if not found |
- - addon_path : path to the addon directory |
- """ |
- |
- def find_id(desc): |
- """finds the addon id give its description""" |
- |
- addon_id = None |
- for elem in desc: |
- apps = elem.getElementsByTagName('em:targetApplication') |
- if apps: |
- for app in apps: |
- # remove targetApplication nodes, they contain id's we aren't interested in |
- elem.removeChild(app) |
- if elem.getElementsByTagName('em:id'): |
- addon_id = str(elem.getElementsByTagName('em:id')[0].firstChild.data) |
- elif elem.hasAttribute('em:id'): |
- addon_id = str(elem.getAttribute('em:id')) |
- return addon_id |
- |
- doc = minidom.parse(os.path.join(addon_path, 'install.rdf')) |
- |
- for tag in 'Description', 'RDF:Description': |
- desc = doc.getElementsByTagName(tag) |
- addon_id = find_id(desc) |
- if addon_id: |
- return addon_id |
- |
- |
- def install_addon(self, path): |
- """Installs the given addon or directory of addons in the profile.""" |
- |
- # if the addon is a directory, install all addons in it |
- addons = [path] |
- if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')): |
- addons = [os.path.join(path, x) for x in os.listdir(path)] |
- |
- # install each addon |
- for addon in addons: |
- |
- # if the addon is an .xpi, uncompress it to a temporary directory |
- if addon.endswith('.xpi'): |
- tmpdir = tempfile.mkdtemp(suffix = "." + os.path.split(addon)[-1]) |
- compressed_file = zipfile.ZipFile(addon, "r") |
- for name in compressed_file.namelist(): |
- if name.endswith('/'): |
- makedirs(os.path.join(tmpdir, name)) |
- else: |
- if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))): |
- makedirs(os.path.dirname(os.path.join(tmpdir, name))) |
- data = compressed_file.read(name) |
- f = open(os.path.join(tmpdir, name), 'wb') |
- f.write(data) ; f.close() |
- addon = tmpdir |
- |
- # determine the addon id |
- addon_id = Profile.addon_id(addon) |
- assert addon_id is not None, "The addon id could not be found: %s" % addon |
- |
- # copy the addon to the profile |
- addon_path = os.path.join(self.profile, 'extensions', addon_id) |
- copytree(addon, addon_path, preserve_symlinks=1) |
- self.addons_installed.append(addon_path) |
- |
- def clean_addons(self): |
- """Cleans up addons in the profile.""" |
- for addon in self.addons_installed: |
- if os.path.isdir(addon): |
- rmtree(addon) |
- |
- ### methods related to preferences |
- |
- def set_preferences(self, preferences): |
- """Adds preferences dict to profile preferences""" |
- |
- prefs_file = os.path.join(self.profile, 'user.js') |
- |
- # Ensure that the file exists first otherwise create an empty file |
- if os.path.isfile(prefs_file): |
- f = open(prefs_file, 'a+') |
- else: |
- f = open(prefs_file, 'w') |
- |
- f.write('\n#MozRunner Prefs Start\n') |
- |
- pref_lines = ['user_pref(%s, %s);' % |
- (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in |
- preferences.items()] |
- for line in pref_lines: |
- f.write(line+'\n') |
- f.write('#MozRunner Prefs End\n') |
- f.flush() ; f.close() |
- |
- def clean_preferences(self): |
- """Removed preferences added by mozrunner.""" |
- lines = open(os.path.join(self.profile, 'user.js'), 'r').read().splitlines() |
- s = lines.index('#MozRunner Prefs Start') ; e = lines.index('#MozRunner Prefs End') |
- cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:]) |
- f = open(os.path.join(self.profile, 'user.js'), 'w') |
- f.write(cleaned_prefs) ; f.flush() ; f.close() |
- |
- ### cleanup |
- |
- def cleanup(self): |
- """Cleanup operations on the profile.""" |
- if self.create_new: |
- rmtree(self.profile) |
- else: |
- self.clean_preferences() |
- self.clean_addons() |
- |
-class FirefoxProfile(Profile): |
- """Specialized Profile subclass for Firefox""" |
- preferences = {# Don't automatically update the application |
- 'app.update.enabled' : False, |
- # Don't restore the last open set of tabs if the browser has crashed |
- 'browser.sessionstore.resume_from_crash': False, |
- # Don't check for the default web browser |
- 'browser.shell.checkDefaultBrowser' : False, |
- # Don't warn on exit when multiple tabs are open |
- 'browser.tabs.warnOnClose' : False, |
- # Don't warn when exiting the browser |
- 'browser.warnOnQuit': False, |
- # Only install add-ons from the profile and the app folder |
- 'extensions.enabledScopes' : 5, |
- # Dont' run the add-on compatibility check during start-up |
- 'extensions.showMismatchUI' : False, |
- # Don't automatically update add-ons |
- 'extensions.update.enabled' : False, |
- # Don't open a dialog to show available add-on updates |
- 'extensions.update.notifyUser' : False, |
- } |
- |
- @property |
- def names(self): |
- if sys.platform == 'darwin': |
- return ['firefox', 'minefield', 'shiretoko'] |
- if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): |
- return ['firefox', 'mozilla-firefox', 'iceweasel'] |
- if os.name == 'nt' or sys.platform == 'cygwin': |
- return ['firefox'] |
- |
-class ThunderbirdProfile(Profile): |
- preferences = {'extensions.update.enabled' : False, |
- 'extensions.update.notifyUser' : False, |
- 'browser.shell.checkDefaultBrowser' : False, |
- 'browser.tabs.warnOnClose' : False, |
- 'browser.warnOnQuit': False, |
- 'browser.sessionstore.resume_from_crash': False, |
- } |
- names = ["thunderbird", "shredder"] |
- |
- |
-class Runner(object): |
- """Handles all running operations. Finds bins, runs and kills the process.""" |
- |
- def __init__(self, binary=None, profile=None, cmdargs=[], env=None, |
- aggressively_kill=['crashreporter'], kp_kwargs={}): |
- if binary is None: |
- self.binary = self.find_binary() |
- elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1: |
- self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.names[0]) |
- else: |
- self.binary = binary |
- |
- if not os.path.exists(self.binary): |
- raise Exception("Binary path does not exist "+self.binary) |
- |
- if sys.platform == 'linux2' and self.binary.endswith('-bin'): |
- dirname = os.path.dirname(self.binary) |
- if os.environ.get('LD_LIBRARY_PATH', None): |
- os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) |
- else: |
- os.environ['LD_LIBRARY_PATH'] = dirname |
- |
- self.profile = profile |
- |
- self.cmdargs = cmdargs |
- if env is None: |
- self.env = copy.copy(os.environ) |
- self.env.update({'MOZ_NO_REMOTE':"1",}) |
- else: |
- self.env = env |
- self.aggressively_kill = aggressively_kill |
- self.kp_kwargs = kp_kwargs |
- |
- def find_binary(self): |
- """Finds the binary for self.names if one was not provided.""" |
- binary = None |
- if sys.platform in ('linux2', 'sunos5', 'solaris'): |
- for name in reversed(self.names): |
- binary = findInPath(name) |
- elif os.name == 'nt' or sys.platform == 'cygwin': |
- |
- # find the default executable from the windows registry |
- try: |
- # assumes self.app_name is defined, as it should be for |
- # implementors |
- import _winreg |
- app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"Software\Mozilla\Mozilla %s" % self.app_name) |
- version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion") |
- version_key = _winreg.OpenKey(app_key, version + r"\Main") |
- path, _ = _winreg.QueryValueEx(version_key, "PathToExe") |
- return path |
- except: # XXX not sure what type of exception this should be |
- pass |
- |
- # search for the binary in the path |
- for name in reversed(self.names): |
- binary = findInPath(name) |
- if sys.platform == 'cygwin': |
- program_files = os.environ['PROGRAMFILES'] |
- else: |
- program_files = os.environ['ProgramFiles'] |
- |
- if binary is None: |
- for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'), |
- (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'), |
- (program_files,'Minefield', 'firefox.exe'), |
- (os.environ.get("ProgramFiles(x86)"),'Minefield', 'firefox.exe') |
- ]: |
- path = os.path.join(*bin) |
- if os.path.isfile(path): |
- binary = path |
- break |
- elif sys.platform == 'darwin': |
- for name in reversed(self.names): |
- appdir = os.path.join('Applications', name.capitalize()+'.app') |
- if os.path.isdir(os.path.join(os.path.expanduser('~/'), appdir)): |
- binary = os.path.join(os.path.expanduser('~/'), appdir, |
- 'Contents/MacOS/'+name+'-bin') |
- elif os.path.isdir('/'+appdir): |
- binary = os.path.join("/"+appdir, 'Contents/MacOS/'+name+'-bin') |
- |
- if binary is not None: |
- if not os.path.isfile(binary): |
- binary = binary.replace(name+'-bin', 'firefox-bin') |
- if not os.path.isfile(binary): |
- binary = None |
- if binary is None: |
- raise Exception('Mozrunner could not locate your binary, you will need to set it.') |
- return binary |
- |
- @property |
- def command(self): |
- """Returns the command list to run.""" |
- cmd = [self.binary, '-profile', self.profile.profile] |
- # On i386 OS X machines, i386+x86_64 universal binaries need to be told |
- # to run as i386 binaries. If we're not running a i386+x86_64 universal |
- # binary, then this command modification is harmless. |
- if sys.platform == 'darwin': |
- if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit': |
- cmd = ['arch', '-i386'] + cmd |
- return cmd |
- |
- def get_repositoryInfo(self): |
- """Read repository information from application.ini and platform.ini.""" |
- import ConfigParser |
- |
- config = ConfigParser.RawConfigParser() |
- dirname = os.path.dirname(self.binary) |
- repository = { } |
- |
- for entry in [['application', 'App'], ['platform', 'Build']]: |
- (file, section) = entry |
- config.read(os.path.join(dirname, '%s.ini' % file)) |
- |
- for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]: |
- (key, id) = entry |
- |
- try: |
- repository['%s_%s' % (file, id)] = config.get(section, key); |
- except: |
- repository['%s_%s' % (file, id)] = None |
- |
- return repository |
- |
- def start(self): |
- """Run self.command in the proper environment.""" |
- if self.profile is None: |
- self.profile = self.profile_class() |
- self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs) |
- |
- def wait(self, timeout=None): |
- """Wait for the browser to exit.""" |
- self.process_handler.wait(timeout=timeout) |
- |
- if sys.platform != 'win32': |
- for name in self.names: |
- for pid in get_pids(name, self.process_handler.pid): |
- self.process_handler.pid = pid |
- self.process_handler.wait(timeout=timeout) |
- |
- def kill(self, kill_signal=signal.SIGTERM): |
- """Kill the browser""" |
- if sys.platform != 'win32': |
- self.process_handler.kill() |
- for name in self.names: |
- for pid in get_pids(name, self.process_handler.pid): |
- self.process_handler.pid = pid |
- self.process_handler.kill() |
- else: |
- try: |
- self.process_handler.kill(group=True) |
- except Exception, e: |
- logger.error('Cannot kill process, '+type(e).__name__+' '+e.message) |
- |
- for name in self.aggressively_kill: |
- kill_process_by_name(name) |
- |
- def stop(self): |
- self.kill() |
- |
-class FirefoxRunner(Runner): |
- """Specialized Runner subclass for running Firefox.""" |
- |
- app_name = 'Firefox' |
- profile_class = FirefoxProfile |
- |
- @property |
- def names(self): |
- if sys.platform == 'darwin': |
- return ['firefox', 'minefield', 'shiretoko'] |
- if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): |
- return ['firefox', 'mozilla-firefox', 'iceweasel'] |
- if os.name == 'nt' or sys.platform == 'cygwin': |
- return ['firefox'] |
- |
-class ThunderbirdRunner(Runner): |
- """Specialized Runner subclass for running Thunderbird""" |
- |
- app_name = 'Thunderbird' |
- profile_class = ThunderbirdProfile |
- |
- names = ["thunderbird", "shredder"] |
- |
-class CLI(object): |
- """Command line interface.""" |
- |
- runner_class = FirefoxRunner |
- profile_class = FirefoxProfile |
- module = "mozrunner" |
- |
- parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path.", |
- metavar=None, default=None), |
- ('-p', "--profile",): dict(dest="profile", help="Profile path.", |
- metavar=None, default=None), |
- ('-a', "--addons",): dict(dest="addons", |
- help="Addons paths to install.", |
- metavar=None, default=None), |
- ("--info",): dict(dest="info", default=False, |
- action="store_true", |
- help="Print module information") |
- } |
- |
- def __init__(self): |
- """ Setup command line parser and parse arguments """ |
- self.metadata = self.get_metadata_from_egg() |
- self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"]) |
- for names, opts in self.parser_options.items(): |
- self.parser.add_option(*names, **opts) |
- (self.options, self.args) = self.parser.parse_args() |
- |
- if self.options.info: |
- self.print_metadata() |
- sys.exit(0) |
- |
- # XXX should use action='append' instead of rolling our own |
- try: |
- self.addons = self.options.addons.split(',') |
- except: |
- self.addons = [] |
- |
- def get_metadata_from_egg(self): |
- import pkg_resources |
- ret = {} |
- dist = pkg_resources.get_distribution(self.module) |
- if dist.has_metadata("PKG-INFO"): |
- for line in dist.get_metadata_lines("PKG-INFO"): |
- key, value = line.split(':', 1) |
- ret[key] = value |
- if dist.has_metadata("requires.txt"): |
- ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") |
- return ret |
- |
- def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", |
- "Author", "Author-email", "License", "Platform", "Dependencies")): |
- for key in data: |
- if key in self.metadata: |
- print key + ": " + self.metadata[key] |
- |
- def create_runner(self): |
- """ Get the runner object """ |
- runner = self.get_runner(binary=self.options.binary) |
- profile = self.get_profile(binary=runner.binary, |
- profile=self.options.profile, |
- addons=self.addons) |
- runner.profile = profile |
- return runner |
- |
- def get_runner(self, binary=None, profile=None): |
- """Returns the runner instance for the given command line binary argument |
- the profile instance returned from self.get_profile().""" |
- return self.runner_class(binary, profile) |
- |
- def get_profile(self, binary=None, profile=None, addons=None, preferences=None): |
- """Returns the profile instance for the given command line arguments.""" |
- addons = addons or [] |
- preferences = preferences or {} |
- return self.profile_class(binary, profile, addons, preferences) |
- |
- def run(self): |
- runner = self.create_runner() |
- self.start(runner) |
- runner.profile.cleanup() |
- |
- def start(self, runner): |
- """Starts the runner and waits for Firefox to exitor Keyboard Interrupt. |
- Shoule be overwritten to provide custom running of the runner instance.""" |
- runner.start() |
- print 'Started:', ' '.join(runner.command) |
- try: |
- runner.wait() |
- except KeyboardInterrupt: |
- runner.stop() |
- |
- |
-def cli(): |
- CLI().run() |
- |
-def print_addon_ids(args=sys.argv[1:]): |
- """print addon ids for testing""" |
- for arg in args: |
- print Profile.addon_id(arg) |
- |
- |