build/automation.py.in
author Olli Pettay <Olli.Pettay@helsinki.fi>
Tue, 04 May 2010 12:46:37 +0300
changeset 41756 d6bb0f9e951926d3f4f0d8ee3e70d80b408f6411
parent 41684 8a7a28c65fbd68f75f7dddc50d5d96840d26d9e8
child 42054 98501168f5febac77459e114834ac534c5b3e43b
permissions -rw-r--r--
backout Bug 562321

#
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is mozilla.org code.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2008
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
#   Robert Sayre <sayrer@gmail.com>
#   Jeff Walden <jwalden+bmo@mit.edu>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****

import codecs
from datetime import datetime, timedelta
import itertools
import logging
import os
import re
import select
import shutil
import signal
import subprocess
import sys
import threading
import tempfile

SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
sys.path.insert(0, SCRIPT_DIR)
import automationutils

_DEFAULT_WEB_SERVER = "127.0.0.1"
_DEFAULT_HTTP_PORT = 8888
_DEFAULT_SSL_PORT = 4443

#expand _DIST_BIN = __XPC_BIN_PATH__
#expand _IS_WIN32 = len("__WIN32__") != 0
#expand _IS_MAC = __IS_MAC__ != 0
#expand _IS_LINUX = __IS_LINUX__ != 0
#ifdef IS_CYGWIN
#expand _IS_CYGWIN = __IS_CYGWIN__ == 1
#else
_IS_CYGWIN = False
#endif
#expand _IS_CAMINO = __IS_CAMINO__ != 0
#expand _BIN_SUFFIX = __BIN_SUFFIX__
#expand _PERL = __PERL__

#expand _DEFAULT_APP = "./" + __BROWSER_PATH__
#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
#expand _IS_TEST_BUILD = __IS_TEST_BUILD__
#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
#expand _CRASHREPORTER = __CRASHREPORTER__ == 1


if _IS_WIN32:
  import ctypes, ctypes.wintypes, time, msvcrt
else:
  import errno


# We use the logging system here primarily because it'll handle multiple
# threads, which is needed to process the output of the server and application
# processes simultaneously.
_log = logging.getLogger()
handler = logging.StreamHandler(sys.stdout)
_log.setLevel(logging.INFO)
_log.addHandler(handler)


#################
# PROFILE SETUP #
#################

class SyntaxError(Exception):
  "Signifies a syntax error on a particular line in server-locations.txt."

  def __init__(self, lineno, msg = None):
    self.lineno = lineno
    self.msg = msg

  def __str__(self):
    s = "Syntax error on line " + str(self.lineno)
    if self.msg:
      s += ": %s." % self.msg
    else:
      s += "."
    return s


class Location:
  "Represents a location line in server-locations.txt."

  def __init__(self, scheme, host, port, options):
    self.scheme = scheme
    self.host = host
    self.port = port
    self.options = options

class Automation(object):
  """
  Runs the browser from a script, and provides useful utilities
  for setting up the browser environment.
  """

  DIST_BIN = _DIST_BIN
  IS_WIN32 = _IS_WIN32
  IS_MAC = _IS_MAC
  IS_LINUX = _IS_LINUX
  IS_CYGWIN = _IS_CYGWIN
  IS_CAMINO = _IS_CAMINO
  BIN_SUFFIX = _BIN_SUFFIX
  PERL = _PERL

  UNIXISH = not IS_WIN32 and not IS_MAC

  DEFAULT_APP = _DEFAULT_APP
  CERTS_SRC_DIR = _CERTS_SRC_DIR
  IS_TEST_BUILD = _IS_TEST_BUILD
  IS_DEBUG_BUILD = _IS_DEBUG_BUILD
  CRASHREPORTER = _CRASHREPORTER

  # timeout, in seconds
  DEFAULT_TIMEOUT = 60.0
  DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
  DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
  DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT

  def __init__(self):
    self.log = _log

  def setServerInfo(self, webServer = _DEFAULT_WEB_SERVER, httpPort = _DEFAULT_HTTP_PORT, sslPort = _DEFAULT_SSL_PORT):
    self.webServer = webServer
    self.httpPort = httpPort
    self.sslPort = sslPort

  @property
  def __all__(self):
    return [
           "UNIXISH",
           "IS_WIN32",
           "IS_MAC",
           "log",
           "runApp",
           "Process",
           "addCommonOptions",
           "initializeProfile",
           "DIST_BIN",
           "DEFAULT_APP",
           "CERTS_SRC_DIR",
           "environment",
           "IS_TEST_BUILD",
           "IS_DEBUG_BUILD",
           "DEFAULT_TIMEOUT",
          ]

  class Process(subprocess.Popen):
    """
    Represents our view of a subprocess.
    It adds a kill() method which allows it to be stopped explicitly.
    """

    def __init__(self,
                 args,
                 bufsize=0,
                 executable=None,
                 stdin=None,
                 stdout=None,
                 stderr=None,
                 preexec_fn=None,
                 close_fds=False,
                 shell=False,
                 cwd=None,
                 env=None,
                 universal_newlines=False,
                 startupinfo=None,
                 creationflags=0):
      subprocess.Popen.__init__(self, args, bufsize, executable,
                                stdin, stdout, stderr,
                                preexec_fn, close_fds,
                                shell, cwd, env,
                                universal_newlines, startupinfo, creationflags)
      self.log = _log

    def kill(self):
      if Automation().IS_WIN32:
        import platform
        pid = "%i" % self.pid
        if platform.release() == "2000":
          # Windows 2000 needs 'kill.exe' from the 
          #'Windows 2000 Resource Kit tools'. (See bug 475455.)
          try:
            subprocess.Popen(["kill", "-f", pid]).wait()
          except:
            self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
        else:
          # Windows XP and later.
          subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
      else:
        os.kill(self.pid, signal.SIGKILL)

  def readLocations(self, locationsPath = "server-locations.txt"):
    """
    Reads the locations at which the Mochitest HTTP server is available from
    server-locations.txt.
    """

    locationFile = codecs.open(locationsPath, "r", "UTF-8")

    # Perhaps more detail than necessary, but it's the easiest way to make sure
    # we get exactly the format we want.  See server-locations.txt for the exact
    # format guaranteed here.
    lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
                      r"://"
                      r"(?P<host>"
                        r"\d+\.\d+\.\d+\.\d+"
                        r"|"
                        r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
                        r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
                      r")"
                      r":"
                      r"(?P<port>\d+)"
                      r"(?:"
                      r"\s+"
                      r"(?P<options>\S+(?:,\S+)*)"
                      r")?$")
    locations = []
    lineno = 0
    seenPrimary = False
    for line in locationFile:
      lineno += 1
      if line.startswith("#") or line == "\n":
        continue
      
      match = lineRe.match(line)
      if not match:
        raise SyntaxError(lineno)

      options = match.group("options")
      if options:
        options = options.split(",")
        if "primary" in options:
          if seenPrimary:
            raise SyntaxError(lineno, "multiple primary locations")
          seenPrimary = True
      else:
        options = []

      locations.append(Location(match.group("scheme"), match.group("host"),
                                match.group("port"), options))

    if not seenPrimary:
      raise SyntaxError(lineno + 1, "missing primary location")

    return locations

  def initializeProfile(self, profileDir, extraPrefs = [], useServerLocations = False):
    " Sets up the standard testing profile."

    prefs = []
    # Start with a clean slate.
    shutil.rmtree(profileDir, True)
    os.mkdir(profileDir)

    part = """\
user_pref("browser.dom.window.dump.enabled", true);
user_pref("dom.allow_scripts_to_close_windows", true);
user_pref("dom.disable_open_during_load", false);
user_pref("dom.max_script_run_time", 0); // no slow script dialogs
user_pref("dom.max_chrome_script_run_time", 0);
user_pref("dom.popup_maximum", -1);
user_pref("signed.applets.codebase_principal_support", true);
user_pref("security.warn_submit_insecure", false);
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("shell.checkDefaultClient", false);
user_pref("browser.warnOnQuit", false);
user_pref("accessibility.typeaheadfind.autostart", false);
user_pref("javascript.options.showInConsole", true);
user_pref("layout.debug.enable_data_xbl", true);
user_pref("browser.EULA.override", true);
user_pref("javascript.options.jit.content", true);
user_pref("gfx.color_management.force_srgb", true);
user_pref("network.manage-offline-status", false);
user_pref("test.mousescroll", true);
user_pref("security.default_personal_cert", "Select Automatically"); // Need to client auth test be w/o any dialogs
user_pref("network.http.prompt-temp-redirect", false);
user_pref("media.cache_size", 100);
user_pref("security.warn_viewing_mixed", false);

user_pref("geo.wifi.uri", "http://%(server)s/tests/dom/tests/mochitest/geolocation/network_geolocation.sjs");
user_pref("geo.wifi.testing", true);

user_pref("camino.warn_when_closing", false); // Camino-only, harmless to others

// Make url-classifier updates so rare that they won't affect tests
user_pref("urlclassifier.updateinterval", 172800);
// Point the url-classifier to the local testing server for fast failures
user_pref("browser.safebrowsing.provider.0.gethashURL", "http://%(server)s/safebrowsing-dummy/gethash");
user_pref("browser.safebrowsing.provider.0.keyURL", "http://%(server)s/safebrowsing-dummy/newkey");
user_pref("browser.safebrowsing.provider.0.updateURL", "http://%(server)s/safebrowsing-dummy/update");
""" % { "server" : self.webServer + ":" + str(self.httpPort) }
    prefs.append(part)

    if useServerLocations == False:
      part = """
user_pref("capability.principal.codebase.p1.granted",
          "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
           UniversalPreferencesRead UniversalPreferencesWrite \
           UniversalFileRead");
user_pref("capability.principal.codebase.p1.id", "%(origin)s");
user_pref("capability.principal.codebase.p1.subjectName", "");
"""  % { "origin": "http://" + self.webServer + ":" + str(self.httpPort) }
      prefs.append(part)
    else:
      locations = self.readLocations()

      # Grant God-power to all the privileged servers on which tests run.
      privileged = filter(lambda loc: "privileged" in loc.options, locations)
      for (i, l) in itertools.izip(itertools.count(1), privileged):
        part = """
user_pref("capability.principal.codebase.p%(i)d.granted",
          "UniversalXPConnect UniversalBrowserRead UniversalBrowserWrite \
           UniversalPreferencesRead UniversalPreferencesWrite \
           UniversalFileRead");
user_pref("capability.principal.codebase.p%(i)d.id", "%(origin)s");
user_pref("capability.principal.codebase.p%(i)d.subjectName", "");
"""  % { "i": i,
         "origin": (l.scheme + "://" + l.host + ":" + str(l.port)) }
        prefs.append(part)

      # We need to proxy every server but the primary one.
      origins = ["'%s://%s:%s'" % (l.scheme, l.host, l.port)
                for l in filter(lambda l: "primary" not in l.options, locations)]
      origins = ", ".join(origins)

      pacURL = """data:text/plain,
function FindProxyForURL(url, host)
{
  var origins = [%(origins)s];
  var regex = new RegExp('^([a-z][-a-z0-9+.]*)' +
                         '://' +
                         '(?:[^/@]*@)?' +
                         '(.*?)' +
                         '(?::(\\\\\\\\d+))?/');
  var matches = regex.exec(url);
  if (!matches)
    return 'DIRECT';
  var isHttp = matches[1] == 'http';
  var isHttps = matches[1] == 'https';
  if (!matches[3])
  {
    if (isHttp) matches[3] = '80';
    if (isHttps) matches[3] = '443';
  }
    
  var origin = matches[1] + '://' + matches[2] + ':' + matches[3];
  if (origins.indexOf(origin) < 0)
    return 'DIRECT';
  if (isHttp)
    return 'PROXY %(remote)s:%(httpport)s';
  if (isHttps)
    return 'PROXY %(remote)s:%(sslport)s';
  return 'DIRECT';
}""" % { "origins": origins,
         "remote":  self.webServer,
         "httpport":self.httpPort,
         "sslport": self.sslPort }
      pacURL = "".join(pacURL.splitlines())

      part += """
user_pref("network.proxy.type", 2);
user_pref("network.proxy.autoconfig_url", "%(pacURL)s");

user_pref("camino.use_system_proxy_settings", false); // Camino-only, harmless to others
""" % {"pacURL": pacURL}
      prefs.append(part)

    for v in extraPrefs:
      thispref = v.split("=")
      if len(thispref) < 2:
        print "Error: syntax error in --setpref=" + v
        sys.exit(1)
      part = 'user_pref("%s", %s);\n' % (thispref[0], thispref[1])
      prefs.append(part)

    # write the preferences
    prefsFile = open(profileDir + "/" + "user.js", "a")
    prefsFile.write("".join(prefs))
    prefsFile.close()

  def addCommonOptions(self, parser):
    "Adds command-line options which are common to mochitest and reftest."

    parser.add_option("--setpref",
                      action = "append", type = "string",
                      default = [],
                      dest = "extraPrefs", metavar = "PREF=VALUE",
                      help = "defines an extra user preference")  

  def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
    pwfilePath = os.path.join(profileDir, ".crtdbpw")
  
    pwfile = open(pwfilePath, "w")
    pwfile.write("\n")
    pwfile.close()

    # Create head of the ssltunnel configuration file
    sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
    sslTunnelConfig = open(sslTunnelConfigPath, "w")
  
    sslTunnelConfig.write("httpproxy:1\n")
    sslTunnelConfig.write("certdbdir:%s\n" % certPath)
    sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
    sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)

    # Configure automatic certificate and bind custom certificates, client authentication
    locations = self.readLocations()
    locations.pop(0)
    for loc in locations:
      if loc.scheme == "https" and "nocert" not in loc.options:
        customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
        clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
        for option in loc.options:
          match = customCertRE.match(option)
          if match:
            customcert = match.group("nickname");
            sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
                      (loc.host, loc.port, self.sslPort, customcert))

          match = clientAuthRE.match(option)
          if match:
            clientauth = match.group("clientauth");
            sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
                      (loc.host, loc.port, self.sslPort, clientauth))

    sslTunnelConfig.close()

    # Pre-create the certification database for the profile
    env = self.environment(xrePath = xrePath)
    certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
    pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)

    status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
    if status != 0:
      return status

    # Walk the cert directory and add custom CAs and client certs
    files = os.listdir(certPath)
    for item in files:
      root, ext = os.path.splitext(item)
      if ext == ".ca":
        trustBits = "CT,,"
        if root.endswith("-object"):
          trustBits = "CT,,CT"
        self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
                    "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
                    env = env).wait()
      if ext == ".client":
        self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
                    pwfilePath, "-d", profileDir], 
                    env = env).wait()

    os.unlink(pwfilePath)
    return 0

  def environment(self, env = None, xrePath = None, crashreporter = True):
    if xrePath == None:
      xrePath = self.DIST_BIN
    if env == None:
      env = dict(os.environ)

    ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
    if self.UNIXISH or self.IS_MAC:
      envVar = "LD_LIBRARY_PATH"
      if self.IS_MAC:
        envVar = "DYLD_LIBRARY_PATH"
      else: # unixish
        env['MOZILLA_FIVE_HOME'] = xrePath
      if envVar in env:
        ldLibraryPath = ldLibraryPath + ":" + env[envVar]
      env[envVar] = ldLibraryPath
    elif self.IS_WIN32:
      env["PATH"] = env["PATH"] + ";" + ldLibraryPath

    if crashreporter:
      env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
      env['MOZ_CRASHREPORTER'] = '1'
    else:
      env['MOZ_CRASHREPORTER_DISABLE'] = '1'

    env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
    env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
    return env

  if IS_WIN32:
    PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
    GetLastError = ctypes.windll.kernel32.GetLastError

    def readWithTimeout(self, f, timeout):
      """Try to read a line of output from the file object |f|.
      |f| must be a  pipe, like the |stdout| member of a subprocess.Popen
      object created with stdout=PIPE. If no output
      is received within |timeout| seconds, return a blank line.
      Returns a tuple (line, did_timeout), where |did_timeout| is True
      if the read timed out, and False otherwise."""
      if timeout is None:
        # shortcut to allow callers to pass in "None" for no timeout.
        return (f.readline(), False)
      x = msvcrt.get_osfhandle(f.fileno())
      l = ctypes.c_long()
      done = time.time() + timeout
      while time.time() < done:
        if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
          err = self.GetLastError()
          if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
            return ('', False)
          else:
            log.error("readWithTimeout got error: %d", err)
        if l.value > 0:
          # we're assuming that the output is line-buffered,
          # which is not unreasonable
          return (f.readline(), False)
        time.sleep(0.01)
      return ('', True)

    def isPidAlive(self, pid):
      STILL_ACTIVE = 259
      PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
      pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
      if not pHandle:
        return False
      pExitCode = ctypes.wintypes.DWORD()
      ctypes.windll.kernel32.GetExitCodeProcess(pHandle, self.ctypes.byref(pExitCode))
      ctypes.windll.kernel32.CloseHandle(pHandle)
      if (pExitCode.value == STILL_ACTIVE):
        return True
      else:
        return False

    def killPid(self, pid):
      PROCESS_TERMINATE = 0x0001
      pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_TERMINATE, 0, pid)
      if not pHandle:
        return
      success = ctypes.windll.kernel32.TerminateProcess(pHandle, 1)
      ctypes.windll.kernel32.CloseHandle(pHandle)

  else:

    def readWithTimeout(self, f, timeout):
      """Try to read a line of output from the file object |f|. If no output
      is received within |timeout| seconds, return a blank line.
      Returns a tuple (line, did_timeout), where |did_timeout| is True
      if the read timed out, and False otherwise."""
      (r, w, e) = select.select([f], [], [], timeout)
      if len(r) == 0:
        return ('', True)
      return (f.readline(), False)

    def isPidAlive(self, pid):
      try:
        # kill(pid, 0) checks for a valid PID without actually sending a signal
        # The method throws OSError if the PID is invalid, which we catch below.
        os.kill(pid, 0)

        # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
        # the process terminates before we get to this point.
        wpid, wstatus = os.waitpid(pid, os.WNOHANG)
        if wpid == 0:
          return True

        return False
      except OSError, err:
        # Catch the errors we might expect from os.kill/os.waitpid, 
        # and re-raise any others
        if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
          return False
        raise

    def killPid(self, pid):
      os.kill(pid, signal.SIGKILL)

  def killAndGetStack(self, proc, utilityPath, debuggerInfo):
    """Kill the process, preferrably in a way that gets us a stack trace."""
    if self.CRASHREPORTER and not debuggerInfo:
      if self.UNIXISH:
        # ABRT will get picked up by Breakpad's signal handler
        os.kill(proc.pid, signal.SIGABRT)
        return
      elif self.IS_WIN32:
        # We should have a "crashinject" program in our utility path
        crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
        if os.path.exists(crashinject) and subprocess.Popen([crashinject, str(proc.pid)]).wait() == 0:
          return
      #TODO: kill the process such that it triggers Breakpad on OS X (bug 525296)
    self.log.info("Can't trigger Breakpad, just killing process")
    proc.kill()

  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo):
    """ Look for timeout or crashes and return the status after the process terminates """
    stackFixerProcess = None
    stackFixerModule = None
    didTimeout = False
    if proc.stdout is None:
      self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
    else:
      logsource = proc.stdout
      if self.IS_DEBUG_BUILD and self.IS_LINUX:
        # Run logsource through fix-linux-stack.pl
        stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
                                         stdin=logsource,
                                         stdout=subprocess.PIPE)
        logsource = stackFixerProcess.stdout

      if self.IS_DEBUG_BUILD and self.IS_MAC:
        # Import fix_macosx_stack.py from utilityPath
        sys.path.insert(0, utilityPath)
        import fix_macosx_stack as stackFixerModule
        del sys.path[0]

      (line, didTimeout) = self.readWithTimeout(logsource, timeout)
      hitMaxTime = False
      while line != "" and not didTimeout:
        if stackFixerModule:
          line = stackFixerModule.fixSymbols(line)
        self.log.info(line.rstrip())
        (line, didTimeout) = self.readWithTimeout(logsource, timeout)
        if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
          # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
          hitMaxTime = True
          self.log.info("TEST-UNEXPECTED-FAIL | automation.py | application ran for longer than allowed maximum time of %d seconds", int(maxTime))
          self.killAndGetStack(proc, utilityPath, debuggerInfo)
      if didTimeout:
        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | application timed out after %d seconds with no output", int(timeout))
        self.killAndGetStack(proc, utilityPath, debuggerInfo)

    status = proc.wait()
    if status != 0 and not didTimeout and not hitMaxTime:
      self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Exited with code %d during test run", status)
    if stackFixerProcess is not None:
      fixerStatus = stackFixerProcess.wait()
      if fixerStatus != 0 and not didTimeout and not hitMaxTime:
        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
    return status

  def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
    """ build the application command line """

    cmd = app
    if self.IS_MAC and not self.IS_CAMINO and not cmd.endswith("-bin"):
      cmd += "-bin"
    cmd = os.path.abspath(cmd)

    args = []

    if debuggerInfo:
      args.extend(debuggerInfo["args"])
      args.append(cmd)
      cmd = os.path.abspath(debuggerInfo["path"])

    if self.IS_MAC:
      args.append("-foreground")

    if self.IS_CYGWIN:
      profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
    else:
      profileDirectory = profileDir + "/"

    args.extend(("-no-remote", "-profile", profileDirectory))
    if testURL is not None:
      if self.IS_CAMINO:
        args.extend(("-url", testURL))
      else:
        args.append((testURL))
    args.extend(extraArgs)
    return cmd, args

  def checkForZombies(self, processLog):
    """ Look for hung processes """
    if not os.path.exists(processLog):
      self.log.info('INFO | automation.py | PID log not found: %s', processLog)
    else:
      self.log.info('INFO | automation.py | Reading PID log: %s', processLog)
      processList = []
      pidRE = re.compile(r'launched child process (\d+)$')
      processLogFD = open(processLog)
      for line in processLogFD:
        self.log.info(line.rstrip())
        m = pidRE.search(line)
        if m:
          processList.append(int(m.group(1)))
      processLogFD.close()

      for processPID in processList:
        self.log.info("INFO | automation.py | Checking for orphan process with PID: %d", processPID)
        if self.isPidAlive(processPID):
          self.log.info("TEST-UNEXPECTED-FAIL | automation.py | child process %d still alive after shutdown", processPID)
          self.killPid(processPID)

  def runApp(self, testURL, env, app, profileDir, extraArgs,
             runSSLTunnel = False, utilityPath = None,
             xrePath = None, certPath = None,
             debuggerInfo = None, symbolsPath = None,
             timeout = -1, maxTime = None):
    """
    Run the app, log the duration it took to execute, return the status code.
    Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
    """

    if utilityPath == None:
      utilityPath = self.DIST_BIN
    if xrePath == None:
      xrePath = self.DIST_BIN
    if certPath == None:
      certPath = self.CERTS_SRC_DIR
    if timeout == -1:
      timeout = self.DEFAULT_TIMEOUT

    # copy env so we don't munge the caller's environment
    env = dict(env);
    env["NO_EM_RESTART"] = "1"
    tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
    os.close(tmpfd)
    env["MOZ_PROCESS_LOG"] = processLog

    if self.IS_TEST_BUILD and runSSLTunnel:
      # create certificate database for the profile
      certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
      if certificateStatus != 0:
        self.log.info("TEST-UNEXPECTED FAIL | automation.py | Certificate integration failed")
        return certificateStatus

      # start ssltunnel to provide https:// URLs capability
      ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
      ssltunnelProcess = self.Process([ssltunnel, 
                               os.path.join(profileDir, "ssltunnel.cfg")], 
                               env = self.environment(xrePath = xrePath))
      self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)

    cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
    startTime = datetime.now()

    if debuggerInfo and debuggerInfo["interactive"]:
      # If an interactive debugger is attached, don't redirect output
      # and don't use timeouts.
      timeout = None
      maxTime = None
      outputPipe = None
    else:
      outputPipe = subprocess.PIPE

    proc = self.Process([cmd] + args,
                 env = self.environment(env, xrePath = xrePath,
                                   crashreporter = not debuggerInfo),
                 stdout = outputPipe,
                 stderr = subprocess.STDOUT)
    self.log.info("INFO | automation.py | Application pid: %d", proc.pid)

    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo)
    self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))

    # Do a final check for zombie child processes.
    self.checkForZombies(processLog)
    automationutils.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath)

    if os.path.exists(processLog):
      os.unlink(processLog)

    if self.IS_TEST_BUILD and runSSLTunnel:
      ssltunnelProcess.kill()

    return status