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

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

mercurial