build/automation.py.in

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/build/automation.py.in	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,988 @@
     1.4 +#
     1.5 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.6 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.7 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.8 +
     1.9 +from __future__ import with_statement
    1.10 +import codecs
    1.11 +import itertools
    1.12 +import json
    1.13 +import logging
    1.14 +import os
    1.15 +import re
    1.16 +import select
    1.17 +import shutil
    1.18 +import signal
    1.19 +import subprocess
    1.20 +import sys
    1.21 +import threading
    1.22 +import tempfile
    1.23 +import sqlite3
    1.24 +from datetime import datetime, timedelta
    1.25 +from string import Template
    1.26 +
    1.27 +SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
    1.28 +sys.path.insert(0, SCRIPT_DIR)
    1.29 +import automationutils
    1.30 +
    1.31 +# --------------------------------------------------------------
    1.32 +# TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
    1.33 +# These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR
    1.34 +here = os.path.dirname(os.path.realpath(__file__))
    1.35 +mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
    1.36 +
    1.37 +if os.path.isdir(mozbase):
    1.38 +    for package in os.listdir(mozbase):
    1.39 +        package_path = os.path.join(mozbase, package)
    1.40 +        if package_path not in sys.path:
    1.41 +            sys.path.append(package_path)
    1.42 +
    1.43 +import mozcrash
    1.44 +from mozprofile import Profile, Preferences
    1.45 +from mozprofile.permissions import ServerLocations
    1.46 +
    1.47 +# ---------------------------------------------------------------
    1.48 +
    1.49 +_DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js')
    1.50 +_DEFAULT_APPS_FILE = os.path.join(SCRIPT_DIR, 'webapps_mochitest.json')
    1.51 +
    1.52 +_DEFAULT_WEB_SERVER = "127.0.0.1"
    1.53 +_DEFAULT_HTTP_PORT = 8888
    1.54 +_DEFAULT_SSL_PORT = 4443
    1.55 +_DEFAULT_WEBSOCKET_PORT = 9988
    1.56 +
    1.57 +# from nsIPrincipal.idl
    1.58 +_APP_STATUS_NOT_INSTALLED = 0
    1.59 +_APP_STATUS_INSTALLED     = 1
    1.60 +_APP_STATUS_PRIVILEGED    = 2
    1.61 +_APP_STATUS_CERTIFIED     = 3
    1.62 +
    1.63 +#expand _DIST_BIN = __XPC_BIN_PATH__
    1.64 +#expand _IS_WIN32 = len("__WIN32__") != 0
    1.65 +#expand _IS_MAC = __IS_MAC__ != 0
    1.66 +#expand _IS_LINUX = __IS_LINUX__ != 0
    1.67 +#ifdef IS_CYGWIN
    1.68 +#expand _IS_CYGWIN = __IS_CYGWIN__ == 1
    1.69 +#else
    1.70 +_IS_CYGWIN = False
    1.71 +#endif
    1.72 +#expand _IS_CAMINO = __IS_CAMINO__ != 0
    1.73 +#expand _BIN_SUFFIX = __BIN_SUFFIX__
    1.74 +#expand _PERL = __PERL__
    1.75 +
    1.76 +#expand _DEFAULT_APP = "./" + __BROWSER_PATH__
    1.77 +#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
    1.78 +#expand _IS_TEST_BUILD = __IS_TEST_BUILD__
    1.79 +#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
    1.80 +#expand _CRASHREPORTER = __CRASHREPORTER__ == 1
    1.81 +#expand _IS_ASAN = __IS_ASAN__ == 1
    1.82 +
    1.83 +
    1.84 +if _IS_WIN32:
    1.85 +  import ctypes, ctypes.wintypes, time, msvcrt
    1.86 +else:
    1.87 +  import errno
    1.88 +
    1.89 +
    1.90 +def getGlobalLog():
    1.91 +  return _log
    1.92 +
    1.93 +def resetGlobalLog(log):
    1.94 +  while _log.handlers:
    1.95 +    _log.removeHandler(_log.handlers[0])
    1.96 +  handler = logging.StreamHandler(log)
    1.97 +  _log.setLevel(logging.INFO)
    1.98 +  _log.addHandler(handler)
    1.99 +
   1.100 +# We use the logging system here primarily because it'll handle multiple
   1.101 +# threads, which is needed to process the output of the server and application
   1.102 +# processes simultaneously.
   1.103 +_log = logging.getLogger()
   1.104 +resetGlobalLog(sys.stdout)
   1.105 +
   1.106 +
   1.107 +#################
   1.108 +# PROFILE SETUP #
   1.109 +#################
   1.110 +
   1.111 +class SyntaxError(Exception):
   1.112 +  "Signifies a syntax error on a particular line in server-locations.txt."
   1.113 +
   1.114 +  def __init__(self, lineno, msg = None):
   1.115 +    self.lineno = lineno
   1.116 +    self.msg = msg
   1.117 +
   1.118 +  def __str__(self):
   1.119 +    s = "Syntax error on line " + str(self.lineno)
   1.120 +    if self.msg:
   1.121 +      s += ": %s." % self.msg
   1.122 +    else:
   1.123 +      s += "."
   1.124 +    return s
   1.125 +
   1.126 +
   1.127 +class Location:
   1.128 +  "Represents a location line in server-locations.txt."
   1.129 +
   1.130 +  def __init__(self, scheme, host, port, options):
   1.131 +    self.scheme = scheme
   1.132 +    self.host = host
   1.133 +    self.port = port
   1.134 +    self.options = options
   1.135 +
   1.136 +class Automation(object):
   1.137 +  """
   1.138 +  Runs the browser from a script, and provides useful utilities
   1.139 +  for setting up the browser environment.
   1.140 +  """
   1.141 +
   1.142 +  DIST_BIN = _DIST_BIN
   1.143 +  IS_WIN32 = _IS_WIN32
   1.144 +  IS_MAC = _IS_MAC
   1.145 +  IS_LINUX = _IS_LINUX
   1.146 +  IS_CYGWIN = _IS_CYGWIN
   1.147 +  IS_CAMINO = _IS_CAMINO
   1.148 +  BIN_SUFFIX = _BIN_SUFFIX
   1.149 +  PERL = _PERL
   1.150 +
   1.151 +  UNIXISH = not IS_WIN32 and not IS_MAC
   1.152 +
   1.153 +  DEFAULT_APP = _DEFAULT_APP
   1.154 +  CERTS_SRC_DIR = _CERTS_SRC_DIR
   1.155 +  IS_TEST_BUILD = _IS_TEST_BUILD
   1.156 +  IS_DEBUG_BUILD = _IS_DEBUG_BUILD
   1.157 +  CRASHREPORTER = _CRASHREPORTER
   1.158 +  IS_ASAN = _IS_ASAN
   1.159 +
   1.160 +  # timeout, in seconds
   1.161 +  DEFAULT_TIMEOUT = 60.0
   1.162 +  DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
   1.163 +  DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
   1.164 +  DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
   1.165 +  DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
   1.166 +
   1.167 +  def __init__(self):
   1.168 +    self.log = _log
   1.169 +    self.lastTestSeen = "automation.py"
   1.170 +    self.haveDumpedScreen = False
   1.171 +
   1.172 +  def setServerInfo(self, 
   1.173 +                    webServer = _DEFAULT_WEB_SERVER, 
   1.174 +                    httpPort = _DEFAULT_HTTP_PORT, 
   1.175 +                    sslPort = _DEFAULT_SSL_PORT,
   1.176 +                    webSocketPort = _DEFAULT_WEBSOCKET_PORT):
   1.177 +    self.webServer = webServer
   1.178 +    self.httpPort = httpPort
   1.179 +    self.sslPort = sslPort
   1.180 +    self.webSocketPort = webSocketPort
   1.181 +
   1.182 +  @property
   1.183 +  def __all__(self):
   1.184 +    return [
   1.185 +           "UNIXISH",
   1.186 +           "IS_WIN32",
   1.187 +           "IS_MAC",
   1.188 +           "log",
   1.189 +           "runApp",
   1.190 +           "Process",
   1.191 +           "addCommonOptions",
   1.192 +           "initializeProfile",
   1.193 +           "DIST_BIN",
   1.194 +           "DEFAULT_APP",
   1.195 +           "CERTS_SRC_DIR",
   1.196 +           "environment",
   1.197 +           "IS_TEST_BUILD",
   1.198 +           "IS_DEBUG_BUILD",
   1.199 +           "DEFAULT_TIMEOUT",
   1.200 +          ]
   1.201 +
   1.202 +  class Process(subprocess.Popen):
   1.203 +    """
   1.204 +    Represents our view of a subprocess.
   1.205 +    It adds a kill() method which allows it to be stopped explicitly.
   1.206 +    """
   1.207 +
   1.208 +    def __init__(self,
   1.209 +                 args,
   1.210 +                 bufsize=0,
   1.211 +                 executable=None,
   1.212 +                 stdin=None,
   1.213 +                 stdout=None,
   1.214 +                 stderr=None,
   1.215 +                 preexec_fn=None,
   1.216 +                 close_fds=False,
   1.217 +                 shell=False,
   1.218 +                 cwd=None,
   1.219 +                 env=None,
   1.220 +                 universal_newlines=False,
   1.221 +                 startupinfo=None,
   1.222 +                 creationflags=0):
   1.223 +      _log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(args))
   1.224 +      subprocess.Popen.__init__(self, args, bufsize, executable,
   1.225 +                                stdin, stdout, stderr,
   1.226 +                                preexec_fn, close_fds,
   1.227 +                                shell, cwd, env,
   1.228 +                                universal_newlines, startupinfo, creationflags)
   1.229 +      self.log = _log
   1.230 +
   1.231 +    def kill(self):
   1.232 +      if Automation().IS_WIN32:
   1.233 +        import platform
   1.234 +        pid = "%i" % self.pid
   1.235 +        if platform.release() == "2000":
   1.236 +          # Windows 2000 needs 'kill.exe' from the 
   1.237 +          #'Windows 2000 Resource Kit tools'. (See bug 475455.)
   1.238 +          try:
   1.239 +            subprocess.Popen(["kill", "-f", pid]).wait()
   1.240 +          except:
   1.241 +            self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
   1.242 +        else:
   1.243 +          # Windows XP and later.
   1.244 +          subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
   1.245 +      else:
   1.246 +        os.kill(self.pid, signal.SIGKILL)
   1.247 +
   1.248 +  def readLocations(self, locationsPath = "server-locations.txt"):
   1.249 +    """
   1.250 +    Reads the locations at which the Mochitest HTTP server is available from
   1.251 +    server-locations.txt.
   1.252 +    """
   1.253 +
   1.254 +    locationFile = codecs.open(locationsPath, "r", "UTF-8")
   1.255 +
   1.256 +    # Perhaps more detail than necessary, but it's the easiest way to make sure
   1.257 +    # we get exactly the format we want.  See server-locations.txt for the exact
   1.258 +    # format guaranteed here.
   1.259 +    lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
   1.260 +                      r"://"
   1.261 +                      r"(?P<host>"
   1.262 +                        r"\d+\.\d+\.\d+\.\d+"
   1.263 +                        r"|"
   1.264 +                        r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
   1.265 +                        r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
   1.266 +                      r")"
   1.267 +                      r":"
   1.268 +                      r"(?P<port>\d+)"
   1.269 +                      r"(?:"
   1.270 +                      r"\s+"
   1.271 +                      r"(?P<options>\S+(?:,\S+)*)"
   1.272 +                      r")?$")
   1.273 +    locations = []
   1.274 +    lineno = 0
   1.275 +    seenPrimary = False
   1.276 +    for line in locationFile:
   1.277 +      lineno += 1
   1.278 +      if line.startswith("#") or line == "\n":
   1.279 +        continue
   1.280 +      
   1.281 +      match = lineRe.match(line)
   1.282 +      if not match:
   1.283 +        raise SyntaxError(lineno)
   1.284 +
   1.285 +      options = match.group("options")
   1.286 +      if options:
   1.287 +        options = options.split(",")
   1.288 +        if "primary" in options:
   1.289 +          if seenPrimary:
   1.290 +            raise SyntaxError(lineno, "multiple primary locations")
   1.291 +          seenPrimary = True
   1.292 +      else:
   1.293 +        options = []
   1.294 +
   1.295 +      locations.append(Location(match.group("scheme"), match.group("host"),
   1.296 +                                match.group("port"), options))
   1.297 +
   1.298 +    if not seenPrimary:
   1.299 +      raise SyntaxError(lineno + 1, "missing primary location")
   1.300 +
   1.301 +    return locations
   1.302 +
   1.303 +  def setupPermissionsDatabase(self, profileDir, permissions):
   1.304 +    # Included for reftest compatibility;
   1.305 +    # see https://bugzilla.mozilla.org/show_bug.cgi?id=688667
   1.306 +
   1.307 +    # Open database and create table
   1.308 +    permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
   1.309 +    cursor = permDB.cursor();
   1.310 +
   1.311 +    cursor.execute("PRAGMA user_version=3");
   1.312 +
   1.313 +    # SQL copied from nsPermissionManager.cpp
   1.314 +    cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
   1.315 +      id INTEGER PRIMARY KEY,
   1.316 +      host TEXT,
   1.317 +      type TEXT,
   1.318 +      permission INTEGER,
   1.319 +      expireType INTEGER,
   1.320 +      expireTime INTEGER,
   1.321 +      appId INTEGER,
   1.322 +      isInBrowserElement INTEGER)""")
   1.323 +
   1.324 +    # Insert desired permissions
   1.325 +    for perm in permissions.keys():
   1.326 +      for host,allow in permissions[perm]:
   1.327 +        cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)",
   1.328 +                       (host, perm, 1 if allow else 2))
   1.329 +
   1.330 +    # Commit and close
   1.331 +    permDB.commit()
   1.332 +    cursor.close()
   1.333 +
   1.334 +  def initializeProfile(self, profileDir,
   1.335 +                              extraPrefs=None,
   1.336 +                              useServerLocations=False,
   1.337 +                              prefsPath=_DEFAULT_PREFERENCE_FILE,
   1.338 +                              appsPath=_DEFAULT_APPS_FILE,
   1.339 +                              addons=None):
   1.340 +    " Sets up the standard testing profile."
   1.341 +
   1.342 +    extraPrefs = extraPrefs or []
   1.343 +
   1.344 +    # create the profile
   1.345 +    prefs = {}
   1.346 +    locations = None
   1.347 +    if useServerLocations:
   1.348 +        locations = ServerLocations()
   1.349 +        locations.read(os.path.abspath('server-locations.txt'), True)
   1.350 +    else:
   1.351 +      prefs['network.proxy.type'] = 0
   1.352 +
   1.353 +    prefs.update(Preferences.read_prefs(prefsPath))
   1.354 +
   1.355 +    for v in extraPrefs:
   1.356 +      thispref = v.split("=", 1)
   1.357 +      if len(thispref) < 2:
   1.358 +        print "Error: syntax error in --setpref=" + v
   1.359 +        sys.exit(1)
   1.360 +      prefs[thispref[0]] = thispref[1]
   1.361 +
   1.362 +
   1.363 +    interpolation = {"server": "%s:%s" % (self.webServer, self.httpPort)}
   1.364 +    prefs = json.loads(json.dumps(prefs) % interpolation)
   1.365 +    for pref in prefs:
   1.366 +        prefs[pref] = Preferences.cast(prefs[pref])
   1.367 +
   1.368 +    # load apps
   1.369 +    apps = None
   1.370 +    if appsPath and os.path.exists(appsPath):
   1.371 +        with open(appsPath, 'r') as apps_file:
   1.372 +            apps = json.load(apps_file)
   1.373 +
   1.374 +    proxy = {'remote': str(self.webServer),
   1.375 +             'http': str(self.httpPort),
   1.376 +             'https': str(self.sslPort),
   1.377 +    # use SSL port for legacy compatibility; see
   1.378 +    # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
   1.379 +    # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
   1.380 +    #             'ws': str(self.webSocketPort)
   1.381 +             'ws': str(self.sslPort)
   1.382 +             }
   1.383 +
   1.384 +    # return profile object
   1.385 +    profile = Profile(profile=profileDir,
   1.386 +                      addons=addons,
   1.387 +                      locations=locations,
   1.388 +                      preferences=prefs,
   1.389 +                      restore=False,
   1.390 +                      apps=apps,
   1.391 +                      proxy=proxy)
   1.392 +    return profile
   1.393 +
   1.394 +  def addCommonOptions(self, parser):
   1.395 +    "Adds command-line options which are common to mochitest and reftest."
   1.396 +
   1.397 +    parser.add_option("--setpref",
   1.398 +                      action = "append", type = "string",
   1.399 +                      default = [],
   1.400 +                      dest = "extraPrefs", metavar = "PREF=VALUE",
   1.401 +                      help = "defines an extra user preference")
   1.402 +
   1.403 +  def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
   1.404 +    pwfilePath = os.path.join(profileDir, ".crtdbpw")
   1.405 +    pwfile = open(pwfilePath, "w")
   1.406 +    pwfile.write("\n")
   1.407 +    pwfile.close()
   1.408 +
   1.409 +    # Create head of the ssltunnel configuration file
   1.410 +    sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
   1.411 +    sslTunnelConfig = open(sslTunnelConfigPath, "w")
   1.412 +  
   1.413 +    sslTunnelConfig.write("httpproxy:1\n")
   1.414 +    sslTunnelConfig.write("certdbdir:%s\n" % certPath)
   1.415 +    sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
   1.416 +    sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
   1.417 +    sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
   1.418 +
   1.419 +    # Configure automatic certificate and bind custom certificates, client authentication
   1.420 +    locations = self.readLocations()
   1.421 +    locations.pop(0)
   1.422 +    for loc in locations:
   1.423 +      if loc.scheme == "https" and "nocert" not in loc.options:
   1.424 +        customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
   1.425 +        clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
   1.426 +        redirRE      = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
   1.427 +        for option in loc.options:
   1.428 +          match = customCertRE.match(option)
   1.429 +          if match:
   1.430 +            customcert = match.group("nickname");
   1.431 +            sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
   1.432 +                      (loc.host, loc.port, self.sslPort, customcert))
   1.433 +
   1.434 +          match = clientAuthRE.match(option)
   1.435 +          if match:
   1.436 +            clientauth = match.group("clientauth");
   1.437 +            sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
   1.438 +                      (loc.host, loc.port, self.sslPort, clientauth))
   1.439 +
   1.440 +          match = redirRE.match(option)
   1.441 +          if match:
   1.442 +            redirhost = match.group("redirhost")
   1.443 +            sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
   1.444 +                      (loc.host, loc.port, self.sslPort, redirhost))
   1.445 +
   1.446 +    sslTunnelConfig.close()
   1.447 +
   1.448 +    # Pre-create the certification database for the profile
   1.449 +    env = self.environment(xrePath = xrePath)
   1.450 +    certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
   1.451 +    pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
   1.452 +
   1.453 +    status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
   1.454 +    automationutils.printstatus(status, "certutil")
   1.455 +    if status != 0:
   1.456 +      return status
   1.457 +
   1.458 +    # Walk the cert directory and add custom CAs and client certs
   1.459 +    files = os.listdir(certPath)
   1.460 +    for item in files:
   1.461 +      root, ext = os.path.splitext(item)
   1.462 +      if ext == ".ca":
   1.463 +        trustBits = "CT,,"
   1.464 +        if root.endswith("-object"):
   1.465 +          trustBits = "CT,,CT"
   1.466 +        status = self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
   1.467 +                    "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
   1.468 +                    env = env).wait()
   1.469 +        automationutils.printstatus(status, "certutil")
   1.470 +      if ext == ".client":
   1.471 +        status = self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
   1.472 +                    pwfilePath, "-d", profileDir], 
   1.473 +                    env = env).wait()
   1.474 +        automationutils.printstatus(status, "pk12util")
   1.475 +
   1.476 +    os.unlink(pwfilePath)
   1.477 +    return 0
   1.478 +
   1.479 +  def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None):
   1.480 +    if xrePath == None:
   1.481 +      xrePath = self.DIST_BIN
   1.482 +    if env == None:
   1.483 +      env = dict(os.environ)
   1.484 +
   1.485 +    ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
   1.486 +    dmdLibrary = None
   1.487 +    preloadEnvVar = None
   1.488 +    if self.UNIXISH or self.IS_MAC:
   1.489 +      envVar = "LD_LIBRARY_PATH"
   1.490 +      preloadEnvVar = "LD_PRELOAD"
   1.491 +      if self.IS_MAC:
   1.492 +        envVar = "DYLD_LIBRARY_PATH"
   1.493 +        dmdLibrary = "libdmd.dylib"
   1.494 +      else: # unixish
   1.495 +        env['MOZILLA_FIVE_HOME'] = xrePath
   1.496 +        dmdLibrary = "libdmd.so"
   1.497 +      if envVar in env:
   1.498 +        ldLibraryPath = ldLibraryPath + ":" + env[envVar]
   1.499 +      env[envVar] = ldLibraryPath
   1.500 +    elif self.IS_WIN32:
   1.501 +      env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath)
   1.502 +      dmdLibrary = "dmd.dll"
   1.503 +      preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
   1.504 +
   1.505 +    if dmdPath and dmdLibrary and preloadEnvVar:
   1.506 +      env['DMD'] = '1'
   1.507 +      env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)
   1.508 +
   1.509 +    if crashreporter and not debugger:
   1.510 +      env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
   1.511 +      env['MOZ_CRASHREPORTER'] = '1'
   1.512 +    else:
   1.513 +      env['MOZ_CRASHREPORTER_DISABLE'] = '1'
   1.514 +
   1.515 +    # Crash on non-local network connections.
   1.516 +    env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
   1.517 +
   1.518 +    env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
   1.519 +    env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
   1.520 +    env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
   1.521 +
   1.522 +    # Set WebRTC logging in case it is not set yet
   1.523 +    env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:3')
   1.524 +    env.setdefault('R_LOG_LEVEL', '5')
   1.525 +    env.setdefault('R_LOG_DESTINATION', 'stderr')
   1.526 +    env.setdefault('R_LOG_VERBOSE', '1')
   1.527 +
   1.528 +    # ASan specific environment stuff
   1.529 +    if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC):
   1.530 +      # Symbolizer support
   1.531 +      llvmsym = os.path.join(xrePath, "llvm-symbolizer")
   1.532 +      if os.path.isfile(llvmsym):
   1.533 +        env["ASAN_SYMBOLIZER_PATH"] = llvmsym
   1.534 +        self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym)
   1.535 +
   1.536 +      try:
   1.537 +        totalMemory = int(os.popen("free").readlines()[1].split()[1])
   1.538 +
   1.539 +        # Only 4 GB RAM or less available? Use custom ASan options to reduce
   1.540 +        # the amount of resources required to do the tests. Standard options 
   1.541 +        # will otherwise lead to OOM conditions on the current test slaves.
   1.542 +        if totalMemory <= 1024 * 1024 * 4:
   1.543 +          self.log.info("INFO | automation.py | ASan running in low-memory configuration")
   1.544 +          env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
   1.545 +        else:
   1.546 +          self.log.info("INFO | automation.py | ASan running in default memory configuration")
   1.547 +      except OSError,err:
   1.548 +        self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror)
   1.549 +      except:
   1.550 +        self.log.info("Failed determine available memory, disabling ASan low-memory configuration")
   1.551 +
   1.552 +    return env
   1.553 +
   1.554 +  def killPid(self, pid):
   1.555 +    try:
   1.556 +      os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
   1.557 +    except WindowsError:
   1.558 +      self.log.info("Failed to kill process %d." % pid)
   1.559 +
   1.560 +  if IS_WIN32:
   1.561 +    PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
   1.562 +    GetLastError = ctypes.windll.kernel32.GetLastError
   1.563 +
   1.564 +    def readWithTimeout(self, f, timeout):
   1.565 +      """
   1.566 +      Try to read a line of output from the file object |f|. |f| must be a
   1.567 +      pipe, like the |stdout| member of a subprocess.Popen object created
   1.568 +      with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout|
   1.569 +      is True if the read timed out, and False otherwise. If no output is
   1.570 +      received within |timeout| seconds, returns a blank line.
   1.571 +      """
   1.572 +
   1.573 +      if timeout is None:
   1.574 +        timeout = 0
   1.575 +
   1.576 +      x = msvcrt.get_osfhandle(f.fileno())
   1.577 +      l = ctypes.c_long()
   1.578 +      done = time.time() + timeout
   1.579 +
   1.580 +      buffer = ""
   1.581 +      while timeout == 0 or time.time() < done:
   1.582 +        if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
   1.583 +          err = self.GetLastError()
   1.584 +          if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
   1.585 +            return ('', False)
   1.586 +          else:
   1.587 +            self.log.error("readWithTimeout got error: %d", err)
   1.588 +        # read a character at a time, checking for eol. Return once we get there.
   1.589 +        index = 0
   1.590 +        while index < l.value:
   1.591 +          char = f.read(1)
   1.592 +          buffer += char
   1.593 +          if char == '\n':
   1.594 +            return (buffer, False)
   1.595 +          index = index + 1
   1.596 +        time.sleep(0.01)
   1.597 +      return (buffer, True)
   1.598 +
   1.599 +    def isPidAlive(self, pid):
   1.600 +      STILL_ACTIVE = 259
   1.601 +      PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
   1.602 +      pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
   1.603 +      if not pHandle:
   1.604 +        return False
   1.605 +      pExitCode = ctypes.wintypes.DWORD()
   1.606 +      ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
   1.607 +      ctypes.windll.kernel32.CloseHandle(pHandle)
   1.608 +      return pExitCode.value == STILL_ACTIVE
   1.609 +
   1.610 +  else:
   1.611 +
   1.612 +    def readWithTimeout(self, f, timeout):
   1.613 +      """Try to read a line of output from the file object |f|. If no output
   1.614 +      is received within |timeout| seconds, return a blank line.
   1.615 +      Returns a tuple (line, did_timeout), where |did_timeout| is True
   1.616 +      if the read timed out, and False otherwise."""
   1.617 +      (r, w, e) = select.select([f], [], [], timeout)
   1.618 +      if len(r) == 0:
   1.619 +        return ('', True)
   1.620 +      return (f.readline(), False)
   1.621 +
   1.622 +    def isPidAlive(self, pid):
   1.623 +      try:
   1.624 +        # kill(pid, 0) checks for a valid PID without actually sending a signal
   1.625 +        # The method throws OSError if the PID is invalid, which we catch below.
   1.626 +        os.kill(pid, 0)
   1.627 +
   1.628 +        # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
   1.629 +        # the process terminates before we get to this point.
   1.630 +        wpid, wstatus = os.waitpid(pid, os.WNOHANG)
   1.631 +        return wpid == 0
   1.632 +      except OSError, err:
   1.633 +        # Catch the errors we might expect from os.kill/os.waitpid, 
   1.634 +        # and re-raise any others
   1.635 +        if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
   1.636 +          return False
   1.637 +        raise
   1.638 +
   1.639 +  def dumpScreen(self, utilityPath):
   1.640 +    if self.haveDumpedScreen:
   1.641 +      self.log.info("Not taking screenshot here: see the one that was previously logged")
   1.642 +      return
   1.643 +
   1.644 +    self.haveDumpedScreen = True;
   1.645 +    automationutils.dumpScreen(utilityPath)
   1.646 +
   1.647 +
   1.648 +  def killAndGetStack(self, processPID, utilityPath, debuggerInfo):
   1.649 +    """Kill the process, preferrably in a way that gets us a stack trace.
   1.650 +       Also attempts to obtain a screenshot before killing the process."""
   1.651 +    if not debuggerInfo:
   1.652 +      self.dumpScreen(utilityPath)
   1.653 +    self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo)
   1.654 +
   1.655 +  def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo):
   1.656 +    """Kill the process, preferrably in a way that gets us a stack trace."""
   1.657 +    if self.CRASHREPORTER and not debuggerInfo:
   1.658 +      if not self.IS_WIN32:
   1.659 +        # ABRT will get picked up by Breakpad's signal handler
   1.660 +        os.kill(processPID, signal.SIGABRT)
   1.661 +        return
   1.662 +      else:
   1.663 +        # We should have a "crashinject" program in our utility path
   1.664 +        crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
   1.665 +        if os.path.exists(crashinject):
   1.666 +          status = subprocess.Popen([crashinject, str(processPID)]).wait()
   1.667 +          automationutils.printstatus(status, "crashinject")
   1.668 +          if status == 0:
   1.669 +            return
   1.670 +    self.log.info("Can't trigger Breakpad, just killing process")
   1.671 +    self.killPid(processPID)
   1.672 +
   1.673 +  def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
   1.674 +    """ Look for timeout or crashes and return the status after the process terminates """
   1.675 +    stackFixerProcess = None
   1.676 +    stackFixerFunction = None
   1.677 +    didTimeout = False
   1.678 +    hitMaxTime = False
   1.679 +    if proc.stdout is None:
   1.680 +      self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
   1.681 +    else:
   1.682 +      logsource = proc.stdout
   1.683 +
   1.684 +      if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath):
   1.685 +        # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
   1.686 +        # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
   1.687 +        sys.path.insert(0, utilityPath)
   1.688 +        import fix_stack_using_bpsyms as stackFixerModule
   1.689 +        stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
   1.690 +        del sys.path[0]
   1.691 +      elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
   1.692 +        # Run each line through a function in fix_macosx_stack.py (uses atos)
   1.693 +        sys.path.insert(0, utilityPath)
   1.694 +        import fix_macosx_stack as stackFixerModule
   1.695 +        stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
   1.696 +        del sys.path[0]
   1.697 +      elif self.IS_DEBUG_BUILD and self.IS_LINUX:
   1.698 +        # Run logsource through fix-linux-stack.pl (uses addr2line)
   1.699 +        # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
   1.700 +        stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
   1.701 +                                         stdin=logsource,
   1.702 +                                         stdout=subprocess.PIPE)
   1.703 +        logsource = stackFixerProcess.stdout
   1.704 +
   1.705 +      # With metro browser runs this script launches the metro test harness which launches the browser.
   1.706 +      # The metro test harness hands back the real browser process id via log output which we need to
   1.707 +      # pick up on and parse out. This variable tracks the real browser process id if we find it.
   1.708 +      browserProcessId = -1
   1.709 +
   1.710 +      (line, didTimeout) = self.readWithTimeout(logsource, timeout)
   1.711 +      while line != "" and not didTimeout:
   1.712 +        if stackFixerFunction:
   1.713 +          line = stackFixerFunction(line)
   1.714 +        self.log.info(line.rstrip().decode("UTF-8", "ignore"))
   1.715 +        if "TEST-START" in line and "|" in line:
   1.716 +          self.lastTestSeen = line.split("|")[1].strip()
   1.717 +        if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
   1.718 +          self.dumpScreen(utilityPath)
   1.719 +
   1.720 +        (line, didTimeout) = self.readWithTimeout(logsource, timeout)
   1.721 +
   1.722 +        if "METRO_BROWSER_PROCESS" in line:
   1.723 +          index = line.find("=")
   1.724 +          if index:
   1.725 +            browserProcessId = line[index+1:].rstrip()
   1.726 +            self.log.info("INFO | automation.py | metro browser sub process id detected: %s", browserProcessId)
   1.727 +
   1.728 +        if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
   1.729 +          # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
   1.730 +          hitMaxTime = True
   1.731 +          self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
   1.732 +          self.killAndGetStack(proc.pid, utilityPath, debuggerInfo)
   1.733 +      if didTimeout:
   1.734 +        if line:
   1.735 +          self.log.info(line.rstrip().decode("UTF-8", "ignore"))
   1.736 +        self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
   1.737 +        if browserProcessId == -1:
   1.738 +          browserProcessId = proc.pid
   1.739 +        self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo)
   1.740 +
   1.741 +    status = proc.wait()
   1.742 +    automationutils.printstatus(status, "Main app process")
   1.743 +    if status == 0:
   1.744 +      self.lastTestSeen = "Main app process exited normally"
   1.745 +    if status != 0 and not didTimeout and not hitMaxTime:
   1.746 +      self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
   1.747 +    if stackFixerProcess is not None:
   1.748 +      fixerStatus = stackFixerProcess.wait()
   1.749 +      automationutils.printstatus(status, "stackFixerProcess")
   1.750 +      if fixerStatus != 0 and not didTimeout and not hitMaxTime:
   1.751 +        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
   1.752 +    return status
   1.753 +
   1.754 +  def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
   1.755 +    """ build the application command line """
   1.756 +
   1.757 +    cmd = os.path.abspath(app)
   1.758 +    if self.IS_MAC and not self.IS_CAMINO and os.path.exists(cmd + "-bin"):
   1.759 +      # Prefer 'app-bin' in case 'app' is a shell script.
   1.760 +      # We can remove this hack once bug 673899 etc are fixed.
   1.761 +      cmd += "-bin"
   1.762 +
   1.763 +    args = []
   1.764 +
   1.765 +    if debuggerInfo:
   1.766 +      args.extend(debuggerInfo["args"])
   1.767 +      args.append(cmd)
   1.768 +      cmd = os.path.abspath(debuggerInfo["path"])
   1.769 +
   1.770 +    if self.IS_MAC:
   1.771 +      args.append("-foreground")
   1.772 +
   1.773 +    if self.IS_CYGWIN:
   1.774 +      profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
   1.775 +    else:
   1.776 +      profileDirectory = profileDir + "/"
   1.777 +
   1.778 +    args.extend(("-no-remote", "-profile", profileDirectory))
   1.779 +    if testURL is not None:
   1.780 +      if self.IS_CAMINO:
   1.781 +        args.extend(("-url", testURL))
   1.782 +      else:
   1.783 +        args.append((testURL))
   1.784 +    args.extend(extraArgs)
   1.785 +    return cmd, args
   1.786 +
   1.787 +  def checkForZombies(self, processLog, utilityPath, debuggerInfo):
   1.788 +    """ Look for hung processes """
   1.789 +    if not os.path.exists(processLog):
   1.790 +      self.log.info('Automation Error: PID log not found: %s', processLog)
   1.791 +      # Whilst no hung process was found, the run should still display as a failure
   1.792 +      return True
   1.793 +
   1.794 +    foundZombie = False
   1.795 +    self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
   1.796 +    processList = []
   1.797 +    pidRE = re.compile(r'launched child process (\d+)$')
   1.798 +    processLogFD = open(processLog)
   1.799 +    for line in processLogFD:
   1.800 +      self.log.info(line.rstrip())
   1.801 +      m = pidRE.search(line)
   1.802 +      if m:
   1.803 +        processList.append(int(m.group(1)))
   1.804 +    processLogFD.close()
   1.805 +
   1.806 +    for processPID in processList:
   1.807 +      self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
   1.808 +      if self.isPidAlive(processPID):
   1.809 +        foundZombie = True
   1.810 +        self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
   1.811 +        self.killAndGetStack(processPID, utilityPath, debuggerInfo)
   1.812 +    return foundZombie
   1.813 +
   1.814 +  def checkForCrashes(self, minidumpDir, symbolsPath):
   1.815 +    return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen)
   1.816 +
   1.817 +  def runApp(self, testURL, env, app, profileDir, extraArgs,
   1.818 +             runSSLTunnel = False, utilityPath = None,
   1.819 +             xrePath = None, certPath = None,
   1.820 +             debuggerInfo = None, symbolsPath = None,
   1.821 +             timeout = -1, maxTime = None, onLaunch = None,
   1.822 +             webapprtChrome = False, hide_subtests=None, screenshotOnFail=False):
   1.823 +    """
   1.824 +    Run the app, log the duration it took to execute, return the status code.
   1.825 +    Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
   1.826 +    """
   1.827 +
   1.828 +    if utilityPath == None:
   1.829 +      utilityPath = self.DIST_BIN
   1.830 +    if xrePath == None:
   1.831 +      xrePath = self.DIST_BIN
   1.832 +    if certPath == None:
   1.833 +      certPath = self.CERTS_SRC_DIR
   1.834 +    if timeout == -1:
   1.835 +      timeout = self.DEFAULT_TIMEOUT
   1.836 +
   1.837 +    # copy env so we don't munge the caller's environment
   1.838 +    env = dict(env);
   1.839 +    env["NO_EM_RESTART"] = "1"
   1.840 +    tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
   1.841 +    os.close(tmpfd)
   1.842 +    env["MOZ_PROCESS_LOG"] = processLog
   1.843 +
   1.844 +    if self.IS_TEST_BUILD and runSSLTunnel:
   1.845 +      # create certificate database for the profile
   1.846 +      certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
   1.847 +      if certificateStatus != 0:
   1.848 +        self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
   1.849 +        return certificateStatus
   1.850 +
   1.851 +      # start ssltunnel to provide https:// URLs capability
   1.852 +      ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
   1.853 +      ssltunnelProcess = self.Process([ssltunnel, 
   1.854 +                               os.path.join(profileDir, "ssltunnel.cfg")], 
   1.855 +                               env = self.environment(xrePath = xrePath))
   1.856 +      self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
   1.857 +
   1.858 +    cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
   1.859 +    startTime = datetime.now()
   1.860 +
   1.861 +    if debuggerInfo and debuggerInfo["interactive"]:
   1.862 +      # If an interactive debugger is attached, don't redirect output,
   1.863 +      # don't use timeouts, and don't capture ctrl-c.
   1.864 +      timeout = None
   1.865 +      maxTime = None
   1.866 +      outputPipe = None
   1.867 +      signal.signal(signal.SIGINT, lambda sigid, frame: None)
   1.868 +    else:
   1.869 +      outputPipe = subprocess.PIPE
   1.870 +
   1.871 +    self.lastTestSeen = "automation.py"
   1.872 +    proc = self.Process([cmd] + args,
   1.873 +                 env = self.environment(env, xrePath = xrePath,
   1.874 +                                   crashreporter = not debuggerInfo),
   1.875 +                 stdout = outputPipe,
   1.876 +                 stderr = subprocess.STDOUT)
   1.877 +    self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
   1.878 +
   1.879 +    if onLaunch is not None:
   1.880 +      # Allow callers to specify an onLaunch callback to be fired after the
   1.881 +      # app is launched.
   1.882 +      onLaunch()
   1.883 +
   1.884 +    status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
   1.885 +    self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
   1.886 +
   1.887 +    # Do a final check for zombie child processes.
   1.888 +    zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
   1.889 +
   1.890 +    crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath)
   1.891 +
   1.892 +    if crashed or zombieProcesses:
   1.893 +      status = 1
   1.894 +
   1.895 +    if os.path.exists(processLog):
   1.896 +      os.unlink(processLog)
   1.897 +
   1.898 +    if self.IS_TEST_BUILD and runSSLTunnel:
   1.899 +      ssltunnelProcess.kill()
   1.900 +
   1.901 +    return status
   1.902 +
   1.903 +  def getExtensionIDFromRDF(self, rdfSource):
   1.904 +    """
   1.905 +    Retrieves the extension id from an install.rdf file (or string).
   1.906 +    """
   1.907 +    from xml.dom.minidom import parse, parseString, Node
   1.908 +
   1.909 +    if isinstance(rdfSource, file):
   1.910 +      document = parse(rdfSource)
   1.911 +    else:
   1.912 +      document = parseString(rdfSource)
   1.913 +
   1.914 +    # Find the <em:id> element. There can be multiple <em:id> tags
   1.915 +    # within <em:targetApplication> tags, so we have to check this way.
   1.916 +    for rdfChild in document.documentElement.childNodes:
   1.917 +      if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
   1.918 +        for descChild in rdfChild.childNodes:
   1.919 +          if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
   1.920 +            return descChild.childNodes[0].data
   1.921 +
   1.922 +    return None
   1.923 +
   1.924 +  def installExtension(self, extensionSource, profileDir, extensionID = None):
   1.925 +    """
   1.926 +    Copies an extension into the extensions directory of the given profile.
   1.927 +    extensionSource - the source location of the extension files.  This can be either
   1.928 +                      a directory or a path to an xpi file.
   1.929 +    profileDir      - the profile directory we are copying into.  We will create the
   1.930 +                      "extensions" directory there if it doesn't exist.
   1.931 +    extensionID     - the id of the extension to be used as the containing directory for the
   1.932 +                      extension, if extensionSource is a directory, i.e.
   1.933 +                  this is the name of the folder in the <profileDir>/extensions/<extensionID>
   1.934 +    """
   1.935 +    if not os.path.isdir(profileDir):
   1.936 +      self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
   1.937 +      return
   1.938 +
   1.939 +    installRDFFilename = "install.rdf"
   1.940 +
   1.941 +    extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
   1.942 +    if not os.path.isdir(extensionsRootDir):
   1.943 +      os.makedirs(extensionsRootDir)
   1.944 +
   1.945 +    if os.path.isfile(extensionSource):
   1.946 +      reader = automationutils.ZipFileReader(extensionSource)
   1.947 +
   1.948 +      for filename in reader.namelist():
   1.949 +        # Sanity check the zip file.
   1.950 +        if os.path.isabs(filename):
   1.951 +          self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
   1.952 +          return
   1.953 +
   1.954 +        # We may need to dig the extensionID out of the zip file...
   1.955 +        if extensionID is None and filename == installRDFFilename:
   1.956 +          extensionID = self.getExtensionIDFromRDF(reader.read(filename))
   1.957 +
   1.958 +      # We must know the extensionID now.
   1.959 +      if extensionID is None:
   1.960 +        self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
   1.961 +        return
   1.962 +
   1.963 +      # Make the extension directory.
   1.964 +      extensionDir = os.path.join(extensionsRootDir, extensionID)
   1.965 +      os.mkdir(extensionDir)
   1.966 +
   1.967 +      # Extract all files.
   1.968 +      reader.extractall(extensionDir)
   1.969 +
   1.970 +    elif os.path.isdir(extensionSource):
   1.971 +      if extensionID is None:
   1.972 +        filename = os.path.join(extensionSource, installRDFFilename)
   1.973 +        if os.path.isfile(filename):
   1.974 +          with open(filename, "r") as installRDF:
   1.975 +            extensionID = self.getExtensionIDFromRDF(installRDF)
   1.976 +
   1.977 +        if extensionID is None:
   1.978 +          self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
   1.979 +          return
   1.980 +
   1.981 +      # Copy extension tree into its own directory.
   1.982 +      # "destination directory must not already exist".
   1.983 +      shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
   1.984 +
   1.985 +    else:
   1.986 +      self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)
   1.987 +
   1.988 +  def elf_arm(self, filename):
   1.989 +    data = open(filename, 'rb').read(20)
   1.990 +    return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM
   1.991 +

mercurial