Bug 685903 - Remove firebug automation from testing/firebug. r=jhammel

This commit is contained in:
Andrew Halberstadt 2012-09-26 11:07:07 -04:00
parent 709a50e38b
commit 337c6cc0d9
20 changed files with 0 additions and 2119 deletions

View File

@ -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 -)

View File

@ -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

View File

@ -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())

View File

@ -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()

View File

@ -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/.
#

View File

@ -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)

View File

@ -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

View File

@ -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!"

View File

@ -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()

View File

@ -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."

View File

@ -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: -*-
""",
)

View File

@ -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 *

View File

@ -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)

View File

@ -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
""",
)

View File

@ -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 *

View File

@ -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()

View File

@ -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)

View File

@ -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',
]
)

View File

@ -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 \

View File

@ -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