michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: import os michael@0: import sys michael@0: import copy michael@0: import tempfile michael@0: import signal michael@0: import commands michael@0: import zipfile michael@0: import optparse michael@0: import killableprocess michael@0: import subprocess michael@0: import platform michael@0: import shutil michael@0: from StringIO import StringIO michael@0: from xml.dom import minidom michael@0: michael@0: from distutils import dir_util michael@0: from time import sleep michael@0: michael@0: # conditional (version-dependent) imports michael@0: try: michael@0: import simplejson michael@0: except ImportError: michael@0: import json as simplejson michael@0: michael@0: import logging michael@0: logger = logging.getLogger(__name__) michael@0: michael@0: # Use dir_util for copy/rm operations because shutil is all kinds of broken michael@0: copytree = dir_util.copy_tree michael@0: rmtree = dir_util.remove_tree michael@0: michael@0: def findInPath(fileName, path=os.environ['PATH']): michael@0: dirs = path.split(os.pathsep) michael@0: for dir in dirs: michael@0: if os.path.isfile(os.path.join(dir, fileName)): michael@0: return os.path.join(dir, fileName) michael@0: if os.name == 'nt' or sys.platform == 'cygwin': michael@0: if os.path.isfile(os.path.join(dir, fileName + ".exe")): michael@0: return os.path.join(dir, fileName + ".exe") michael@0: return None michael@0: michael@0: stdout = sys.stdout michael@0: stderr = sys.stderr michael@0: stdin = sys.stdin michael@0: michael@0: def run_command(cmd, env=None, **kwargs): michael@0: """Run the given command in killable process.""" michael@0: killable_kwargs = {'stdout':stdout ,'stderr':stderr, 'stdin':stdin} michael@0: killable_kwargs.update(kwargs) michael@0: michael@0: if sys.platform != "win32": michael@0: return killableprocess.Popen(cmd, preexec_fn=lambda : os.setpgid(0, 0), michael@0: env=env, **killable_kwargs) michael@0: else: michael@0: return killableprocess.Popen(cmd, env=env, **killable_kwargs) michael@0: michael@0: def getoutput(l): michael@0: tmp = tempfile.mktemp() michael@0: x = open(tmp, 'w') michael@0: subprocess.call(l, stdout=x, stderr=x) michael@0: x.close(); x = open(tmp, 'r') michael@0: r = x.read() ; x.close() michael@0: os.remove(tmp) michael@0: return r michael@0: michael@0: def get_pids(name, minimun_pid=0): michael@0: """Get all the pids matching name, exclude any pids below minimum_pid.""" michael@0: if os.name == 'nt' or sys.platform == 'cygwin': michael@0: import wpk michael@0: michael@0: pids = wpk.get_pids(name) michael@0: michael@0: else: michael@0: data = getoutput(['ps', 'ax']).splitlines() michael@0: pids = [int(line.split()[0]) for line in data if line.find(name) is not -1] michael@0: michael@0: matching_pids = [m for m in pids if m > minimun_pid] michael@0: return matching_pids michael@0: michael@0: def makedirs(name): michael@0: michael@0: head, tail = os.path.split(name) michael@0: if not tail: michael@0: head, tail = os.path.split(head) michael@0: if head and tail and not os.path.exists(head): michael@0: try: michael@0: makedirs(head) michael@0: except OSError, e: michael@0: pass michael@0: if tail == os.curdir: # xxx/newdir/. exists if xxx/newdir exists michael@0: return michael@0: try: michael@0: os.mkdir(name) michael@0: except: michael@0: pass michael@0: michael@0: # addon_details() copied from mozprofile michael@0: def addon_details(install_rdf_fh): michael@0: """ michael@0: returns a dictionary of details about the addon michael@0: - addon_path : path to the addon directory michael@0: Returns: michael@0: {'id': u'rainbow@colors.org', # id of the addon michael@0: 'version': u'1.4', # version of the addon michael@0: 'name': u'Rainbow', # name of the addon michael@0: 'unpack': # whether to unpack the addon michael@0: """ michael@0: michael@0: details = { michael@0: 'id': None, michael@0: 'unpack': False, michael@0: 'name': None, michael@0: 'version': None michael@0: } michael@0: michael@0: def get_namespace_id(doc, url): michael@0: attributes = doc.documentElement.attributes michael@0: namespace = "" michael@0: for i in range(attributes.length): michael@0: if attributes.item(i).value == url: michael@0: if ":" in attributes.item(i).name: michael@0: # If the namespace is not the default one remove 'xlmns:' michael@0: namespace = attributes.item(i).name.split(':')[1] + ":" michael@0: break michael@0: return namespace michael@0: michael@0: def get_text(element): michael@0: """Retrieve the text value of a given node""" michael@0: rc = [] michael@0: for node in element.childNodes: michael@0: if node.nodeType == node.TEXT_NODE: michael@0: rc.append(node.data) michael@0: return ''.join(rc).strip() michael@0: michael@0: doc = minidom.parse(install_rdf_fh) michael@0: michael@0: # Get the namespaces abbreviations michael@0: em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#") michael@0: rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#") michael@0: michael@0: description = doc.getElementsByTagName(rdf + "Description").item(0) michael@0: for node in description.childNodes: michael@0: # Remove the namespace prefix from the tag for comparison michael@0: entry = node.nodeName.replace(em, "") michael@0: if entry in details.keys(): michael@0: details.update({ entry: get_text(node) }) michael@0: michael@0: # turn unpack into a true/false value michael@0: if isinstance(details['unpack'], basestring): michael@0: details['unpack'] = details['unpack'].lower() == 'true' michael@0: michael@0: return details michael@0: michael@0: class Profile(object): michael@0: """Handles all operations regarding profile. Created new profiles, installs extensions, michael@0: sets preferences and handles cleanup.""" michael@0: michael@0: def __init__(self, binary=None, profile=None, addons=None, michael@0: preferences=None): michael@0: michael@0: self.binary = binary michael@0: michael@0: self.create_new = not(bool(profile)) michael@0: if profile: michael@0: self.profile = profile michael@0: else: michael@0: self.profile = self.create_new_profile(self.binary) michael@0: michael@0: self.addons_installed = [] michael@0: self.addons = addons or [] michael@0: michael@0: ### set preferences from class preferences michael@0: preferences = preferences or {} michael@0: if hasattr(self.__class__, 'preferences'): michael@0: self.preferences = self.__class__.preferences.copy() michael@0: else: michael@0: self.preferences = {} michael@0: self.preferences.update(preferences) michael@0: michael@0: for addon in self.addons: michael@0: self.install_addon(addon) michael@0: michael@0: self.set_preferences(self.preferences) michael@0: michael@0: def create_new_profile(self, binary): michael@0: """Create a new clean profile in tmp which is a simple empty folder""" michael@0: profile = tempfile.mkdtemp(suffix='.mozrunner') michael@0: return profile michael@0: michael@0: def unpack_addon(self, xpi_zipfile, addon_path): michael@0: for name in xpi_zipfile.namelist(): michael@0: if name.endswith('/'): michael@0: makedirs(os.path.join(addon_path, name)) michael@0: else: michael@0: if not os.path.isdir(os.path.dirname(os.path.join(addon_path, name))): michael@0: makedirs(os.path.dirname(os.path.join(addon_path, name))) michael@0: data = xpi_zipfile.read(name) michael@0: f = open(os.path.join(addon_path, name), 'wb') michael@0: f.write(data) ; f.close() michael@0: zi = xpi_zipfile.getinfo(name) michael@0: os.chmod(os.path.join(addon_path,name), (zi.external_attr>>16)) michael@0: michael@0: def install_addon(self, path): michael@0: """Installs the given addon or directory of addons in the profile.""" michael@0: michael@0: extensions_path = os.path.join(self.profile, 'extensions') michael@0: if not os.path.exists(extensions_path): michael@0: os.makedirs(extensions_path) michael@0: michael@0: addons = [path] michael@0: if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')): michael@0: addons = [os.path.join(path, x) for x in os.listdir(path)] michael@0: michael@0: for addon in addons: michael@0: if addon.endswith('.xpi'): michael@0: xpi_zipfile = zipfile.ZipFile(addon, "r") michael@0: details = addon_details(StringIO(xpi_zipfile.read('install.rdf'))) michael@0: addon_path = os.path.join(extensions_path, details["id"]) michael@0: if details.get("unpack", True): michael@0: self.unpack_addon(xpi_zipfile, addon_path) michael@0: self.addons_installed.append(addon_path) michael@0: else: michael@0: shutil.copy(addon, addon_path + '.xpi') michael@0: else: michael@0: # it's already unpacked, but we need to extract the id so we michael@0: # can copy it michael@0: details = addon_details(open(os.path.join(addon, "install.rdf"), "rb")) michael@0: addon_path = os.path.join(extensions_path, details["id"]) michael@0: shutil.copytree(addon, addon_path, symlinks=True) michael@0: michael@0: def set_preferences(self, preferences): michael@0: """Adds preferences dict to profile preferences""" michael@0: prefs_file = os.path.join(self.profile, 'user.js') michael@0: # Ensure that the file exists first otherwise create an empty file michael@0: if os.path.isfile(prefs_file): michael@0: f = open(prefs_file, 'a+') michael@0: else: michael@0: f = open(prefs_file, 'w') michael@0: michael@0: f.write('\n#MozRunner Prefs Start\n') michael@0: michael@0: pref_lines = ['user_pref(%s, %s);' % michael@0: (simplejson.dumps(k), simplejson.dumps(v) ) for k, v in michael@0: preferences.items()] michael@0: for line in pref_lines: michael@0: f.write(line+'\n') michael@0: f.write('#MozRunner Prefs End\n') michael@0: f.flush() ; f.close() michael@0: michael@0: def pop_preferences(self): michael@0: """ michael@0: pop the last set of preferences added michael@0: returns True if popped michael@0: """ michael@0: michael@0: # our magic markers michael@0: delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End') michael@0: michael@0: lines = file(os.path.join(self.profile, 'user.js')).read().splitlines() michael@0: def last_index(_list, value): michael@0: """ michael@0: returns the last index of an item; michael@0: this should actually be part of python code but it isn't michael@0: """ michael@0: for index in reversed(range(len(_list))): michael@0: if _list[index] == value: michael@0: return index michael@0: s = last_index(lines, delimeters[0]) michael@0: e = last_index(lines, delimeters[1]) michael@0: michael@0: # ensure both markers are found michael@0: if s is None: michael@0: assert e is None, '%s found without %s' % (delimeters[1], delimeters[0]) michael@0: return False # no preferences found michael@0: elif e is None: michael@0: assert e is None, '%s found without %s' % (delimeters[0], delimeters[1]) michael@0: michael@0: # ensure the markers are in the proper order michael@0: assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s) michael@0: michael@0: # write the prefs michael@0: cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:]) michael@0: f = file(os.path.join(self.profile, 'user.js'), 'w') michael@0: f.write(cleaned_prefs) michael@0: f.close() michael@0: return True michael@0: michael@0: def clean_preferences(self): michael@0: """Removed preferences added by mozrunner.""" michael@0: while True: michael@0: if not self.pop_preferences(): michael@0: break michael@0: michael@0: def clean_addons(self): michael@0: """Cleans up addons in the profile.""" michael@0: for addon in self.addons_installed: michael@0: if os.path.isdir(addon): michael@0: rmtree(addon) michael@0: michael@0: def cleanup(self): michael@0: """Cleanup operations on the profile.""" michael@0: def oncleanup_error(function, path, excinfo): michael@0: #TODO: How should we handle this? michael@0: print "Error Cleaning up: " + str(excinfo[1]) michael@0: if self.create_new: michael@0: shutil.rmtree(self.profile, False, oncleanup_error) michael@0: else: michael@0: self.clean_preferences() michael@0: self.clean_addons() michael@0: michael@0: class FirefoxProfile(Profile): michael@0: """Specialized Profile subclass for Firefox""" michael@0: preferences = {# Don't automatically update the application michael@0: 'app.update.enabled' : False, michael@0: # Don't restore the last open set of tabs if the browser has crashed michael@0: 'browser.sessionstore.resume_from_crash': False, michael@0: # Don't check for the default web browser michael@0: 'browser.shell.checkDefaultBrowser' : False, michael@0: # Don't warn on exit when multiple tabs are open michael@0: 'browser.tabs.warnOnClose' : False, michael@0: # Don't warn when exiting the browser michael@0: 'browser.warnOnQuit': False, michael@0: # Only install add-ons from the profile and the app folder michael@0: 'extensions.enabledScopes' : 5, michael@0: # Don't automatically update add-ons michael@0: 'extensions.update.enabled' : False, michael@0: # Don't open a dialog to show available add-on updates michael@0: 'extensions.update.notifyUser' : False, michael@0: } michael@0: michael@0: # The possible names of application bundles on Mac OS X, in order of michael@0: # preference from most to least preferred. michael@0: # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly, michael@0: # but it will still be present if users update an older nightly build michael@0: # via the app update service. michael@0: bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly'] michael@0: michael@0: # The possible names of binaries, in order of preference from most to least michael@0: # preferred. michael@0: @property michael@0: def names(self): michael@0: if sys.platform == 'darwin': michael@0: return ['firefox', 'nightly', 'shiretoko'] michael@0: if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')): michael@0: return ['firefox', 'mozilla-firefox', 'iceweasel'] michael@0: if os.name == 'nt' or sys.platform == 'cygwin': michael@0: return ['firefox'] michael@0: michael@0: class ThunderbirdProfile(Profile): michael@0: preferences = {'extensions.update.enabled' : False, michael@0: 'extensions.update.notifyUser' : False, michael@0: 'browser.shell.checkDefaultBrowser' : False, michael@0: 'browser.tabs.warnOnClose' : False, michael@0: 'browser.warnOnQuit': False, michael@0: 'browser.sessionstore.resume_from_crash': False, michael@0: } michael@0: michael@0: # The possible names of application bundles on Mac OS X, in order of michael@0: # preference from most to least preferred. michael@0: bundle_names = ["Thunderbird", "Shredder"] michael@0: michael@0: # The possible names of binaries, in order of preference from most to least michael@0: # preferred. michael@0: names = ["thunderbird", "shredder"] michael@0: michael@0: michael@0: class Runner(object): michael@0: """Handles all running operations. Finds bins, runs and kills the process.""" michael@0: michael@0: def __init__(self, binary=None, profile=None, cmdargs=[], env=None, michael@0: kp_kwargs={}): michael@0: if binary is None: michael@0: self.binary = self.find_binary() michael@0: elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1: michael@0: self.binary = os.path.join(binary, 'Contents/MacOS/%s-bin' % self.names[0]) michael@0: else: michael@0: self.binary = binary michael@0: michael@0: if not os.path.exists(self.binary): michael@0: raise Exception("Binary path does not exist "+self.binary) michael@0: michael@0: if sys.platform == 'linux2' and self.binary.endswith('-bin'): michael@0: dirname = os.path.dirname(self.binary) michael@0: if os.environ.get('LD_LIBRARY_PATH', None): michael@0: os.environ['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) michael@0: else: michael@0: os.environ['LD_LIBRARY_PATH'] = dirname michael@0: michael@0: # Disable the crash reporter by default michael@0: os.environ['MOZ_CRASHREPORTER_NO_REPORT'] = '1' michael@0: michael@0: self.profile = profile michael@0: michael@0: self.cmdargs = cmdargs michael@0: if env is None: michael@0: self.env = copy.copy(os.environ) michael@0: self.env.update({'MOZ_NO_REMOTE':"1",}) michael@0: else: michael@0: self.env = env michael@0: self.kp_kwargs = kp_kwargs or {} michael@0: michael@0: def find_binary(self): michael@0: """Finds the binary for self.names if one was not provided.""" michael@0: binary = None michael@0: if sys.platform in ('linux2', 'sunos5', 'solaris') \ michael@0: or sys.platform.startswith('freebsd'): michael@0: for name in reversed(self.names): michael@0: binary = findInPath(name) michael@0: elif os.name == 'nt' or sys.platform == 'cygwin': michael@0: michael@0: # find the default executable from the windows registry michael@0: try: michael@0: import _winreg michael@0: except ImportError: michael@0: pass michael@0: else: michael@0: sam_flags = [0] michael@0: # KEY_WOW64_32KEY etc only appeared in 2.6+, but that's OK as michael@0: # only 2.6+ has functioning 64bit builds. michael@0: if hasattr(_winreg, "KEY_WOW64_32KEY"): michael@0: if "64 bit" in sys.version: michael@0: # a 64bit Python should also look in the 32bit registry michael@0: sam_flags.append(_winreg.KEY_WOW64_32KEY) michael@0: else: michael@0: # possibly a 32bit Python on 64bit Windows, so look in michael@0: # the 64bit registry incase there is a 64bit app. michael@0: sam_flags.append(_winreg.KEY_WOW64_64KEY) michael@0: for sam_flag in sam_flags: michael@0: try: michael@0: # assumes self.app_name is defined, as it should be for michael@0: # implementors michael@0: keyname = r"Software\Mozilla\Mozilla %s" % self.app_name michael@0: sam = _winreg.KEY_READ | sam_flag michael@0: app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keyname, 0, sam) michael@0: version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion") michael@0: version_key = _winreg.OpenKey(app_key, version + r"\Main") michael@0: path, _ = _winreg.QueryValueEx(version_key, "PathToExe") michael@0: return path michael@0: except _winreg.error: michael@0: pass michael@0: michael@0: # search for the binary in the path michael@0: for name in reversed(self.names): michael@0: binary = findInPath(name) michael@0: if sys.platform == 'cygwin': michael@0: program_files = os.environ['PROGRAMFILES'] michael@0: else: michael@0: program_files = os.environ['ProgramFiles'] michael@0: michael@0: if binary is None: michael@0: for bin in [(program_files, 'Mozilla Firefox', 'firefox.exe'), michael@0: (os.environ.get("ProgramFiles(x86)"),'Mozilla Firefox', 'firefox.exe'), michael@0: (program_files, 'Nightly', 'firefox.exe'), michael@0: (os.environ.get("ProgramFiles(x86)"),'Nightly', 'firefox.exe'), michael@0: (program_files, 'Aurora', 'firefox.exe'), michael@0: (os.environ.get("ProgramFiles(x86)"),'Aurora', 'firefox.exe') michael@0: ]: michael@0: path = os.path.join(*bin) michael@0: if os.path.isfile(path): michael@0: binary = path michael@0: break michael@0: elif sys.platform == 'darwin': michael@0: for bundle_name in self.bundle_names: michael@0: # Look for the application bundle in the user's home directory michael@0: # or the system-wide /Applications directory. If we don't find michael@0: # it in one of those locations, we move on to the next possible michael@0: # bundle name. michael@0: appdir = os.path.join("~/Applications/%s.app" % bundle_name) michael@0: if not os.path.isdir(appdir): michael@0: appdir = "/Applications/%s.app" % bundle_name michael@0: if not os.path.isdir(appdir): michael@0: continue michael@0: michael@0: # Look for a binary with any of the possible binary names michael@0: # inside the application bundle. michael@0: for binname in self.names: michael@0: binpath = os.path.join(appdir, michael@0: "Contents/MacOS/%s-bin" % binname) michael@0: if (os.path.isfile(binpath)): michael@0: binary = binpath michael@0: break michael@0: michael@0: if binary: michael@0: break michael@0: michael@0: if binary is None: michael@0: raise Exception('Mozrunner could not locate your binary, you will need to set it.') michael@0: return binary michael@0: michael@0: @property michael@0: def command(self): michael@0: """Returns the command list to run.""" michael@0: cmd = [self.binary, '-profile', self.profile.profile] michael@0: # On i386 OS X machines, i386+x86_64 universal binaries need to be told michael@0: # to run as i386 binaries. If we're not running a i386+x86_64 universal michael@0: # binary, then this command modification is harmless. michael@0: if sys.platform == 'darwin': michael@0: if hasattr(platform, 'architecture') and platform.architecture()[0] == '32bit': michael@0: cmd = ['arch', '-i386'] + cmd michael@0: return cmd michael@0: michael@0: def get_repositoryInfo(self): michael@0: """Read repository information from application.ini and platform.ini.""" michael@0: import ConfigParser michael@0: michael@0: config = ConfigParser.RawConfigParser() michael@0: dirname = os.path.dirname(self.binary) michael@0: repository = { } michael@0: michael@0: for entry in [['application', 'App'], ['platform', 'Build']]: michael@0: (file, section) = entry michael@0: config.read(os.path.join(dirname, '%s.ini' % file)) michael@0: michael@0: for entry in [['SourceRepository', 'repository'], ['SourceStamp', 'changeset']]: michael@0: (key, id) = entry michael@0: michael@0: try: michael@0: repository['%s_%s' % (file, id)] = config.get(section, key); michael@0: except: michael@0: repository['%s_%s' % (file, id)] = None michael@0: michael@0: return repository michael@0: michael@0: def start(self): michael@0: """Run self.command in the proper environment.""" michael@0: if self.profile is None: michael@0: self.profile = self.profile_class() michael@0: self.process_handler = run_command(self.command+self.cmdargs, self.env, **self.kp_kwargs) michael@0: michael@0: def wait(self, timeout=None): michael@0: """Wait for the browser to exit.""" michael@0: self.process_handler.wait(timeout=timeout) michael@0: michael@0: if sys.platform != 'win32': michael@0: for name in self.names: michael@0: for pid in get_pids(name, self.process_handler.pid): michael@0: self.process_handler.pid = pid michael@0: self.process_handler.wait(timeout=timeout) michael@0: michael@0: def kill(self, kill_signal=signal.SIGTERM): michael@0: """Kill the browser""" michael@0: if sys.platform != 'win32': michael@0: self.process_handler.kill() michael@0: for name in self.names: michael@0: for pid in get_pids(name, self.process_handler.pid): michael@0: self.process_handler.pid = pid michael@0: self.process_handler.kill() michael@0: else: michael@0: try: michael@0: self.process_handler.kill(group=True) michael@0: # On windows, it sometimes behooves one to wait for dust to settle michael@0: # after killing processes. Let's try that. michael@0: # TODO: Bug 640047 is invesitgating the correct way to handle this case michael@0: self.process_handler.wait(timeout=10) michael@0: except Exception, e: michael@0: logger.error('Cannot kill process, '+type(e).__name__+' '+e.message) michael@0: michael@0: def stop(self): michael@0: self.kill() michael@0: michael@0: class FirefoxRunner(Runner): michael@0: """Specialized Runner subclass for running Firefox.""" michael@0: michael@0: app_name = 'Firefox' michael@0: profile_class = FirefoxProfile michael@0: michael@0: # The possible names of application bundles on Mac OS X, in order of michael@0: # preference from most to least preferred. michael@0: # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly, michael@0: # but it will still be present if users update an older nightly build michael@0: # only via the app update service. michael@0: bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly'] michael@0: michael@0: @property michael@0: def names(self): michael@0: if sys.platform == 'darwin': michael@0: return ['firefox', 'nightly', 'shiretoko'] michael@0: if sys.platform in ('linux2', 'sunos5', 'solaris') \ michael@0: or sys.platform.startswith('freebsd'): michael@0: return ['firefox', 'mozilla-firefox', 'iceweasel'] michael@0: if os.name == 'nt' or sys.platform == 'cygwin': michael@0: return ['firefox'] michael@0: michael@0: class ThunderbirdRunner(Runner): michael@0: """Specialized Runner subclass for running Thunderbird""" michael@0: michael@0: app_name = 'Thunderbird' michael@0: profile_class = ThunderbirdProfile michael@0: michael@0: # The possible names of application bundles on Mac OS X, in order of michael@0: # preference from most to least preferred. michael@0: bundle_names = ["Thunderbird", "Shredder"] michael@0: michael@0: # The possible names of binaries, in order of preference from most to least michael@0: # preferred. michael@0: names = ["thunderbird", "shredder"] michael@0: michael@0: class CLI(object): michael@0: """Command line interface.""" michael@0: michael@0: runner_class = FirefoxRunner michael@0: profile_class = FirefoxProfile michael@0: module = "mozrunner" michael@0: michael@0: parser_options = {("-b", "--binary",): dict(dest="binary", help="Binary path.", michael@0: metavar=None, default=None), michael@0: ('-p', "--profile",): dict(dest="profile", help="Profile path.", michael@0: metavar=None, default=None), michael@0: ('-a', "--addons",): dict(dest="addons", michael@0: help="Addons paths to install.", michael@0: metavar=None, default=None), michael@0: ("--info",): dict(dest="info", default=False, michael@0: action="store_true", michael@0: help="Print module information") michael@0: } michael@0: michael@0: def __init__(self): michael@0: """ Setup command line parser and parse arguments """ michael@0: self.metadata = self.get_metadata_from_egg() michael@0: self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"]) michael@0: for names, opts in self.parser_options.items(): michael@0: self.parser.add_option(*names, **opts) michael@0: (self.options, self.args) = self.parser.parse_args() michael@0: michael@0: if self.options.info: michael@0: self.print_metadata() michael@0: sys.exit(0) michael@0: michael@0: # XXX should use action='append' instead of rolling our own michael@0: try: michael@0: self.addons = self.options.addons.split(',') michael@0: except: michael@0: self.addons = [] michael@0: michael@0: def get_metadata_from_egg(self): michael@0: import pkg_resources michael@0: ret = {} michael@0: dist = pkg_resources.get_distribution(self.module) michael@0: if dist.has_metadata("PKG-INFO"): michael@0: for line in dist.get_metadata_lines("PKG-INFO"): michael@0: key, value = line.split(':', 1) michael@0: ret[key] = value michael@0: if dist.has_metadata("requires.txt"): michael@0: ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") michael@0: return ret michael@0: michael@0: def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", michael@0: "Author", "Author-email", "License", "Platform", "Dependencies")): michael@0: for key in data: michael@0: if key in self.metadata: michael@0: print key + ": " + self.metadata[key] michael@0: michael@0: def create_runner(self): michael@0: """ Get the runner object """ michael@0: runner = self.get_runner(binary=self.options.binary) michael@0: profile = self.get_profile(binary=runner.binary, michael@0: profile=self.options.profile, michael@0: addons=self.addons) michael@0: runner.profile = profile michael@0: return runner michael@0: michael@0: def get_runner(self, binary=None, profile=None): michael@0: """Returns the runner instance for the given command line binary argument michael@0: the profile instance returned from self.get_profile().""" michael@0: return self.runner_class(binary, profile) michael@0: michael@0: def get_profile(self, binary=None, profile=None, addons=None, preferences=None): michael@0: """Returns the profile instance for the given command line arguments.""" michael@0: addons = addons or [] michael@0: preferences = preferences or {} michael@0: return self.profile_class(binary, profile, addons, preferences) michael@0: michael@0: def run(self): michael@0: runner = self.create_runner() michael@0: self.start(runner) michael@0: runner.profile.cleanup() michael@0: michael@0: def start(self, runner): michael@0: """Starts the runner and waits for Firefox to exitor Keyboard Interrupt. michael@0: Shoule be overwritten to provide custom running of the runner instance.""" michael@0: runner.start() michael@0: print 'Started:', ' '.join(runner.command) michael@0: try: michael@0: runner.wait() michael@0: except KeyboardInterrupt: michael@0: runner.stop() michael@0: michael@0: michael@0: def cli(): michael@0: CLI().run()