diff options
Diffstat (limited to 'addon-sdk/source/python-lib/mozrunner')
-rw-r--r-- | addon-sdk/source/python-lib/mozrunner/__init__.py | 694 | ||||
-rw-r--r-- | addon-sdk/source/python-lib/mozrunner/killableprocess.py | 329 | ||||
-rw-r--r-- | addon-sdk/source/python-lib/mozrunner/qijo.py | 166 | ||||
-rw-r--r-- | addon-sdk/source/python-lib/mozrunner/winprocess.py | 379 | ||||
-rw-r--r-- | addon-sdk/source/python-lib/mozrunner/wpk.py | 80 |
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." |