123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694 |
- # This Source Code Form is subject to the terms of the Mozilla Public
- # License, v. 2.0. If a copy of the MPL was not distributed with this
- # file, You can obtain one at http://mozilla.org/MPL/2.0/.
- import os
- import sys
- import copy
- import tempfile
- import signal
- import commands
- import zipfile
- import optparse
- import killableprocess
- import subprocess
- import platform
- import shutil
- from StringIO import StringIO
- from xml.dom import minidom
- from distutils import dir_util
- from time import sleep
- # 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:
- 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 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
- # addon_details() copied from mozprofile
- def addon_details(install_rdf_fh):
- """
- returns a dictionary of details about the addon
- - addon_path : path to the addon directory
- Returns:
- {'id': u'rainbow@colors.org', # id of the addon
- 'version': u'1.4', # version of the addon
- 'name': u'Rainbow', # name of the addon
- 'unpack': # whether to unpack the addon
- """
- details = {
- 'id': None,
- 'unpack': False,
- 'name': None,
- 'version': None
- }
- def get_namespace_id(doc, url):
- attributes = doc.documentElement.attributes
- namespace = ""
- for i in range(attributes.length):
- if attributes.item(i).value == url:
- if ":" in attributes.item(i).name:
- # If the namespace is not the default one remove 'xlmns:'
- namespace = attributes.item(i).name.split(':')[1] + ":"
- break
- return namespace
- def get_text(element):
- """Retrieve the text value of a given node"""
- rc = []
- for node in element.childNodes:
- if node.nodeType == node.TEXT_NODE:
- rc.append(node.data)
- return ''.join(rc).strip()
- doc = minidom.parse(install_rdf_fh)
- # Get the namespaces abbreviations
- em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
- rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
- description = doc.getElementsByTagName(rdf + "Description").item(0)
- for node in description.childNodes:
- # Remove the namespace prefix from the tag for comparison
- entry = node.nodeName.replace(em, "")
- if entry in details.keys():
- details.update({ entry: get_text(node) })
- # turn unpack into a true/false value
- if isinstance(details['unpack'], basestring):
- details['unpack'] = details['unpack'].lower() == 'true'
- return details
- 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
- def unpack_addon(self, xpi_zipfile, addon_path):
- for name in xpi_zipfile.namelist():
- if name.endswith('/'):
- makedirs(os.path.join(addon_path, name))
- else:
- if not os.path.isdir(os.path.dirname(os.path.join(addon_path, name))):
- makedirs(os.path.dirname(os.path.join(addon_path, name)))
- data = xpi_zipfile.read(name)
- f = open(os.path.join(addon_path, name), 'wb')
- f.write(data) ; f.close()
- zi = xpi_zipfile.getinfo(name)
- os.chmod(os.path.join(addon_path,name), (zi.external_attr>>16))
- def install_addon(self, path):
- """Installs the given addon or directory of addons in the profile."""
- extensions_path = os.path.join(self.profile, 'extensions')
- if not os.path.exists(extensions_path):
- os.makedirs(extensions_path)
- 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)]
- for addon in addons:
- if addon.endswith('.xpi'):
- xpi_zipfile = zipfile.ZipFile(addon, "r")
- details = addon_details(StringIO(xpi_zipfile.read('install.rdf')))
- addon_path = os.path.join(extensions_path, details["id"])
- if details.get("unpack", True):
- self.unpack_addon(xpi_zipfile, addon_path)
- self.addons_installed.append(addon_path)
- else:
- shutil.copy(addon, addon_path + '.xpi')
- else:
- # it's already unpacked, but we need to extract the id so we
- # can copy it
- details = addon_details(open(os.path.join(addon, "install.rdf"), "rb"))
- addon_path = os.path.join(extensions_path, details["id"])
- shutil.copytree(addon, addon_path, symlinks=True)
- 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 pop_preferences(self):
- """
- pop the last set of preferences added
- returns True if popped
- """
- # our magic markers
- delimeters = ('#MozRunner Prefs Start', '#MozRunner Prefs End')
- lines = file(os.path.join(self.profile, 'user.js')).read().splitlines()
- def last_index(_list, value):
- """
- returns the last index of an item;
- this should actually be part of python code but it isn't
- """
- for index in reversed(range(len(_list))):
- if _list[index] == value:
- return index
- s = last_index(lines, delimeters[0])
- e = last_index(lines, delimeters[1])
- # ensure both markers are found
- if s is None:
- assert e is None, '%s found without %s' % (delimeters[1], delimeters[0])
- return False # no preferences found
- elif e is None:
- assert e is None, '%s found without %s' % (delimeters[0], delimeters[1])
- # ensure the markers are in the proper order
- assert e > s, '%s found at %s, while %s found at %s' (delimeter[1], e, delimeter[0], s)
- # write the prefs
- cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
- f = file(os.path.join(self.profile, 'user.js'), 'w')
- f.write(cleaned_prefs)
- f.close()
- return True
- def clean_preferences(self):
- """Removed preferences added by mozrunner."""
- while True:
- if not self.pop_preferences():
- break
- def clean_addons(self):
- """Cleans up addons in the profile."""
- for addon in self.addons_installed:
- if os.path.isdir(addon):
- rmtree(addon)
- def cleanup(self):
- """Cleanup operations on the profile."""
- def oncleanup_error(function, path, excinfo):
- #TODO: How should we handle this?
- print "Error Cleaning up: " + str(excinfo[1])
- if self.create_new:
- shutil.rmtree(self.profile, False, oncleanup_error)
- 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,
- # 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,
- }
- # The possible names of application bundles on Mac OS X, in order of
- # preference from most to least preferred.
- # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly,
- # but it will still be present if users update an older nightly build
- # via the app update service.
- bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
- # The possible names of binaries, in order of preference from most to least
- # preferred.
- @property
- def names(self):
- if sys.platform == 'darwin':
- return ['firefox', 'nightly', '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,
- }
- # The possible names of application bundles on Mac OS X, in order of
- # preference from most to least preferred.
- bundle_names = ["Thunderbird", "Shredder"]
- # The possible names of binaries, in order of preference from most to least
- # preferred.
- 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,
- 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
- # Disable the crash reporter by default
- os.environ['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
- 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.kp_kwargs = kp_kwargs or {}
- def find_binary(self):
- """Finds the binary for self.names if one was not provided."""
- binary = None
- if sys.platform in ('linux2', 'sunos5', 'solaris') \
- or sys.platform.startswith('freebsd'):
- 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:
- import _winreg
- except ImportError:
- pass
- else:
- sam_flags = [0]
- # KEY_WOW64_32KEY etc only appeared in 2.6+, but that's OK as
- # only 2.6+ has functioning 64bit builds.
- if hasattr(_winreg, "KEY_WOW64_32KEY"):
- if "64 bit" in sys.version:
- # a 64bit Python should also look in the 32bit registry
- sam_flags.append(_winreg.KEY_WOW64_32KEY)
- else:
- # possibly a 32bit Python on 64bit Windows, so look in
- # the 64bit registry incase there is a 64bit app.
- sam_flags.append(_winreg.KEY_WOW64_64KEY)
- for sam_flag in sam_flags:
- try:
- # assumes self.app_name is defined, as it should be for
- # implementors
- keyname = r"Software\Mozilla\Mozilla %s" % self.app_name
- sam = _winreg.KEY_READ | sam_flag
- app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, keyname, 0, sam)
- 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 _winreg.error:
- 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, 'Nightly', 'firefox.exe'),
- (os.environ.get("ProgramFiles(x86)"),'Nightly', 'firefox.exe'),
- (program_files, 'Aurora', 'firefox.exe'),
- (os.environ.get("ProgramFiles(x86)"),'Aurora', 'firefox.exe')
- ]:
- path = os.path.join(*bin)
- if os.path.isfile(path):
- binary = path
- break
- elif sys.platform == 'darwin':
- for bundle_name in self.bundle_names:
- # Look for the application bundle in the user's home directory
- # or the system-wide /Applications directory. If we don't find
- # it in one of those locations, we move on to the next possible
- # bundle name.
- appdir = os.path.join("~/Applications/%s.app" % bundle_name)
- if not os.path.isdir(appdir):
- appdir = "/Applications/%s.app" % bundle_name
- if not os.path.isdir(appdir):
- continue
- # Look for a binary with any of the possible binary names
- # inside the application bundle.
- for binname in self.names:
- binpath = os.path.join(appdir,
- "Contents/MacOS/%s-bin" % binname)
- if (os.path.isfile(binpath)):
- binary = binpath
- break
- if binary:
- break
- 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)
- # On windows, it sometimes behooves one to wait for dust to settle
- # after killing processes. Let's try that.
- # TODO: Bug 640047 is invesitgating the correct way to handle this case
- self.process_handler.wait(timeout=10)
- except Exception, e:
- logger.error('Cannot kill process, '+type(e).__name__+' '+e.message)
- def stop(self):
- self.kill()
- class FirefoxRunner(Runner):
- """Specialized Runner subclass for running Firefox."""
- app_name = 'Firefox'
- profile_class = FirefoxProfile
- # The possible names of application bundles on Mac OS X, in order of
- # preference from most to least preferred.
- # Note: Nightly is obsolete, as it has been renamed to FirefoxNightly,
- # but it will still be present if users update an older nightly build
- # only via the app update service.
- bundle_names = ['Firefox', 'FirefoxNightly', 'Nightly']
- @property
- def names(self):
- if sys.platform == 'darwin':
- return ['firefox', 'nightly', 'shiretoko']
- if sys.platform in ('linux2', 'sunos5', 'solaris') \
- or sys.platform.startswith('freebsd'):
- 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
- # The possible names of application bundles on Mac OS X, in order of
- # preference from most to least preferred.
- bundle_names = ["Thunderbird", "Shredder"]
- # The possible names of binaries, in order of preference from most to least
- # preferred.
- 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()
|