# ***** 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 # Clint Talbert # Henrik Skupin # # 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 import shutil from distutils import dir_util from time import sleep # conditional (version-dependent) imports try: from xml.etree import ElementTree except ImportError: from elementtree import ElementTree 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 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 install_addon(self, path): """Installs the given addon or directory of addons in the profile.""" 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: tmpdir = None 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 tree = ElementTree.ElementTree(file=os.path.join(addon, 'install.rdf')) # description_element = # tree.find('.//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description/') desc = tree.find('.//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description') apps = desc.findall('.//{http://www.mozilla.org/2004/em-rdf#}targetApplication') for app in apps: desc.remove(app) if len(desc) and desc.attrib.has_key('{http://www.mozilla.org/2004/em-rdf#}id'): addon_id = desc.attrib['{http://www.mozilla.org/2004/em-rdf#}id'] elif len(desc) and desc.find('.//{http://www.mozilla.org/2004/em-rdf#}id') is not None: addon_id = desc.find('.//{http://www.mozilla.org/2004/em-rdf#}id').text else: about = [e for e in tree.findall( './/{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description') if e.get('{http://www.w3.org/1999/02/22-rdf-syntax-ns#}about') == 'urn:mozilla:install-manifest' ] x = e.find('.//{http://www.w3.org/1999/02/22-rdf-syntax-ns#}Description') if len(about) == 0: addon_element = tree.find('.//{http://www.mozilla.org/2004/em-rdf#}id') addon_id = addon_element.text else: addon_id = about[0].get('{http://www.mozilla.org/2004/em-rdf#}id') addon_path = os.path.join(self.profile, 'extensions', addon_id) copytree(addon, addon_path, preserve_symlinks=1) self.addons_installed.append(addon_path) 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() 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, } @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, } 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'): 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,'Nightly', 'firefox.exe'), (os.environ.get("ProgramFiles(x86)"),'Nightly', '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) # 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 @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 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()