mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 685903 - Remove firebug automation from testing/firebug. r=jhammel
This commit is contained in:
parent
709a50e38b
commit
337c6cc0d9
@ -1,54 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
DEPTH = @DEPTH@
|
|
||||||
topsrcdir = @top_srcdir@
|
|
||||||
srcdir = @srcdir@
|
|
||||||
VPATH = @srcdir@
|
|
||||||
otherlicenses = @top_srcdir@/other-licenses
|
|
||||||
|
|
||||||
include $(DEPTH)/config/autoconf.mk
|
|
||||||
|
|
||||||
MODULE = testing_firebug
|
|
||||||
|
|
||||||
include $(topsrcdir)/config/rules.mk
|
|
||||||
|
|
||||||
# Firebug requires several 3rd party packages.
|
|
||||||
# Grab them from the other-licenses directory:
|
|
||||||
# http://mxr.mozilla.org/mozilla-central/source/other-licenses/
|
|
||||||
TEST_OTHER_PACKAGES = \
|
|
||||||
simplejson-2.1.1 \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
TEST_OTHER_EXTRAS = \
|
|
||||||
virtualenv \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
# Harness packages from the srcdir;
|
|
||||||
# python packages to be installed IN INSTALLATION ORDER.
|
|
||||||
# Packages later in the list can depend only on packages earlier in the list.
|
|
||||||
TEST_HARNESS_PACKAGES = \
|
|
||||||
mozprofile \
|
|
||||||
mozprocess \
|
|
||||||
mozrunner \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
TEST_HARNESS_EXTRAS = \
|
|
||||||
installfirebug.py \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
TEST_HARNESS_FILES = \
|
|
||||||
fb_run.py \
|
|
||||||
fb-test-runner.config \
|
|
||||||
$(NULL)
|
|
||||||
|
|
||||||
stage-package: PKG_STAGE = $(DIST)/test-package-stage
|
|
||||||
stage-package:
|
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/firebug
|
|
||||||
@echo $(TEST_OTHER_PACKAGES) $(TEST_HARNESS_PACKAGES) > $(PKG_STAGE)/firebug/PACKAGES
|
|
||||||
@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_PACKAGES)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
|
|
||||||
@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_FILES)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
|
|
||||||
@(cd $(srcdir) && tar $(TAR_CREATE_FLAGS) - $(TEST_HARNESS_EXTRAS)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
|
|
||||||
@(cd $(otherlicenses) && tar $(TAR_CREATE_FLAGS) - $(TEST_OTHER_EXTRAS)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
|
|
||||||
@(cd $(otherlicenses) && tar $(TAR_CREATE_FLAGS) - $(TEST_OTHER_PACKAGES)) | (cd $(PKG_STAGE)/firebug && tar -xf -)
|
|
@ -1,18 +0,0 @@
|
|||||||
# This section contains arguments to be read by the firebug test runner
|
|
||||||
[runner_args]
|
|
||||||
server = http://10.250.5.0
|
|
||||||
|
|
||||||
# This section maps firefox versions to firebug versions.
|
|
||||||
# The default value is used for any firefox version not specified here.
|
|
||||||
[version_map]
|
|
||||||
3.6 = 1.6
|
|
||||||
4.0 = 1.7
|
|
||||||
4.0b = 1.7
|
|
||||||
default = 1.7
|
|
||||||
|
|
||||||
# This section allows disabling tests.
|
|
||||||
# Use 'test_name' = 'comma separated list of Firefox versions to disable the test against'
|
|
||||||
# See the Firebug test console for a full list of tests.
|
|
||||||
[disable_tests]
|
|
||||||
# Example
|
|
||||||
# firebug/testName.js = 3.6,4.0b
|
|
@ -1,270 +0,0 @@
|
|||||||
# 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 mozrunner import FirefoxRunner
|
|
||||||
from mozprofile import FirefoxProfile
|
|
||||||
from optparse import OptionParser
|
|
||||||
from ConfigParser import ConfigParser, NoOptionError, NoSectionError
|
|
||||||
from time import sleep
|
|
||||||
import logging
|
|
||||||
import urllib2
|
|
||||||
import os, sys, platform
|
|
||||||
|
|
||||||
class FBRunner:
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
# Set up the log file or use stdout if none specified
|
|
||||||
logLevel = logging.DEBUG if kwargs["debug"] else logging.INFO
|
|
||||||
filename = kwargs["log"]
|
|
||||||
self.log = logging.getLogger("FIREBUG")
|
|
||||||
if filename:
|
|
||||||
dirname = os.path.dirname(filename)
|
|
||||||
if dirname and not os.path.exists(dirname):
|
|
||||||
os.makedirs(dirname)
|
|
||||||
handler = logging.FileHandler(filename)
|
|
||||||
format = "%(asctime)s - %(name)s %(levelname)s | %(message)s"
|
|
||||||
else:
|
|
||||||
handler = logging.StreamHandler()
|
|
||||||
format = "%(name)s %(levelname)s | %(message)s"
|
|
||||||
handler.setFormatter(logging.Formatter(format))
|
|
||||||
self.log.addHandler(handler)
|
|
||||||
self.log.setLevel(logLevel)
|
|
||||||
|
|
||||||
# Initialization
|
|
||||||
self.binary = kwargs["binary"]
|
|
||||||
self.profile = kwargs["profile"]
|
|
||||||
self.serverpath = kwargs["serverpath"]
|
|
||||||
self.version = kwargs["version"]
|
|
||||||
self.testlist = kwargs["testlist"]
|
|
||||||
self.platform = platform.system().lower()
|
|
||||||
|
|
||||||
# Because the only communication between this script and the FBTest console is the
|
|
||||||
# log file, we don't know whether there was a crash or the test is just taking awhile.
|
|
||||||
# Make 1 minute the timeout for tests.
|
|
||||||
self.TEST_TIMEOUT = 60
|
|
||||||
|
|
||||||
# Get version of Firefox being run (only possible if we were passed in a binary)
|
|
||||||
self.appVersion = ""
|
|
||||||
if self.binary:
|
|
||||||
app = ConfigParser()
|
|
||||||
app.read(os.path.join(os.path.dirname(self.binary), "application.ini"))
|
|
||||||
ver = app.get("App", "Version").rstrip("0123456789pre") # Version should be of the form '3.6' or '4.0b' and not the whole string
|
|
||||||
self.appVersion = ver[:-1] if ver[-1]=="." else ver
|
|
||||||
|
|
||||||
# Read in fb-test-runner.config for local configuration
|
|
||||||
localConfig = ConfigParser()
|
|
||||||
localConfig.read("fb-test-runner.config")
|
|
||||||
if not self.serverpath:
|
|
||||||
self.serverpath = localConfig.get("runner_args", "server")
|
|
||||||
|
|
||||||
# Ensure serverpath has correct format
|
|
||||||
self.serverpath = self.serverpath.rstrip("/") + "/"
|
|
||||||
|
|
||||||
# Make sure we have a firebug version
|
|
||||||
if not self.version:
|
|
||||||
try:
|
|
||||||
self.version = localConfig.get("version_map", self.appVersion)
|
|
||||||
except NoOptionError:
|
|
||||||
self.version = localConfig.get("version_map", "default")
|
|
||||||
self.log.warning("Could not find an appropriate version of Firebug to use, using Firebug " + self.version)
|
|
||||||
|
|
||||||
# Read in the Firebug team's config file
|
|
||||||
try:
|
|
||||||
self.download(self.serverpath + "releases/firebug/test-bot.config", "test-bot.config")
|
|
||||||
except urllib2.URLError:
|
|
||||||
self.log.error("Could not download test-bot.config, check that '" + self.serverpath + "releases/firebug/test-bot.config' is valid")
|
|
||||||
raise
|
|
||||||
self.config = ConfigParser()
|
|
||||||
self.config.read("test-bot.config")
|
|
||||||
|
|
||||||
# Make sure we have a testlist
|
|
||||||
if not self.testlist:
|
|
||||||
self.testlist = self.config.get("Firebug"+self.version, "TEST_LIST")
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""
|
|
||||||
Remove temporarily downloaded files
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
for tmpFile in ["firebug.xpi", "fbtest.xpi", "test-bot.config"]:
|
|
||||||
if os.path.exists(tmpFile):
|
|
||||||
self.log.debug("Removing " + tmpFile)
|
|
||||||
os.remove(tmpFile)
|
|
||||||
except Exception, e:
|
|
||||||
self.log.warn("Could not clean up temporary files: " + str(e))
|
|
||||||
|
|
||||||
def download(self, url, savepath):
|
|
||||||
"""
|
|
||||||
Save the file located at 'url' into 'filename'
|
|
||||||
"""
|
|
||||||
self.log.debug("Downloading '" + url + "' to '" + savepath + "'")
|
|
||||||
ret = urllib2.urlopen(url)
|
|
||||||
savedir = os.path.dirname(savepath)
|
|
||||||
if savedir and not os.path.exists(savedir):
|
|
||||||
os.makedirs(savedir)
|
|
||||||
outfile = open(savepath, 'wb')
|
|
||||||
outfile.write(ret.read())
|
|
||||||
outfile.close()
|
|
||||||
|
|
||||||
def get_extensions(self):
|
|
||||||
"""
|
|
||||||
Downloads the firebug and fbtest extensions
|
|
||||||
for the specified Firebug version
|
|
||||||
"""
|
|
||||||
self.log.debug("Downloading firebug and fbtest extensions from server")
|
|
||||||
FIREBUG_XPI = self.config.get("Firebug" + self.version, "FIREBUG_XPI")
|
|
||||||
FBTEST_XPI = self.config.get("Firebug" + self.version, "FBTEST_XPI")
|
|
||||||
self.download(FIREBUG_XPI, "firebug.xpi")
|
|
||||||
self.download(FBTEST_XPI, "fbtest.xpi")
|
|
||||||
|
|
||||||
def disable_compatibilityCheck(self):
|
|
||||||
"""
|
|
||||||
Disables compatibility check which could
|
|
||||||
potentially prompt the user for action
|
|
||||||
"""
|
|
||||||
self.log.debug("Disabling compatibility check")
|
|
||||||
try:
|
|
||||||
prefs = open(os.path.join(self.profile, "prefs.js"), "a")
|
|
||||||
prefs.write("user_pref(\"extensions.checkCompatibility." + self.appVersion + "\", false);\n")
|
|
||||||
prefs.close()
|
|
||||||
except Exception, e:
|
|
||||||
self.log.warn("Could not disable compatibility check: " + str(e))
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""
|
|
||||||
Code for running the tests
|
|
||||||
"""
|
|
||||||
if self.profile:
|
|
||||||
# Ensure the profile actually exists
|
|
||||||
if not os.path.exists(self.profile):
|
|
||||||
self.log.warn("Profile '" + self.profile + "' doesn't exist. Creating temporary profile")
|
|
||||||
self.profile = None
|
|
||||||
else:
|
|
||||||
# Move any potential existing log files to log_old folder
|
|
||||||
if os.path.exists(os.path.join(self.profile, "firebug/fbtest/logs")):
|
|
||||||
self.log.debug("Moving existing log files to archive")
|
|
||||||
for name in os.listdir(os.path.join(self.profile, "firebug/fbtest/logs")):
|
|
||||||
os.rename(os.path.join(self.profile, "firebug/fbtest/logs", name), os.path.join(self.profile, "firebug/fbtest/logs_old", name))
|
|
||||||
|
|
||||||
# Grab the extensions from server
|
|
||||||
try:
|
|
||||||
self.get_extensions()
|
|
||||||
except (NoSectionError, NoOptionError), e:
|
|
||||||
self.log.error("Extensions could not be downloaded, malformed test-bot.config: " + str(e))
|
|
||||||
self.cleanup()
|
|
||||||
raise
|
|
||||||
except urllib2.URLError, e:
|
|
||||||
self.log.error("Extensions could not be downloaded, urllib2 error: " + str(e))
|
|
||||||
self.cleanup()
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Create environment variables
|
|
||||||
mozEnv = os.environ
|
|
||||||
mozEnv["XPC_DEBUG_WARN"] = "warn" # Suppresses certain alert warnings that may sometimes appear
|
|
||||||
mozEnv["MOZ_CRASHREPORTER_NO_REPORT"] = "true" # Disable crash reporter UI
|
|
||||||
|
|
||||||
# Create profile for mozrunner and start the Firebug tests
|
|
||||||
self.log.info("Starting Firebug Tests")
|
|
||||||
try:
|
|
||||||
self.log.debug("Creating Firefox profile and installing extensions")
|
|
||||||
mozProfile = FirefoxProfile(profile=self.profile, addons=["firebug.xpi", "fbtest.xpi"])
|
|
||||||
self.profile = mozProfile.profile
|
|
||||||
|
|
||||||
# Disable the compatibility check on startup
|
|
||||||
if self.binary:
|
|
||||||
self.disable_compatibilityCheck()
|
|
||||||
else:
|
|
||||||
self.log.warn("Can't disable compatibility check because binary wasn't specified")
|
|
||||||
|
|
||||||
self.log.debug("Running Firefox with cmdargs '-runFBTests " + self.testlist + "'")
|
|
||||||
mozRunner = FirefoxRunner(profile=mozProfile, binary=self.binary, cmdargs=["-runFBTests", self.testlist], env=mozEnv)
|
|
||||||
mozRunner.start()
|
|
||||||
except Exception, e:
|
|
||||||
self.log.error("Could not start Firefox: " + str(e))
|
|
||||||
self.cleanup()
|
|
||||||
raise
|
|
||||||
|
|
||||||
# Find the log file
|
|
||||||
timeout, logfile = 0, 0
|
|
||||||
# Wait up to 60 seconds for the log file to be initialized
|
|
||||||
while not logfile and timeout < 60:
|
|
||||||
try:
|
|
||||||
for name in os.listdir(os.path.join(self.profile, "firebug/fbtest/logs")):
|
|
||||||
logfile = open(os.path.join(self.profile, "firebug/fbtest/logs/", name))
|
|
||||||
except OSError:
|
|
||||||
timeout += 1
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
# If log file was not found
|
|
||||||
if not logfile:
|
|
||||||
self.log.error("Could not find the log file in profile '" + self.profile + "'")
|
|
||||||
self.cleanup()
|
|
||||||
raise
|
|
||||||
# If log file found, exit when fbtests finished (if no activity, wait up self.TEST_TIMEOUT)
|
|
||||||
else:
|
|
||||||
line, timeout = "", 0
|
|
||||||
while timeout < self.TEST_TIMEOUT:
|
|
||||||
line = logfile.readline()
|
|
||||||
if line == "":
|
|
||||||
sleep(1)
|
|
||||||
timeout += 1
|
|
||||||
else:
|
|
||||||
print line.rstrip()
|
|
||||||
if line.find("Test Suite Finished") != -1:
|
|
||||||
break
|
|
||||||
timeout = 0
|
|
||||||
|
|
||||||
# If there was a timeout, then there was most likely a crash (however could also be failure in FBTest console or test itself)
|
|
||||||
if timeout >= self.TEST_TIMEOUT:
|
|
||||||
logfile.seek(1)
|
|
||||||
line = logfile.readlines()[-1]
|
|
||||||
if line.find("FIREBUG INFO") != -1:
|
|
||||||
line = line[line.find("|") + 1:].lstrip() # Extract the test name from log line
|
|
||||||
line = line[:line.find("|")].rstrip()
|
|
||||||
else:
|
|
||||||
line = "Unknown Test"
|
|
||||||
print "FIREBUG TEST-UNEXPECTED-FAIL | " + line + " | Possible Firefox crash detected" # Print out crash message with offending test
|
|
||||||
self.log.warn("Possible crash detected - test run aborted")
|
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
logfile.close()
|
|
||||||
mozRunner.stop()
|
|
||||||
self.cleanup()
|
|
||||||
self.log.debug("Exiting - Status successful")
|
|
||||||
|
|
||||||
|
|
||||||
# Called from the command line
|
|
||||||
def cli(argv=sys.argv[1:]):
|
|
||||||
parser = OptionParser("usage: %prog [options]")
|
|
||||||
parser.add_option("--appname", dest="binary",
|
|
||||||
help="Firefox binary path")
|
|
||||||
|
|
||||||
parser.add_option("--profile-path", dest="profile",
|
|
||||||
help="The profile to use when running Firefox")
|
|
||||||
|
|
||||||
parser.add_option("-s", "--serverpath", dest="serverpath",
|
|
||||||
help="The http server containing the Firebug tests")
|
|
||||||
|
|
||||||
parser.add_option("-v", "--version", dest="version",
|
|
||||||
help="The firebug version to run")
|
|
||||||
|
|
||||||
parser.add_option("-t", "--testlist", dest="testlist",
|
|
||||||
help="Specify the name of the testlist to use, should usually use the default")
|
|
||||||
|
|
||||||
parser.add_option("--log", dest="log",
|
|
||||||
help="Path to the log file (default is stdout)")
|
|
||||||
|
|
||||||
parser.add_option("--debug", dest="debug",
|
|
||||||
action="store_true",
|
|
||||||
help="Enable debug logging")
|
|
||||||
(opt, remainder) = parser.parse_args(argv)
|
|
||||||
|
|
||||||
try:
|
|
||||||
runner = FBRunner(binary=opt.binary, profile=opt.profile, serverpath=opt.serverpath,
|
|
||||||
version=opt.version, testlist=opt.testlist, log=opt.log, debug=opt.debug)
|
|
||||||
runner.run()
|
|
||||||
except Exception:
|
|
||||||
return -1
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(cli())
|
|
@ -1,114 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# 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/.
|
|
||||||
|
|
||||||
"""
|
|
||||||
install firebug and its dependencies
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from optparse import OptionParser
|
|
||||||
from subprocess import call
|
|
||||||
|
|
||||||
### utility functions for cross-platform
|
|
||||||
|
|
||||||
def is_windows():
|
|
||||||
return sys.platform.startswith('win')
|
|
||||||
|
|
||||||
def esc(path):
|
|
||||||
"""quote and escape a path for cross-platform use"""
|
|
||||||
return '"%s"' % repr(path)[1:-1]
|
|
||||||
|
|
||||||
def scripts_path(virtual_env):
|
|
||||||
"""path to scripts directory"""
|
|
||||||
if is_windows():
|
|
||||||
return os.path.join(virtual_env, 'Scripts')
|
|
||||||
return os.path.join(virtual_env, 'bin')
|
|
||||||
|
|
||||||
def python_script_path(virtual_env, script_name):
|
|
||||||
"""path to a python script in a virtualenv"""
|
|
||||||
scripts_dir = scripts_path(virtual_env)
|
|
||||||
if is_windows():
|
|
||||||
script_name = script_name + '-script.py'
|
|
||||||
return os.path.join(scripts_dir, script_name)
|
|
||||||
|
|
||||||
def entry_point_path(virtual_env, entry_point):
|
|
||||||
path = os.path.join(scripts_path(virtual_env), entry_point)
|
|
||||||
if is_windows():
|
|
||||||
path += '.exe'
|
|
||||||
return path
|
|
||||||
|
|
||||||
### command-line entry point
|
|
||||||
|
|
||||||
def main(args=None):
|
|
||||||
"""command line front-end function"""
|
|
||||||
|
|
||||||
# parse command line arguments
|
|
||||||
args = args or sys.argv[1:]
|
|
||||||
usage = "Usage: %prog [options] [destination]"
|
|
||||||
parser = OptionParser(usage=usage)
|
|
||||||
parser.add_option('--develop', dest='develop',
|
|
||||||
action='store_true', default=False,
|
|
||||||
help='setup in development mode')
|
|
||||||
options, args = parser.parse_args(args)
|
|
||||||
|
|
||||||
# Print the python version
|
|
||||||
print 'Python: %s' % sys.version
|
|
||||||
|
|
||||||
# The data is kept in the same directory as the script
|
|
||||||
source=os.path.abspath(os.path.dirname(__file__))
|
|
||||||
|
|
||||||
# directory to install to
|
|
||||||
if not len(args):
|
|
||||||
destination = source
|
|
||||||
elif len(args) == 1:
|
|
||||||
destination = os.path.abspath(args[0])
|
|
||||||
else:
|
|
||||||
parser.print_usage()
|
|
||||||
parser.exit(1)
|
|
||||||
|
|
||||||
os.chdir(source)
|
|
||||||
|
|
||||||
# check for existence of necessary files
|
|
||||||
if not os.path.exists('virtualenv'):
|
|
||||||
print "File not found: virtualenv"
|
|
||||||
sys.exit(1)
|
|
||||||
PACKAGES_FILE = 'PACKAGES'
|
|
||||||
if not os.path.exists(PACKAGES_FILE) and destination != source:
|
|
||||||
PACKAGES_FILE = os.path.join(destination, PACKAGES_FILE)
|
|
||||||
if not os.path.exists(PACKAGES_FILE):
|
|
||||||
print "File not found: PACKAGES"
|
|
||||||
|
|
||||||
# packages to install in dependency order
|
|
||||||
PACKAGES=file(PACKAGES_FILE).read().split()
|
|
||||||
assert PACKAGES
|
|
||||||
|
|
||||||
# create the virtualenv and install packages
|
|
||||||
env = os.environ.copy()
|
|
||||||
env.pop('PYTHONHOME', None)
|
|
||||||
returncode = call([sys.executable, os.path.join('virtualenv', 'virtualenv.py'), destination], env=env)
|
|
||||||
if returncode:
|
|
||||||
print 'Failure to install virtualenv'
|
|
||||||
sys.exit(returncode)
|
|
||||||
if options.develop:
|
|
||||||
python = entry_point_path(destination, 'python')
|
|
||||||
for package in PACKAGES:
|
|
||||||
oldcwd = os.getcwd()
|
|
||||||
os.chdir(package)
|
|
||||||
returncode = call([python, 'setup.py', 'develop'])
|
|
||||||
os.chdir(oldcwd)
|
|
||||||
if returncode:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
pip = entry_point_path(destination, 'pip')
|
|
||||||
returncode = call([pip, 'install'] + PACKAGES, env=env)
|
|
||||||
|
|
||||||
if returncode:
|
|
||||||
print 'Failure to install packages'
|
|
||||||
sys.exit(returncode)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
@ -1,5 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
#
|
|
@ -1,292 +0,0 @@
|
|||||||
# 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
|
|
||||||
|
|
||||||
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, executable, preexec_fn, close_fds,
|
|
||||||
cwd, env, universal_newlines, startupinfo,
|
|
||||||
creationflags, shell,
|
|
||||||
p2cread, p2cwrite,
|
|
||||||
c2pread, c2pwrite,
|
|
||||||
errread, errwrite):
|
|
||||||
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:
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
winprocess.TerminateProcess(self._handle, 127)
|
|
||||||
except:
|
|
||||||
# TODO: better error handling here
|
|
||||||
pass
|
|
||||||
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
|
|
||||||
|
|
||||||
if self.returncode is not None:
|
|
||||||
return self.returncode
|
|
||||||
|
|
||||||
starttime = datetime.datetime.now()
|
|
||||||
|
|
||||||
if mswindows:
|
|
||||||
if timeout is None:
|
|
||||||
timeout = -1
|
|
||||||
rc = winprocess.WaitForSingleObject(self._handle, timeout)
|
|
||||||
|
|
||||||
if rc != winprocess.WAIT_TIMEOUT:
|
|
||||||
def check():
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
diff = now - starttime
|
|
||||||
if (diff.seconds * 1000 * 1000 + diff.microseconds) < (timeout * 1000):
|
|
||||||
if self._job:
|
|
||||||
if (winprocess.QueryInformationJobObject(self._job, 8)['BasicInfo']['ActiveProcesses'] > 0):
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
while check():
|
|
||||||
time.sleep(.5)
|
|
||||||
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
diff = now - starttime
|
|
||||||
if (diff.seconds * 1000 * 1000 + diff.microseconds) > (timeout * 1000):
|
|
||||||
self.kill(group)
|
|
||||||
else:
|
|
||||||
self.returncode = winprocess.GetExitCodeProcess(self._handle)
|
|
||||||
else:
|
|
||||||
if (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
|
|
||||||
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)
|
|
@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# 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 subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
def get_pids(name, minimun_pid=0):
|
|
||||||
"""Get all the pids matching name, exclude any pids below minimum_pid."""
|
|
||||||
# XXX see also https://bugzilla.mozilla.org/show_bug.cgi?id=592750
|
|
||||||
|
|
||||||
if os.name == 'nt' or sys.platform == 'cygwin':
|
|
||||||
import wpk
|
|
||||||
pids = wpk.get_pids(name)
|
|
||||||
else:
|
|
||||||
process = subprocess.Popen(['ps', 'ax'], stdout=subprocess.PIPE)
|
|
||||||
output, _ = process.communicate()
|
|
||||||
data = output.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
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
import sys
|
|
||||||
pids = set()
|
|
||||||
for i in sys.argv[1:]:
|
|
||||||
pids.update(get_pids(i))
|
|
||||||
for i in sorted(pids):
|
|
||||||
print i
|
|
@ -1,166 +0,0 @@
|
|||||||
# 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!"
|
|
@ -1,381 +0,0 @@
|
|||||||
# 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
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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():
|
|
||||||
currentProc = GetCurrentProcess()
|
|
||||||
if IsProcessInJob(currentProc):
|
|
||||||
jobinfo = QueryInformationJobObject(HANDLE(0), 'JobObjectExtendedLimitInformation')
|
|
||||||
limitflags = jobinfo['BasicLimitInformation']['LimitFlags']
|
|
||||||
return bool(limitflags & JOB_OBJECT_LIMIT_BREAKAWAY_OK) or bool(limitflags & JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK)
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
### 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()
|
|
@ -1,80 +0,0 @@
|
|||||||
# 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."
|
|
@ -1,25 +0,0 @@
|
|||||||
from setuptools import setup, find_packages
|
|
||||||
|
|
||||||
version = '0.1a'
|
|
||||||
|
|
||||||
setup(name='mozprocess',
|
|
||||||
version=version,
|
|
||||||
description="Mozilla-authored process handling",
|
|
||||||
long_description="""\
|
|
||||||
""",
|
|
||||||
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
|
||||||
keywords='',
|
|
||||||
author='Mozilla Automation and Testing Team',
|
|
||||||
author_email='mozmill-dev@googlegroups.com',
|
|
||||||
url='http://github.com/mozautomation/mozmill',
|
|
||||||
license='MPL',
|
|
||||||
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
|
|
||||||
include_package_data=True,
|
|
||||||
zip_safe=False,
|
|
||||||
install_requires=[
|
|
||||||
# -*- Extra requirements: -*-
|
|
||||||
],
|
|
||||||
entry_points="""
|
|
||||||
# -*- Entry points: -*-
|
|
||||||
""",
|
|
||||||
)
|
|
@ -1,5 +0,0 @@
|
|||||||
# 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 profile import *
|
|
@ -1,219 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
__all__ = ['Profile', 'FirefoxProfile', 'ThunderbirdProfile', 'print_addon_ids']
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import zipfile
|
|
||||||
from xml.dom import minidom
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson
|
|
||||||
except ImportError:
|
|
||||||
import json as simplejson
|
|
||||||
|
|
||||||
# Use dir_util for copy/rm operations because shutil is all kinds of broken
|
|
||||||
from distutils import dir_util
|
|
||||||
copytree = dir_util.copy_tree
|
|
||||||
rmtree = dir_util.remove_tree
|
|
||||||
|
|
||||||
|
|
||||||
class Profile(object):
|
|
||||||
"""Handles all operations regarding profile. Created new profiles, installs extensions,
|
|
||||||
sets preferences and handles cleanup."""
|
|
||||||
|
|
||||||
def __init__(self, profile=None, addons=None, preferences=None):
|
|
||||||
|
|
||||||
# Handle profile creation
|
|
||||||
self.create_new = not profile
|
|
||||||
if profile:
|
|
||||||
self.profile = profile
|
|
||||||
else:
|
|
||||||
self.profile = self.create_new_profile()
|
|
||||||
|
|
||||||
# set preferences from class preferences
|
|
||||||
if hasattr(self.__class__, 'preferences'):
|
|
||||||
self.preferences = self.__class__.preferences.copy()
|
|
||||||
else:
|
|
||||||
self.preferences = {}
|
|
||||||
self.preferences.update(preferences or {})
|
|
||||||
self.set_preferences(self.preferences)
|
|
||||||
|
|
||||||
# handle addon installation
|
|
||||||
self.addons_installed = []
|
|
||||||
self.addons = addons or []
|
|
||||||
for addon in self.addons:
|
|
||||||
self.install_addon(addon)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""
|
|
||||||
reset the profile to the beginning state
|
|
||||||
"""
|
|
||||||
self.cleanup()
|
|
||||||
if self.create_new:
|
|
||||||
self.__init__(addons=self.addons, preferences=self.preferences)
|
|
||||||
else:
|
|
||||||
self.__init__(profile=self.profile, addons=self.addons, preferences=self.preferences)
|
|
||||||
|
|
||||||
def create_new_profile(self):
|
|
||||||
"""Create a new clean profile in tmp which is a simple empty folder"""
|
|
||||||
profile = tempfile.mkdtemp(suffix='.mozrunner')
|
|
||||||
return profile
|
|
||||||
|
|
||||||
### methods related to addons
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def addon_id(self, addon_path):
|
|
||||||
"""
|
|
||||||
return the id for a given addon, or None if not found
|
|
||||||
- addon_path : path to the addon directory
|
|
||||||
"""
|
|
||||||
|
|
||||||
def find_id(desc):
|
|
||||||
"""finds the addon id give its description"""
|
|
||||||
|
|
||||||
addon_id = None
|
|
||||||
for elem in desc:
|
|
||||||
apps = elem.getElementsByTagName('em:targetApplication')
|
|
||||||
if apps:
|
|
||||||
for app in apps:
|
|
||||||
# remove targetApplication nodes, they contain id's we aren't interested in
|
|
||||||
elem.removeChild(app)
|
|
||||||
if elem.getElementsByTagName('em:id'):
|
|
||||||
addon_id = str(elem.getElementsByTagName('em:id')[0].firstChild.data)
|
|
||||||
elif elem.hasAttribute('em:id'):
|
|
||||||
addon_id = str(elem.getAttribute('em:id'))
|
|
||||||
return addon_id
|
|
||||||
|
|
||||||
doc = minidom.parse(os.path.join(addon_path, 'install.rdf'))
|
|
||||||
|
|
||||||
for tag in 'Description', 'RDF:Description':
|
|
||||||
desc = doc.getElementsByTagName(tag)
|
|
||||||
addon_id = find_id(desc)
|
|
||||||
if addon_id:
|
|
||||||
return addon_id
|
|
||||||
|
|
||||||
def install_addon(self, path):
|
|
||||||
"""Installs the given addon or directory of addons in the profile."""
|
|
||||||
|
|
||||||
# if the addon is a directory, install all addons in it
|
|
||||||
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'):
|
|
||||||
tmpdir = tempfile.mkdtemp(suffix = "." + os.path.split(addon)[-1])
|
|
||||||
compressed_file = zipfile.ZipFile(addon, "r")
|
|
||||||
for name in compressed_file.namelist():
|
|
||||||
if name.endswith('/'):
|
|
||||||
os.makedirs(os.path.join(tmpdir, name))
|
|
||||||
else:
|
|
||||||
if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):
|
|
||||||
os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))
|
|
||||||
data = compressed_file.read(name)
|
|
||||||
f = open(os.path.join(tmpdir, name), 'wb')
|
|
||||||
f.write(data)
|
|
||||||
f.close()
|
|
||||||
addon = tmpdir
|
|
||||||
|
|
||||||
# determine the addon id
|
|
||||||
addon_id = Profile.addon_id(addon)
|
|
||||||
assert addon_id is not None, "The addon id could not be found: %s" % addon
|
|
||||||
|
|
||||||
# copy the addon to the profile
|
|
||||||
addon_path = os.path.join(self.profile, 'extensions', addon_id)
|
|
||||||
copytree(addon, addon_path, preserve_symlinks=1)
|
|
||||||
self.addons_installed.append(addon_path)
|
|
||||||
|
|
||||||
def clean_addons(self):
|
|
||||||
"""Cleans up addons in the profile."""
|
|
||||||
for addon in self.addons_installed:
|
|
||||||
if os.path.isdir(addon):
|
|
||||||
rmtree(addon)
|
|
||||||
|
|
||||||
### methods for preferences
|
|
||||||
|
|
||||||
def set_preferences(self, preferences):
|
|
||||||
"""Adds preferences dict to profile preferences"""
|
|
||||||
|
|
||||||
prefs_file = os.path.join(self.profile, 'user.js')
|
|
||||||
|
|
||||||
# Ensure that the file exists first otherwise create an empty file
|
|
||||||
if os.path.isfile(prefs_file):
|
|
||||||
f = open(prefs_file, 'a+')
|
|
||||||
else:
|
|
||||||
f = open(prefs_file, 'w')
|
|
||||||
|
|
||||||
f.write('\n#MozRunner Prefs Start\n')
|
|
||||||
|
|
||||||
pref_lines = ['user_pref(%s, %s);' %
|
|
||||||
(simplejson.dumps(k), simplejson.dumps(v) ) for k, v in
|
|
||||||
preferences.items()]
|
|
||||||
for line in pref_lines:
|
|
||||||
f.write(line+'\n')
|
|
||||||
f.write('#MozRunner Prefs End\n')
|
|
||||||
f.flush() ; f.close()
|
|
||||||
|
|
||||||
def clean_preferences(self):
|
|
||||||
"""Removed preferences added by mozrunner."""
|
|
||||||
lines = open(os.path.join(self.profile, 'user.js'), 'r').read().splitlines()
|
|
||||||
s = lines.index('#MozRunner Prefs Start') ; e = lines.index('#MozRunner Prefs End')
|
|
||||||
cleaned_prefs = '\n'.join(lines[:s] + lines[e+1:])
|
|
||||||
f = open(os.path.join(self.profile, 'user.js'), 'w')
|
|
||||||
f.write(cleaned_prefs) ; f.flush() ; f.close()
|
|
||||||
|
|
||||||
### cleanup
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Cleanup operations on the profile."""
|
|
||||||
if self.create_new:
|
|
||||||
if os.path.exists(self.profile):
|
|
||||||
rmtree(self.profile)
|
|
||||||
else:
|
|
||||||
self.clean_preferences()
|
|
||||||
self.clean_addons()
|
|
||||||
|
|
||||||
__del__ = cleanup
|
|
||||||
|
|
||||||
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,
|
|
||||||
# Dont' run the add-on compatibility check during start-up
|
|
||||||
'extensions.showMismatchUI' : False,
|
|
||||||
# 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,
|
|
||||||
# Suppress automatic safe mode after crashes
|
|
||||||
'toolkit.startup.max_resumed_crashes' : -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def print_addon_ids(args=sys.argv[1:]):
|
|
||||||
"""print addon ids for testing"""
|
|
||||||
for arg in args:
|
|
||||||
print Profile.addon_id(arg)
|
|
@ -1,39 +0,0 @@
|
|||||||
# 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 setuptools import setup, find_packages
|
|
||||||
import sys
|
|
||||||
|
|
||||||
version = '0.1a'
|
|
||||||
|
|
||||||
# we only support python 2 right now
|
|
||||||
assert sys.version_info[0] == 2
|
|
||||||
|
|
||||||
deps = []
|
|
||||||
# version-dependent dependencies
|
|
||||||
if sys.version_info[1] < 6:
|
|
||||||
deps.append('simplejson')
|
|
||||||
|
|
||||||
setup(name='mozprofile',
|
|
||||||
version=version,
|
|
||||||
description="handling of Mozilla XUL app profiles",
|
|
||||||
long_description="""\
|
|
||||||
""",
|
|
||||||
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
|
|
||||||
keywords='',
|
|
||||||
author='Mozilla Automation + Testing Team',
|
|
||||||
author_email='mozmill-dev@googlegroups.com',
|
|
||||||
url='http://github.com/mozautomation/mozmill',
|
|
||||||
license='MPL',
|
|
||||||
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
|
|
||||||
include_package_data=True,
|
|
||||||
zip_safe=False,
|
|
||||||
install_requires=deps,
|
|
||||||
entry_points="""
|
|
||||||
# -*- Entry points: -*-
|
|
||||||
|
|
||||||
[console_scripts]
|
|
||||||
addon_id = mozprofile:print_addon_ids
|
|
||||||
""",
|
|
||||||
)
|
|
@ -1,5 +0,0 @@
|
|||||||
# 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 runner import *
|
|
@ -1,337 +0,0 @@
|
|||||||
# 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/.
|
|
||||||
|
|
||||||
__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'create_runner', 'CLI', 'cli']
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import optparse
|
|
||||||
import ConfigParser
|
|
||||||
|
|
||||||
from utils import findInPath
|
|
||||||
from mozprocess import killableprocess
|
|
||||||
from mozprocess.pid import get_pids
|
|
||||||
from mozprofile import *
|
|
||||||
|
|
||||||
class Runner(object):
|
|
||||||
"""Handles all running operations. Finds bins, runs and kills the process."""
|
|
||||||
|
|
||||||
def __init__(self, profile, binary=None, cmdargs=None, env=None, kp_kwargs=None):
|
|
||||||
self.process_handler = None
|
|
||||||
self.profile = profile
|
|
||||||
|
|
||||||
self.binary = self.__class__.get_binary(binary)
|
|
||||||
|
|
||||||
if not os.path.exists(self.binary):
|
|
||||||
raise Exception("Binary path does not exist "+self.binary)
|
|
||||||
|
|
||||||
self.cmdargs = cmdargs or []
|
|
||||||
_cmdargs = [i for i in self.cmdargs
|
|
||||||
if i != '-foreground']
|
|
||||||
if len(_cmdargs) != len(self.cmdargs):
|
|
||||||
# foreground should be last; see
|
|
||||||
# - https://bugzilla.mozilla.org/show_bug.cgi?id=625614
|
|
||||||
# - https://bugzilla.mozilla.org/show_bug.cgi?id=626826
|
|
||||||
self.cmdargs = _cmdargs
|
|
||||||
self.cmdargs.append('-foreground')
|
|
||||||
|
|
||||||
if env is None:
|
|
||||||
self.env = os.environ.copy()
|
|
||||||
self.env.update({'MOZ_NO_REMOTE':'1',})
|
|
||||||
else:
|
|
||||||
self.env = env
|
|
||||||
self.kp_kwargs = kp_kwargs or {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_binary(cls, binary=None):
|
|
||||||
"""determine the binary"""
|
|
||||||
if binary is None:
|
|
||||||
return cls.find_binary()
|
|
||||||
elif sys.platform == 'darwin' and binary.find('Contents/MacOS/') == -1:
|
|
||||||
# TODO FIX ME!!!
|
|
||||||
return os.path.join(binary, 'Contents/MacOS/%s-bin' % cls.names[0])
|
|
||||||
else:
|
|
||||||
return binary
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def find_binary(cls):
|
|
||||||
"""Finds the binary for class names if one was not provided."""
|
|
||||||
|
|
||||||
binary = None
|
|
||||||
if sys.platform in ('linux2', 'sunos5', 'solaris'):
|
|
||||||
for name in reversed(cls.names):
|
|
||||||
binary = findInPath(name)
|
|
||||||
elif os.name == 'nt' or sys.platform == 'cygwin':
|
|
||||||
|
|
||||||
# find the default executable from the windows registry
|
|
||||||
try:
|
|
||||||
# assumes cls.app_name is defined, as it should be for
|
|
||||||
# implementors
|
|
||||||
import _winreg
|
|
||||||
app_key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, r"Software\Mozilla\Mozilla %s" % cls.app_name)
|
|
||||||
version, _type = _winreg.QueryValueEx(app_key, "CurrentVersion")
|
|
||||||
version_key = _winreg.OpenKey(app_key, version + r"\Main")
|
|
||||||
path, _ = _winreg.QueryValueEx(version_key, "PathToExe")
|
|
||||||
return path
|
|
||||||
except: # XXX not sure what type of exception this should be
|
|
||||||
pass
|
|
||||||
|
|
||||||
# search for the binary in the path
|
|
||||||
for name in reversed(cls.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,'Minefield', 'firefox.exe'),
|
|
||||||
(os.environ.get("ProgramFiles(x86)"),'Minefield', 'firefox.exe')
|
|
||||||
]:
|
|
||||||
path = os.path.join(*bin)
|
|
||||||
if os.path.isfile(path):
|
|
||||||
binary = path
|
|
||||||
break
|
|
||||||
elif sys.platform == 'darwin':
|
|
||||||
for name in reversed(cls.names):
|
|
||||||
appdir = os.path.join('Applications', name.capitalize()+'.app')
|
|
||||||
if os.path.isdir(os.path.join(os.path.expanduser('~/'), appdir)):
|
|
||||||
binary = os.path.join(os.path.expanduser('~/'), appdir,
|
|
||||||
'Contents/MacOS/'+name+'-bin')
|
|
||||||
elif os.path.isdir('/'+appdir):
|
|
||||||
binary = os.path.join("/"+appdir, 'Contents/MacOS/'+name+'-bin')
|
|
||||||
|
|
||||||
if binary is not None:
|
|
||||||
if not os.path.isfile(binary):
|
|
||||||
binary = binary.replace(name+'-bin', 'firefox-bin')
|
|
||||||
if not os.path.isfile(binary):
|
|
||||||
binary = None
|
|
||||||
if binary is None:
|
|
||||||
raise Exception('Mozrunner could not locate your binary, you will need to set it.')
|
|
||||||
return binary
|
|
||||||
|
|
||||||
@property
|
|
||||||
def command(self):
|
|
||||||
"""Returns the command list to run."""
|
|
||||||
return [self.binary, '-profile', self.profile.profile]
|
|
||||||
|
|
||||||
def get_repositoryInfo(self):
|
|
||||||
"""Read repository information from application.ini and platform.ini."""
|
|
||||||
# TODO: I think we should keep this, but I think Jeff's patch moves it to the top of the fileimport ConfigParser
|
|
||||||
|
|
||||||
config = ConfigParser.RawConfigParser()
|
|
||||||
dirname = os.path.dirname(self.binary)
|
|
||||||
repository = { }
|
|
||||||
|
|
||||||
for file, section in [('application', 'App'), ('platform', 'Build')]:
|
|
||||||
config.read(os.path.join(dirname, '%s.ini' % file))
|
|
||||||
|
|
||||||
for key, id in [('SourceRepository', 'repository'),
|
|
||||||
('SourceStamp', 'changeset')]:
|
|
||||||
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."""
|
|
||||||
self.process_handler = killableprocess.runCommand(self.command+self.cmdargs, env=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 stop(self):
|
|
||||||
"""Kill the app"""
|
|
||||||
if self.process_handler is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
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)
|
|
||||||
except Exception, e:
|
|
||||||
raise Exception('Cannot kill process, '+type(e).__name__+' '+e.message)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""
|
|
||||||
reset the runner between runs
|
|
||||||
currently, only resets the profile, but probably should do more
|
|
||||||
"""
|
|
||||||
self.profile.reset()
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
self.stop()
|
|
||||||
self.profile.cleanup()
|
|
||||||
|
|
||||||
__del__ = cleanup
|
|
||||||
|
|
||||||
|
|
||||||
class FirefoxRunner(Runner):
|
|
||||||
"""Specialized Runner subclass for running Firefox."""
|
|
||||||
|
|
||||||
app_name = 'Firefox'
|
|
||||||
profile_class = FirefoxProfile
|
|
||||||
|
|
||||||
if sys.platform == 'darwin':
|
|
||||||
names = ['firefox', 'minefield', 'shiretoko']
|
|
||||||
elif (sys.platform == 'linux2') or (sys.platform in ('sunos5', 'solaris')):
|
|
||||||
names = ['firefox', 'mozilla-firefox', 'iceweasel']
|
|
||||||
elif os.name == 'nt' or sys.platform == 'cygwin':
|
|
||||||
names =['firefox']
|
|
||||||
else:
|
|
||||||
raise AssertionError("I don't know what platform you're on")
|
|
||||||
|
|
||||||
class ThunderbirdRunner(Runner):
|
|
||||||
"""Specialized Runner subclass for running Thunderbird"""
|
|
||||||
app_name = 'Thunderbird'
|
|
||||||
|
|
||||||
names = ["thunderbird", "shredder"]
|
|
||||||
|
|
||||||
def create_runner(profile_class, runner_class,
|
|
||||||
binary=None, profile_args=None, runner_args=None):
|
|
||||||
"""Get the runner object, a not-very-abstract factory"""
|
|
||||||
profile_args = profile_args or {}
|
|
||||||
runner_args = runner_args or {}
|
|
||||||
profile = profile_class(**profile_args)
|
|
||||||
binary = runner_class.get_binary(binary)
|
|
||||||
runner = runner_class(binary=binary,
|
|
||||||
profile=profile,
|
|
||||||
**runner_args)
|
|
||||||
return runner
|
|
||||||
|
|
||||||
class CLI(object):
|
|
||||||
"""Command line interface."""
|
|
||||||
|
|
||||||
module = "mozrunner"
|
|
||||||
|
|
||||||
def __init__(self, args=sys.argv[1:]):
|
|
||||||
"""
|
|
||||||
Setup command line parser and parse arguments
|
|
||||||
- args : command line arguments
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.metadata = self.get_metadata_from_egg()
|
|
||||||
self.parser = optparse.OptionParser(version="%prog " + self.metadata["Version"])
|
|
||||||
self.add_options(self.parser)
|
|
||||||
(self.options, self.args) = self.parser.parse_args(args)
|
|
||||||
|
|
||||||
if self.options.info:
|
|
||||||
self.print_metadata()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# choose appropriate runner and profile classes
|
|
||||||
if self.options.app == 'firefox':
|
|
||||||
self.runner_class = FirefoxRunner
|
|
||||||
self.profile_class = FirefoxProfile
|
|
||||||
elif self.options.app == 'thunderbird':
|
|
||||||
self.runner_class = ThunderbirdRunner
|
|
||||||
self.profile_class = ThunderbirdProfile
|
|
||||||
else:
|
|
||||||
self.parser.error('Application "%s" unknown (should be one of "firefox" or "thunderbird"' % self.options.app)
|
|
||||||
|
|
||||||
def add_options(self, parser):
|
|
||||||
"""add options to the parser"""
|
|
||||||
|
|
||||||
parser.add_option('-b', "--binary",
|
|
||||||
dest="binary", help="Binary path.",
|
|
||||||
metavar=None, default=None)
|
|
||||||
|
|
||||||
parser.add_option('-p', "--profile",
|
|
||||||
dest="profile", help="Profile path.",
|
|
||||||
metavar=None, default=None)
|
|
||||||
|
|
||||||
parser.add_option('-a', "--addon", dest="addons",
|
|
||||||
action='append',
|
|
||||||
help="Addons paths to install",
|
|
||||||
metavar=None, default=[])
|
|
||||||
|
|
||||||
parser.add_option("--info", dest="info", default=False,
|
|
||||||
action="store_true",
|
|
||||||
help="Print module information")
|
|
||||||
parser.add_option('--app', dest='app', default='firefox',
|
|
||||||
help="Application to use [DEFAULT: %default]")
|
|
||||||
parser.add_option('--app-arg', dest='appArgs',
|
|
||||||
default=[], action='append',
|
|
||||||
help="provides an argument to the test application")
|
|
||||||
|
|
||||||
### methods regarding introspecting data
|
|
||||||
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]
|
|
||||||
|
|
||||||
### methods for running
|
|
||||||
def profile_args(self):
|
|
||||||
"""arguments to instantiate the profile class"""
|
|
||||||
return dict(profile=self.options.profile,
|
|
||||||
addons=self.options.addons)
|
|
||||||
|
|
||||||
def command_args(self):
|
|
||||||
"""additional arguments for the mozilla application"""
|
|
||||||
return self.options.appArgs
|
|
||||||
|
|
||||||
def runner_args(self):
|
|
||||||
"""arguments to instantiate the runner class"""
|
|
||||||
return dict(cmdargs=self.command_args())
|
|
||||||
|
|
||||||
def create_runner(self):
|
|
||||||
return create_runner(self.profile_class,
|
|
||||||
self.runner_class,
|
|
||||||
self.options.binary,
|
|
||||||
self.profile_args(),
|
|
||||||
self.runner_args())
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
runner = self.create_runner()
|
|
||||||
self.start(runner)
|
|
||||||
# XXX should be runner.cleanup,
|
|
||||||
# and other runner cleanup code should go in there
|
|
||||||
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(args=sys.argv[1:]):
|
|
||||||
CLI(args).run()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
cli()
|
|
@ -1,27 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
# 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/.
|
|
||||||
|
|
||||||
"""
|
|
||||||
utility functions for mozrunner
|
|
||||||
"""
|
|
||||||
|
|
||||||
__all__ = ['findInPath']
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
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")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
for i in sys.argv[1:]:
|
|
||||||
print findInPath(i)
|
|
@ -1,42 +0,0 @@
|
|||||||
# 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 setuptools import setup, find_packages
|
|
||||||
import sys
|
|
||||||
|
|
||||||
desc = """Reliable start/stop/configuration of Mozilla Applications (Firefox, Thunderbird, etc.)"""
|
|
||||||
|
|
||||||
|
|
||||||
PACKAGE_NAME = "mozrunner"
|
|
||||||
PACKAGE_VERSION = "3.0a"
|
|
||||||
|
|
||||||
deps = ['mozprocess', 'mozprofile']
|
|
||||||
|
|
||||||
# we only support python 2 right now
|
|
||||||
assert sys.version_info[0] == 2
|
|
||||||
|
|
||||||
setup(name=PACKAGE_NAME,
|
|
||||||
version=PACKAGE_VERSION,
|
|
||||||
description=desc,
|
|
||||||
long_description=desc,
|
|
||||||
author='Mikeal Rogers, Mozilla',
|
|
||||||
author_email='mikeal.rogers@gmail.com',
|
|
||||||
url='http://github.com/mozautomation/mozmill',
|
|
||||||
license='MPL 1.1/GPL 2.0/LGPL 2.1',
|
|
||||||
packages=find_packages(exclude=['legacy']),
|
|
||||||
zip_safe=False,
|
|
||||||
entry_points="""
|
|
||||||
[console_scripts]
|
|
||||||
mozrunner = mozrunner:cli
|
|
||||||
""",
|
|
||||||
platforms =['Any'],
|
|
||||||
install_requires = deps,
|
|
||||||
classifiers=['Development Status :: 4 - Beta',
|
|
||||||
'Environment :: Console',
|
|
||||||
'Intended Audience :: Developers',
|
|
||||||
'License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)',
|
|
||||||
'Operating System :: OS Independent',
|
|
||||||
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
||||||
]
|
|
||||||
)
|
|
@ -308,7 +308,6 @@ package-tests: \
|
|||||||
stage-xpcshell \
|
stage-xpcshell \
|
||||||
stage-jstests \
|
stage-jstests \
|
||||||
stage-jetpack \
|
stage-jetpack \
|
||||||
stage-firebug \
|
|
||||||
stage-peptest \
|
stage-peptest \
|
||||||
stage-mozbase \
|
stage-mozbase \
|
||||||
stage-tps \
|
stage-tps \
|
||||||
@ -344,7 +343,6 @@ make-stage-dir:
|
|||||||
$(NSINSTALL) -D $(PKG_STAGE)/bin/components
|
$(NSINSTALL) -D $(PKG_STAGE)/bin/components
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/certs
|
$(NSINSTALL) -D $(PKG_STAGE)/certs
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/jetpack
|
$(NSINSTALL) -D $(PKG_STAGE)/jetpack
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/firebug
|
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/peptest
|
$(NSINSTALL) -D $(PKG_STAGE)/peptest
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/mozbase
|
$(NSINSTALL) -D $(PKG_STAGE)/mozbase
|
||||||
$(NSINSTALL) -D $(PKG_STAGE)/modules
|
$(NSINSTALL) -D $(PKG_STAGE)/modules
|
||||||
@ -379,9 +377,6 @@ stage-android: make-stage-dir
|
|||||||
stage-jetpack: make-stage-dir
|
stage-jetpack: make-stage-dir
|
||||||
$(NSINSTALL) $(topsrcdir)/testing/jetpack/jetpack-location.txt $(PKG_STAGE)/jetpack
|
$(NSINSTALL) $(topsrcdir)/testing/jetpack/jetpack-location.txt $(PKG_STAGE)/jetpack
|
||||||
|
|
||||||
stage-firebug: make-stage-dir
|
|
||||||
$(MAKE) -C $(DEPTH)/testing/firebug stage-package
|
|
||||||
|
|
||||||
stage-peptest: make-stage-dir
|
stage-peptest: make-stage-dir
|
||||||
$(MAKE) -C $(DEPTH)/testing/peptest stage-package
|
$(MAKE) -C $(DEPTH)/testing/peptest stage-package
|
||||||
|
|
||||||
@ -426,7 +421,6 @@ stage-mozbase: make-stage-dir
|
|||||||
stage-jstests \
|
stage-jstests \
|
||||||
stage-android \
|
stage-android \
|
||||||
stage-jetpack \
|
stage-jetpack \
|
||||||
stage-firebug \
|
|
||||||
stage-peptest \
|
stage-peptest \
|
||||||
stage-mozbase \
|
stage-mozbase \
|
||||||
stage-tps \
|
stage-tps \
|
||||||
|
@ -892,7 +892,6 @@ if [ "$ENABLE_TESTS" ]; then
|
|||||||
services/crypto/component/tests/Makefile
|
services/crypto/component/tests/Makefile
|
||||||
startupcache/test/Makefile
|
startupcache/test/Makefile
|
||||||
storage/test/Makefile
|
storage/test/Makefile
|
||||||
testing/firebug/Makefile
|
|
||||||
testing/mochitest/Makefile
|
testing/mochitest/Makefile
|
||||||
testing/mochitest/MochiKit/Makefile
|
testing/mochitest/MochiKit/Makefile
|
||||||
testing/mochitest/chrome/Makefile
|
testing/mochitest/chrome/Makefile
|
||||||
|
Loading…
Reference in New Issue
Block a user