summaryrefslogtreecommitdiffstats
path: root/addon-sdk/source/python-lib/mozrunner
diff options
context:
space:
mode:
Diffstat (limited to 'addon-sdk/source/python-lib/mozrunner')
-rw-r--r--addon-sdk/source/python-lib/mozrunner/__init__.py694
-rw-r--r--addon-sdk/source/python-lib/mozrunner/killableprocess.py329
-rw-r--r--addon-sdk/source/python-lib/mozrunner/qijo.py166
-rw-r--r--addon-sdk/source/python-lib/mozrunner/winprocess.py379
-rw-r--r--addon-sdk/source/python-lib/mozrunner/wpk.py80
5 files changed, 1648 insertions, 0 deletions
diff --git a/addon-sdk/source/python-lib/mozrunner/__init__.py b/addon-sdk/source/python-lib/mozrunner/__init__.py
new file mode 100644
index 000000000..87c2c320f
--- /dev/null
+++ b/addon-sdk/source/python-lib/mozrunner/__init__.py
@@ -0,0 +1,694 @@
+# 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()
diff --git a/addon-sdk/source/python-lib/mozrunner/killableprocess.py b/addon-sdk/source/python-lib/mozrunner/killableprocess.py
new file mode 100644
index 000000000..daf52f0c9
--- /dev/null
+++ b/addon-sdk/source/python-lib/mozrunner/killableprocess.py
@@ -0,0 +1,329 @@
+# killableprocess - subprocesses which can be reliably killed
+#
+# Parts of this module are copied from the subprocess.py file contained
+# in the Python distribution.
+#
+# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se>
+#
+# Additions and modifications written by Benjamin Smedberg
+# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
+# <http://www.mozilla.org/>
+#
+# More Modifications
+# Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com>
+# Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com>
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of the
+# author not be used in advertising or publicity pertaining to
+# distribution of the software without specific, written prior
+# permission.
+#
+# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+"""killableprocess - Subprocesses which can be reliably killed
+
+This module is a subclass of the builtin "subprocess" module. It allows
+processes that launch subprocesses to be reliably killed on Windows (via the Popen.kill() method.
+
+It also adds a timeout argument to Wait() for a limited period of time before
+forcefully killing the process.
+
+Note: On Windows, this module requires Windows 2000 or higher (no support for
+Windows 95, 98, or NT 4.0). It also requires ctypes, which is bundled with
+Python 2.5+ or available from http://python.net/crew/theller/ctypes/
+"""
+
+import subprocess
+import sys
+import os
+import time
+import datetime
+import types
+import exceptions
+
+try:
+ from subprocess import CalledProcessError
+except ImportError:
+ # Python 2.4 doesn't implement CalledProcessError
+ class CalledProcessError(Exception):
+ """This exception is raised when a process run by check_call() returns
+ a non-zero exit status. The exit status will be stored in the
+ returncode attribute."""
+ def __init__(self, returncode, cmd):
+ self.returncode = returncode
+ self.cmd = cmd
+ def __str__(self):
+ return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
+
+mswindows = (sys.platform == "win32")
+
+if mswindows:
+ import winprocess
+else:
+ import signal
+
+# This is normally defined in win32con, but we don't want
+# to incur the huge tree of dependencies (pywin32 and friends)
+# just to get one constant. So here's our hack
+STILL_ACTIVE = 259
+
+def call(*args, **kwargs):
+ waitargs = {}
+ if "timeout" in kwargs:
+ waitargs["timeout"] = kwargs.pop("timeout")
+
+ return Popen(*args, **kwargs).wait(**waitargs)
+
+def check_call(*args, **kwargs):
+ """Call a program with an optional timeout. If the program has a non-zero
+ exit status, raises a CalledProcessError."""
+
+ retcode = call(*args, **kwargs)
+ if retcode:
+ cmd = kwargs.get("args")
+ if cmd is None:
+ cmd = args[0]
+ raise CalledProcessError(retcode, cmd)
+
+if not mswindows:
+ def DoNothing(*args):
+ pass
+
+class Popen(subprocess.Popen):
+ kill_called = False
+ if mswindows:
+ def _execute_child(self, *args_tuple):
+ # workaround for bug 958609
+ if sys.hexversion < 0x02070600: # prior to 2.7.6
+ (args, executable, preexec_fn, close_fds,
+ cwd, env, universal_newlines, startupinfo,
+ creationflags, shell,
+ p2cread, p2cwrite,
+ c2pread, c2pwrite,
+ errread, errwrite) = args_tuple
+ to_close = set()
+ else: # 2.7.6 and later
+ (args, executable, preexec_fn, close_fds,
+ cwd, env, universal_newlines, startupinfo,
+ creationflags, shell, to_close,
+ p2cread, p2cwrite,
+ c2pread, c2pwrite,
+ errread, errwrite) = args_tuple
+
+ if not isinstance(args, types.StringTypes):
+ args = subprocess.list2cmdline(args)
+
+ # Always or in the create new process group
+ creationflags |= winprocess.CREATE_NEW_PROCESS_GROUP
+
+ if startupinfo is None:
+ startupinfo = winprocess.STARTUPINFO()
+
+ if None not in (p2cread, c2pwrite, errwrite):
+ startupinfo.dwFlags |= winprocess.STARTF_USESTDHANDLES
+
+ startupinfo.hStdInput = int(p2cread)
+ startupinfo.hStdOutput = int(c2pwrite)
+ startupinfo.hStdError = int(errwrite)
+ if shell:
+ startupinfo.dwFlags |= winprocess.STARTF_USESHOWWINDOW
+ startupinfo.wShowWindow = winprocess.SW_HIDE
+ comspec = os.environ.get("COMSPEC", "cmd.exe")
+ args = comspec + " /c " + args
+
+ # determine if we can create create a job
+ canCreateJob = winprocess.CanCreateJobObject()
+
+ # set process creation flags
+ creationflags |= winprocess.CREATE_SUSPENDED
+ creationflags |= winprocess.CREATE_UNICODE_ENVIRONMENT
+ if canCreateJob:
+ # Uncomment this line below to discover very useful things about your environment
+ #print "++++ killableprocess: releng twistd patch not applied, we can create job objects"
+ creationflags |= winprocess.CREATE_BREAKAWAY_FROM_JOB
+
+ # create the process
+ hp, ht, pid, tid = winprocess.CreateProcess(
+ executable, args,
+ None, None, # No special security
+ 1, # Must inherit handles!
+ creationflags,
+ winprocess.EnvironmentBlock(env),
+ cwd, startupinfo)
+ self._child_created = True
+ self._handle = hp
+ self._thread = ht
+ self.pid = pid
+ self.tid = tid
+
+ if canCreateJob:
+ # We create a new job for this process, so that we can kill
+ # the process and any sub-processes
+ self._job = winprocess.CreateJobObject()
+ winprocess.AssignProcessToJobObject(self._job, int(hp))
+ else:
+ self._job = None
+
+ winprocess.ResumeThread(int(ht))
+ ht.Close()
+
+ if p2cread is not None:
+ p2cread.Close()
+ if c2pwrite is not None:
+ c2pwrite.Close()
+ if errwrite is not None:
+ errwrite.Close()
+ time.sleep(.1)
+
+ def kill(self, group=True):
+ """Kill the process. If group=True, all sub-processes will also be killed."""
+ self.kill_called = True
+
+ if mswindows:
+ if group and self._job:
+ winprocess.TerminateJobObject(self._job, 127)
+ else:
+ winprocess.TerminateProcess(self._handle, 127)
+ self.returncode = 127
+ else:
+ if group:
+ try:
+ os.killpg(self.pid, signal.SIGKILL)
+ except: pass
+ else:
+ os.kill(self.pid, signal.SIGKILL)
+ self.returncode = -9
+
+ def wait(self, timeout=None, group=True):
+ """Wait for the process to terminate. Returns returncode attribute.
+ If timeout seconds are reached and the process has not terminated,
+ it will be forcefully killed. If timeout is -1, wait will not
+ time out."""
+ if timeout is not None:
+ # timeout is now in milliseconds
+ timeout = timeout * 1000
+
+ starttime = datetime.datetime.now()
+
+ if mswindows:
+ if timeout is None:
+ timeout = -1
+ rc = winprocess.WaitForSingleObject(self._handle, timeout)
+
+ if (rc == winprocess.WAIT_OBJECT_0 or
+ rc == winprocess.WAIT_ABANDONED or
+ rc == winprocess.WAIT_FAILED):
+ # Object has either signaled, or the API call has failed. In
+ # both cases we want to give the OS the benefit of the doubt
+ # and supply a little time before we start shooting processes
+ # with an M-16.
+
+ # Returns 1 if running, 0 if not, -1 if timed out
+ def check():
+ now = datetime.datetime.now()
+ diff = now - starttime
+ if (diff.seconds * 1000000 + diff.microseconds) < (timeout * 1000): # (1000*1000)
+ if self._job:
+ if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0):
+ # Job Object is still containing active processes
+ return 1
+ else:
+ # No job, we use GetExitCodeProcess, which will tell us if the process is still active
+ self.returncode = winprocess.GetExitCodeProcess(self._handle)
+ if (self.returncode == STILL_ACTIVE):
+ # Process still active, continue waiting
+ return 1
+ # Process not active, return 0
+ return 0
+ else:
+ # Timed out, return -1
+ return -1
+
+ notdone = check()
+ while notdone == 1:
+ time.sleep(.5)
+ notdone = check()
+
+ if notdone == -1:
+ # Then check timed out, we have a hung process, attempt
+ # last ditch kill with explosives
+ self.kill(group)
+
+ else:
+ # In this case waitforsingleobject timed out. We have to
+ # take the process behind the woodshed and shoot it.
+ self.kill(group)
+
+ else:
+ if sys.platform in ('linux2', 'sunos5', 'solaris') \
+ or sys.platform.startswith('freebsd'):
+ def group_wait(timeout):
+ try:
+ os.waitpid(self.pid, 0)
+ except OSError, e:
+ pass # If wait has already been called on this pid, bad things happen
+ return self.returncode
+ elif sys.platform == 'darwin':
+ def group_wait(timeout):
+ try:
+ count = 0
+ if timeout is None and self.kill_called:
+ timeout = 10 # Have to set some kind of timeout or else this could go on forever
+ if timeout is None:
+ while 1:
+ os.killpg(self.pid, signal.SIG_DFL)
+ while ((count * 2) <= timeout):
+ os.killpg(self.pid, signal.SIG_DFL)
+ # count is increased by 500ms for every 0.5s of sleep
+ time.sleep(.5); count += 500
+ except exceptions.OSError:
+ return self.returncode
+
+ if timeout is None:
+ if group is True:
+ return group_wait(timeout)
+ else:
+ subprocess.Popen.wait(self)
+ return self.returncode
+
+ returncode = False
+
+ now = datetime.datetime.now()
+ diff = now - starttime
+ while (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000) and ( returncode is False ):
+ if group is True:
+ return group_wait(timeout)
+ else:
+ if subprocess.poll() is not None:
+ returncode = self.returncode
+ time.sleep(.5)
+ now = datetime.datetime.now()
+ diff = now - starttime
+ return self.returncode
+
+ return self.returncode
+ # We get random maxint errors from subprocesses __del__
+ __del__ = lambda self: None
+
+def setpgid_preexec_fn():
+ os.setpgid(0, 0)
+
+def runCommand(cmd, **kwargs):
+ if sys.platform != "win32":
+ return Popen(cmd, preexec_fn=setpgid_preexec_fn, **kwargs)
+ else:
+ return Popen(cmd, **kwargs)
diff --git a/addon-sdk/source/python-lib/mozrunner/qijo.py b/addon-sdk/source/python-lib/mozrunner/qijo.py
new file mode 100644
index 000000000..058055731
--- /dev/null
+++ b/addon-sdk/source/python-lib/mozrunner/qijo.py
@@ -0,0 +1,166 @@
+# 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/.
+
+from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE, addressof, c_size_t, c_ulong
+from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LARGE_INTEGER
+
+LPVOID = c_void_p
+LPDWORD = POINTER(DWORD)
+SIZE_T = c_size_t
+ULONG_PTR = POINTER(c_ulong)
+
+# A ULONGLONG is a 64-bit unsigned integer.
+# Thus there are 8 bytes in a ULONGLONG.
+# XXX why not import c_ulonglong ?
+ULONGLONG = BYTE * 8
+
+class IO_COUNTERS(Structure):
+ # The IO_COUNTERS struct is 6 ULONGLONGs.
+ # TODO: Replace with non-dummy fields.
+ _fields_ = [('dummy', ULONGLONG * 6)]
+
+class JOBOBJECT_BASIC_ACCOUNTING_INFORMATION(Structure):
+ _fields_ = [('TotalUserTime', LARGE_INTEGER),
+ ('TotalKernelTime', LARGE_INTEGER),
+ ('ThisPeriodTotalUserTime', LARGE_INTEGER),
+ ('ThisPeriodTotalKernelTime', LARGE_INTEGER),
+ ('TotalPageFaultCount', DWORD),
+ ('TotalProcesses', DWORD),
+ ('ActiveProcesses', DWORD),
+ ('TotalTerminatedProcesses', DWORD)]
+
+class JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION(Structure):
+ _fields_ = [('BasicInfo', JOBOBJECT_BASIC_ACCOUNTING_INFORMATION),
+ ('IoInfo', IO_COUNTERS)]
+
+# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx
+class JOBOBJECT_BASIC_LIMIT_INFORMATION(Structure):
+ _fields_ = [('PerProcessUserTimeLimit', LARGE_INTEGER),
+ ('PerJobUserTimeLimit', LARGE_INTEGER),
+ ('LimitFlags', DWORD),
+ ('MinimumWorkingSetSize', SIZE_T),
+ ('MaximumWorkingSetSize', SIZE_T),
+ ('ActiveProcessLimit', DWORD),
+ ('Affinity', ULONG_PTR),
+ ('PriorityClass', DWORD),
+ ('SchedulingClass', DWORD)
+ ]
+
+# see http://msdn.microsoft.com/en-us/library/ms684156%28VS.85%29.aspx
+class JOBOBJECT_EXTENDED_LIMIT_INFORMATION(Structure):
+ _fields_ = [('BasicLimitInformation', JOBOBJECT_BASIC_LIMIT_INFORMATION),
+ ('IoInfo', IO_COUNTERS),
+ ('ProcessMemoryLimit', SIZE_T),
+ ('JobMemoryLimit', SIZE_T),
+ ('PeakProcessMemoryUsed', SIZE_T),
+ ('PeakJobMemoryUsed', SIZE_T)]
+
+# XXX Magical numbers like 8 should be documented
+JobObjectBasicAndIoAccountingInformation = 8
+
+# ...like magical number 9 comes from
+# http://community.flexerasoftware.com/archive/index.php?t-181670.html
+# I wish I had a more canonical source
+JobObjectExtendedLimitInformation = 9
+
+class JobObjectInfo(object):
+ mapping = { 'JobObjectBasicAndIoAccountingInformation': 8,
+ 'JobObjectExtendedLimitInformation': 9
+ }
+ structures = { 8: JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION,
+ 9: JOBOBJECT_EXTENDED_LIMIT_INFORMATION
+ }
+ def __init__(self, _class):
+ if isinstance(_class, basestring):
+ assert _class in self.mapping, 'Class should be one of %s; you gave %s' % (self.mapping, _class)
+ _class = self.mapping[_class]
+ assert _class in self.structures, 'Class should be one of %s; you gave %s' % (self.structures, _class)
+ self.code = _class
+ self.info = self.structures[_class]()
+
+
+QueryInformationJobObjectProto = WINFUNCTYPE(
+ BOOL, # Return type
+ HANDLE, # hJob
+ DWORD, # JobObjectInfoClass
+ LPVOID, # lpJobObjectInfo
+ DWORD, # cbJobObjectInfoLength
+ LPDWORD # lpReturnLength
+ )
+
+QueryInformationJobObjectFlags = (
+ (1, 'hJob'),
+ (1, 'JobObjectInfoClass'),
+ (1, 'lpJobObjectInfo'),
+ (1, 'cbJobObjectInfoLength'),
+ (1, 'lpReturnLength', None)
+ )
+
+_QueryInformationJobObject = QueryInformationJobObjectProto(
+ ('QueryInformationJobObject', windll.kernel32),
+ QueryInformationJobObjectFlags
+ )
+
+class SubscriptableReadOnlyStruct(object):
+ def __init__(self, struct):
+ self._struct = struct
+
+ def _delegate(self, name):
+ result = getattr(self._struct, name)
+ if isinstance(result, Structure):
+ return SubscriptableReadOnlyStruct(result)
+ return result
+
+ def __getitem__(self, name):
+ match = [fname for fname, ftype in self._struct._fields_
+ if fname == name]
+ if match:
+ return self._delegate(name)
+ raise KeyError(name)
+
+ def __getattr__(self, name):
+ return self._delegate(name)
+
+def QueryInformationJobObject(hJob, JobObjectInfoClass):
+ jobinfo = JobObjectInfo(JobObjectInfoClass)
+ result = _QueryInformationJobObject(
+ hJob=hJob,
+ JobObjectInfoClass=jobinfo.code,
+ lpJobObjectInfo=addressof(jobinfo.info),
+ cbJobObjectInfoLength=sizeof(jobinfo.info)
+ )
+ if not result:
+ raise WinError()
+ return SubscriptableReadOnlyStruct(jobinfo.info)
+
+def test_qijo():
+ from killableprocess import Popen
+
+ popen = Popen('c:\\windows\\notepad.exe')
+
+ try:
+ result = QueryInformationJobObject(0, 8)
+ raise AssertionError('throw should occur')
+ except WindowsError, e:
+ pass
+
+ try:
+ result = QueryInformationJobObject(0, 1)
+ raise AssertionError('throw should occur')
+ except NotImplementedError, e:
+ pass
+
+ result = QueryInformationJobObject(popen._job, 8)
+ if result['BasicInfo']['ActiveProcesses'] != 1:
+ raise AssertionError('expected ActiveProcesses to be 1')
+ popen.kill()
+
+ result = QueryInformationJobObject(popen._job, 8)
+ if result.BasicInfo.ActiveProcesses != 0:
+ raise AssertionError('expected ActiveProcesses to be 0')
+
+if __name__ == '__main__':
+ print "testing."
+ test_qijo()
+ print "success!"
diff --git a/addon-sdk/source/python-lib/mozrunner/winprocess.py b/addon-sdk/source/python-lib/mozrunner/winprocess.py
new file mode 100644
index 000000000..16666b0eb
--- /dev/null
+++ b/addon-sdk/source/python-lib/mozrunner/winprocess.py
@@ -0,0 +1,379 @@
+# A module to expose various thread/process/job related structures and
+# methods from kernel32
+#
+# The MIT License
+#
+# Copyright (c) 2003-2004 by Peter Astrand <astrand@lysator.liu.se>
+#
+# Additions and modifications written by Benjamin Smedberg
+# <benjamin@smedbergs.us> are Copyright (c) 2006 by the Mozilla Foundation
+# <http://www.mozilla.org/>
+#
+# More Modifications
+# Copyright (c) 2006-2007 by Mike Taylor <bear@code-bear.com>
+# Copyright (c) 2007-2008 by Mikeal Rogers <mikeal@mozilla.com>
+#
+# By obtaining, using, and/or copying this software and/or its
+# associated documentation, you agree that you have read, understood,
+# and will comply with the following terms and conditions:
+#
+# Permission to use, copy, modify, and distribute this software and
+# its associated documentation for any purpose and without fee is
+# hereby granted, provided that the above copyright notice appears in
+# all copies, and that both that copyright notice and this permission
+# notice appear in supporting documentation, and that the name of the
+# author not be used in advertising or publicity pertaining to
+# distribution of the software without specific, written prior
+# permission.
+#
+# THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
+# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS.
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR
+# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
+# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+from ctypes import c_void_p, POINTER, sizeof, Structure, windll, WinError, WINFUNCTYPE
+from ctypes.wintypes import BOOL, BYTE, DWORD, HANDLE, LPCWSTR, LPWSTR, UINT, WORD, \
+ c_buffer, c_ulong, byref
+from qijo import QueryInformationJobObject
+
+LPVOID = c_void_p
+LPBYTE = POINTER(BYTE)
+LPDWORD = POINTER(DWORD)
+LPBOOL = POINTER(BOOL)
+
+def ErrCheckBool(result, func, args):
+ """errcheck function for Windows functions that return a BOOL True
+ on success"""
+ if not result:
+ raise WinError()
+ return args
+
+
+# AutoHANDLE
+
+class AutoHANDLE(HANDLE):
+ """Subclass of HANDLE which will call CloseHandle() on deletion."""
+
+ CloseHandleProto = WINFUNCTYPE(BOOL, HANDLE)
+ CloseHandle = CloseHandleProto(("CloseHandle", windll.kernel32))
+ CloseHandle.errcheck = ErrCheckBool
+
+ def Close(self):
+ if self.value and self.value != HANDLE(-1).value:
+ self.CloseHandle(self)
+ self.value = 0
+
+ def __del__(self):
+ self.Close()
+
+ def __int__(self):
+ return self.value
+
+def ErrCheckHandle(result, func, args):
+ """errcheck function for Windows functions that return a HANDLE."""
+ if not result:
+ raise WinError()
+ return AutoHANDLE(result)
+
+# PROCESS_INFORMATION structure
+
+class PROCESS_INFORMATION(Structure):
+ _fields_ = [("hProcess", HANDLE),
+ ("hThread", HANDLE),
+ ("dwProcessID", DWORD),
+ ("dwThreadID", DWORD)]
+
+ def __init__(self):
+ Structure.__init__(self)
+
+ self.cb = sizeof(self)
+
+LPPROCESS_INFORMATION = POINTER(PROCESS_INFORMATION)
+
+# STARTUPINFO structure
+
+class STARTUPINFO(Structure):
+ _fields_ = [("cb", DWORD),
+ ("lpReserved", LPWSTR),
+ ("lpDesktop", LPWSTR),
+ ("lpTitle", LPWSTR),
+ ("dwX", DWORD),
+ ("dwY", DWORD),
+ ("dwXSize", DWORD),
+ ("dwYSize", DWORD),
+ ("dwXCountChars", DWORD),
+ ("dwYCountChars", DWORD),
+ ("dwFillAttribute", DWORD),
+ ("dwFlags", DWORD),
+ ("wShowWindow", WORD),
+ ("cbReserved2", WORD),
+ ("lpReserved2", LPBYTE),
+ ("hStdInput", HANDLE),
+ ("hStdOutput", HANDLE),
+ ("hStdError", HANDLE)
+ ]
+LPSTARTUPINFO = POINTER(STARTUPINFO)
+
+SW_HIDE = 0
+
+STARTF_USESHOWWINDOW = 0x01
+STARTF_USESIZE = 0x02
+STARTF_USEPOSITION = 0x04
+STARTF_USECOUNTCHARS = 0x08
+STARTF_USEFILLATTRIBUTE = 0x10
+STARTF_RUNFULLSCREEN = 0x20
+STARTF_FORCEONFEEDBACK = 0x40
+STARTF_FORCEOFFFEEDBACK = 0x80
+STARTF_USESTDHANDLES = 0x100
+
+# EnvironmentBlock
+
+class EnvironmentBlock:
+ """An object which can be passed as the lpEnv parameter of CreateProcess.
+ It is initialized with a dictionary."""
+
+ def __init__(self, dict):
+ if not dict:
+ self._as_parameter_ = None
+ else:
+ values = ["%s=%s" % (key, value)
+ for (key, value) in dict.iteritems()]
+ values.append("")
+ self._as_parameter_ = LPCWSTR("\0".join(values))
+
+# CreateProcess()
+
+CreateProcessProto = WINFUNCTYPE(BOOL, # Return type
+ LPCWSTR, # lpApplicationName
+ LPWSTR, # lpCommandLine
+ LPVOID, # lpProcessAttributes
+ LPVOID, # lpThreadAttributes
+ BOOL, # bInheritHandles
+ DWORD, # dwCreationFlags
+ LPVOID, # lpEnvironment
+ LPCWSTR, # lpCurrentDirectory
+ LPSTARTUPINFO, # lpStartupInfo
+ LPPROCESS_INFORMATION # lpProcessInformation
+ )
+
+CreateProcessFlags = ((1, "lpApplicationName", None),
+ (1, "lpCommandLine"),
+ (1, "lpProcessAttributes", None),
+ (1, "lpThreadAttributes", None),
+ (1, "bInheritHandles", True),
+ (1, "dwCreationFlags", 0),
+ (1, "lpEnvironment", None),
+ (1, "lpCurrentDirectory", None),
+ (1, "lpStartupInfo"),
+ (2, "lpProcessInformation"))
+
+def ErrCheckCreateProcess(result, func, args):
+ ErrCheckBool(result, func, args)
+ # return a tuple (hProcess, hThread, dwProcessID, dwThreadID)
+ pi = args[9]
+ return AutoHANDLE(pi.hProcess), AutoHANDLE(pi.hThread), pi.dwProcessID, pi.dwThreadID
+
+CreateProcess = CreateProcessProto(("CreateProcessW", windll.kernel32),
+ CreateProcessFlags)
+CreateProcess.errcheck = ErrCheckCreateProcess
+
+# flags for CreateProcess
+CREATE_BREAKAWAY_FROM_JOB = 0x01000000
+CREATE_DEFAULT_ERROR_MODE = 0x04000000
+CREATE_NEW_CONSOLE = 0x00000010
+CREATE_NEW_PROCESS_GROUP = 0x00000200
+CREATE_NO_WINDOW = 0x08000000
+CREATE_SUSPENDED = 0x00000004
+CREATE_UNICODE_ENVIRONMENT = 0x00000400
+
+# flags for job limit information
+# see http://msdn.microsoft.com/en-us/library/ms684147%28VS.85%29.aspx
+JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800
+JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK = 0x00001000
+
+# XXX these flags should be documented
+DEBUG_ONLY_THIS_PROCESS = 0x00000002
+DEBUG_PROCESS = 0x00000001
+DETACHED_PROCESS = 0x00000008
+
+# CreateJobObject()
+
+CreateJobObjectProto = WINFUNCTYPE(HANDLE, # Return type
+ LPVOID, # lpJobAttributes
+ LPCWSTR # lpName
+ )
+
+CreateJobObjectFlags = ((1, "lpJobAttributes", None),
+ (1, "lpName", None))
+
+CreateJobObject = CreateJobObjectProto(("CreateJobObjectW", windll.kernel32),
+ CreateJobObjectFlags)
+CreateJobObject.errcheck = ErrCheckHandle
+
+# AssignProcessToJobObject()
+
+AssignProcessToJobObjectProto = WINFUNCTYPE(BOOL, # Return type
+ HANDLE, # hJob
+ HANDLE # hProcess
+ )
+AssignProcessToJobObjectFlags = ((1, "hJob"),
+ (1, "hProcess"))
+AssignProcessToJobObject = AssignProcessToJobObjectProto(
+ ("AssignProcessToJobObject", windll.kernel32),
+ AssignProcessToJobObjectFlags)
+AssignProcessToJobObject.errcheck = ErrCheckBool
+
+# GetCurrentProcess()
+# because os.getPid() is way too easy
+GetCurrentProcessProto = WINFUNCTYPE(HANDLE # Return type
+ )
+GetCurrentProcessFlags = ()
+GetCurrentProcess = GetCurrentProcessProto(
+ ("GetCurrentProcess", windll.kernel32),
+ GetCurrentProcessFlags)
+GetCurrentProcess.errcheck = ErrCheckHandle
+
+# IsProcessInJob()
+try:
+ IsProcessInJobProto = WINFUNCTYPE(BOOL, # Return type
+ HANDLE, # Process Handle
+ HANDLE, # Job Handle
+ LPBOOL # Result
+ )
+ IsProcessInJobFlags = ((1, "ProcessHandle"),
+ (1, "JobHandle", HANDLE(0)),
+ (2, "Result"))
+ IsProcessInJob = IsProcessInJobProto(
+ ("IsProcessInJob", windll.kernel32),
+ IsProcessInJobFlags)
+ IsProcessInJob.errcheck = ErrCheckBool
+except AttributeError:
+ # windows 2k doesn't have this API
+ def IsProcessInJob(process):
+ return False
+
+
+# ResumeThread()
+
+def ErrCheckResumeThread(result, func, args):
+ if result == -1:
+ raise WinError()
+
+ return args
+
+ResumeThreadProto = WINFUNCTYPE(DWORD, # Return type
+ HANDLE # hThread
+ )
+ResumeThreadFlags = ((1, "hThread"),)
+ResumeThread = ResumeThreadProto(("ResumeThread", windll.kernel32),
+ ResumeThreadFlags)
+ResumeThread.errcheck = ErrCheckResumeThread
+
+# TerminateProcess()
+
+TerminateProcessProto = WINFUNCTYPE(BOOL, # Return type
+ HANDLE, # hProcess
+ UINT # uExitCode
+ )
+TerminateProcessFlags = ((1, "hProcess"),
+ (1, "uExitCode", 127))
+TerminateProcess = TerminateProcessProto(
+ ("TerminateProcess", windll.kernel32),
+ TerminateProcessFlags)
+TerminateProcess.errcheck = ErrCheckBool
+
+# TerminateJobObject()
+
+TerminateJobObjectProto = WINFUNCTYPE(BOOL, # Return type
+ HANDLE, # hJob
+ UINT # uExitCode
+ )
+TerminateJobObjectFlags = ((1, "hJob"),
+ (1, "uExitCode", 127))
+TerminateJobObject = TerminateJobObjectProto(
+ ("TerminateJobObject", windll.kernel32),
+ TerminateJobObjectFlags)
+TerminateJobObject.errcheck = ErrCheckBool
+
+# WaitForSingleObject()
+
+WaitForSingleObjectProto = WINFUNCTYPE(DWORD, # Return type
+ HANDLE, # hHandle
+ DWORD, # dwMilliseconds
+ )
+WaitForSingleObjectFlags = ((1, "hHandle"),
+ (1, "dwMilliseconds", -1))
+WaitForSingleObject = WaitForSingleObjectProto(
+ ("WaitForSingleObject", windll.kernel32),
+ WaitForSingleObjectFlags)
+
+INFINITE = -1
+WAIT_TIMEOUT = 0x0102
+WAIT_OBJECT_0 = 0x0
+WAIT_ABANDONED = 0x0080
+WAIT_FAILED = 0xFFFFFFFF
+
+# GetExitCodeProcess()
+
+GetExitCodeProcessProto = WINFUNCTYPE(BOOL, # Return type
+ HANDLE, # hProcess
+ LPDWORD, # lpExitCode
+ )
+GetExitCodeProcessFlags = ((1, "hProcess"),
+ (2, "lpExitCode"))
+GetExitCodeProcess = GetExitCodeProcessProto(
+ ("GetExitCodeProcess", windll.kernel32),
+ GetExitCodeProcessFlags)
+GetExitCodeProcess.errcheck = ErrCheckBool
+
+def CanCreateJobObject():
+ # Running firefox in a job (from cfx) hangs on sites using flash plugin
+ # so job creation is turned off for now. (see Bug 768651).
+ return False
+
+### testing functions
+
+def parent():
+ print 'Starting parent'
+ currentProc = GetCurrentProcess()
+ if IsProcessInJob(currentProc):
+ print >> sys.stderr, "You should not be in a job object to test"
+ sys.exit(1)
+ assert CanCreateJobObject()
+ print 'File: %s' % __file__
+ command = [sys.executable, __file__, '-child']
+ print 'Running command: %s' % command
+ process = Popen(command)
+ process.kill()
+ code = process.returncode
+ print 'Child code: %s' % code
+ assert code == 127
+
+def child():
+ print 'Starting child'
+ currentProc = GetCurrentProcess()
+ injob = IsProcessInJob(currentProc)
+ print "Is in a job?: %s" % injob
+ can_create = CanCreateJobObject()
+ print 'Can create job?: %s' % can_create
+ process = Popen('c:\\windows\\notepad.exe')
+ assert process._job
+ jobinfo = QueryInformationJobObject(process._job, 'JobObjectExtendedLimitInformation')
+ print 'Job info: %s' % jobinfo
+ limitflags = jobinfo['BasicLimitInformation']['LimitFlags']
+ print 'LimitFlags: %s' % limitflags
+ process.kill()
+
+if __name__ == '__main__':
+ import sys
+ from killableprocess import Popen
+ nargs = len(sys.argv[1:])
+ if nargs:
+ if nargs != 1 or sys.argv[1] != '-child':
+ raise AssertionError('Wrong flags; run like `python /path/to/winprocess.py`')
+ child()
+ else:
+ parent()
diff --git a/addon-sdk/source/python-lib/mozrunner/wpk.py b/addon-sdk/source/python-lib/mozrunner/wpk.py
new file mode 100644
index 000000000..6c92f5d4e
--- /dev/null
+++ b/addon-sdk/source/python-lib/mozrunner/wpk.py
@@ -0,0 +1,80 @@
+# 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/.
+
+from ctypes import sizeof, windll, addressof, c_wchar, create_unicode_buffer
+from ctypes.wintypes import DWORD, HANDLE
+
+PROCESS_TERMINATE = 0x0001
+PROCESS_QUERY_INFORMATION = 0x0400
+PROCESS_VM_READ = 0x0010
+
+def get_pids(process_name):
+ BIG_ARRAY = DWORD * 4096
+ processes = BIG_ARRAY()
+ needed = DWORD()
+
+ pids = []
+ result = windll.psapi.EnumProcesses(processes,
+ sizeof(processes),
+ addressof(needed))
+ if not result:
+ return pids
+
+ num_results = needed.value / sizeof(DWORD)
+
+ for i in range(num_results):
+ pid = processes[i]
+ process = windll.kernel32.OpenProcess(PROCESS_QUERY_INFORMATION |
+ PROCESS_VM_READ,
+ 0, pid)
+ if process:
+ module = HANDLE()
+ result = windll.psapi.EnumProcessModules(process,
+ addressof(module),
+ sizeof(module),
+ addressof(needed))
+ if result:
+ name = create_unicode_buffer(1024)
+ result = windll.psapi.GetModuleBaseNameW(process, module,
+ name, len(name))
+ # TODO: This might not be the best way to
+ # match a process name; maybe use a regexp instead.
+ if name.value.startswith(process_name):
+ pids.append(pid)
+ windll.kernel32.CloseHandle(module)
+ windll.kernel32.CloseHandle(process)
+
+ return pids
+
+def kill_pid(pid):
+ process = windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
+ if process:
+ windll.kernel32.TerminateProcess(process, 0)
+ windll.kernel32.CloseHandle(process)
+
+if __name__ == '__main__':
+ import subprocess
+ import time
+
+ # This test just opens a new notepad instance and kills it.
+
+ name = 'notepad'
+
+ old_pids = set(get_pids(name))
+ subprocess.Popen([name])
+ time.sleep(0.25)
+ new_pids = set(get_pids(name)).difference(old_pids)
+
+ if len(new_pids) != 1:
+ raise Exception('%s was not opened or get_pids() is '
+ 'malfunctioning' % name)
+
+ kill_pid(tuple(new_pids)[0])
+
+ newest_pids = set(get_pids(name)).difference(old_pids)
+
+ if len(newest_pids) != 0:
+ raise Exception('kill_pid() is malfunctioning')
+
+ print "Test passed."