build/automation.py.in

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

michael@0 1 #
michael@0 2 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 # License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 5
michael@0 6 from __future__ import with_statement
michael@0 7 import codecs
michael@0 8 import itertools
michael@0 9 import json
michael@0 10 import logging
michael@0 11 import os
michael@0 12 import re
michael@0 13 import select
michael@0 14 import shutil
michael@0 15 import signal
michael@0 16 import subprocess
michael@0 17 import sys
michael@0 18 import threading
michael@0 19 import tempfile
michael@0 20 import sqlite3
michael@0 21 from datetime import datetime, timedelta
michael@0 22 from string import Template
michael@0 23
michael@0 24 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0])))
michael@0 25 sys.path.insert(0, SCRIPT_DIR)
michael@0 26 import automationutils
michael@0 27
michael@0 28 # --------------------------------------------------------------
michael@0 29 # TODO: this is a hack for mozbase without virtualenv, remove with bug 849900
michael@0 30 # These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR
michael@0 31 here = os.path.dirname(os.path.realpath(__file__))
michael@0 32 mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase'))
michael@0 33
michael@0 34 if os.path.isdir(mozbase):
michael@0 35 for package in os.listdir(mozbase):
michael@0 36 package_path = os.path.join(mozbase, package)
michael@0 37 if package_path not in sys.path:
michael@0 38 sys.path.append(package_path)
michael@0 39
michael@0 40 import mozcrash
michael@0 41 from mozprofile import Profile, Preferences
michael@0 42 from mozprofile.permissions import ServerLocations
michael@0 43
michael@0 44 # ---------------------------------------------------------------
michael@0 45
michael@0 46 _DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js')
michael@0 47 _DEFAULT_APPS_FILE = os.path.join(SCRIPT_DIR, 'webapps_mochitest.json')
michael@0 48
michael@0 49 _DEFAULT_WEB_SERVER = "127.0.0.1"
michael@0 50 _DEFAULT_HTTP_PORT = 8888
michael@0 51 _DEFAULT_SSL_PORT = 4443
michael@0 52 _DEFAULT_WEBSOCKET_PORT = 9988
michael@0 53
michael@0 54 # from nsIPrincipal.idl
michael@0 55 _APP_STATUS_NOT_INSTALLED = 0
michael@0 56 _APP_STATUS_INSTALLED = 1
michael@0 57 _APP_STATUS_PRIVILEGED = 2
michael@0 58 _APP_STATUS_CERTIFIED = 3
michael@0 59
michael@0 60 #expand _DIST_BIN = __XPC_BIN_PATH__
michael@0 61 #expand _IS_WIN32 = len("__WIN32__") != 0
michael@0 62 #expand _IS_MAC = __IS_MAC__ != 0
michael@0 63 #expand _IS_LINUX = __IS_LINUX__ != 0
michael@0 64 #ifdef IS_CYGWIN
michael@0 65 #expand _IS_CYGWIN = __IS_CYGWIN__ == 1
michael@0 66 #else
michael@0 67 _IS_CYGWIN = False
michael@0 68 #endif
michael@0 69 #expand _IS_CAMINO = __IS_CAMINO__ != 0
michael@0 70 #expand _BIN_SUFFIX = __BIN_SUFFIX__
michael@0 71 #expand _PERL = __PERL__
michael@0 72
michael@0 73 #expand _DEFAULT_APP = "./" + __BROWSER_PATH__
michael@0 74 #expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__
michael@0 75 #expand _IS_TEST_BUILD = __IS_TEST_BUILD__
michael@0 76 #expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__
michael@0 77 #expand _CRASHREPORTER = __CRASHREPORTER__ == 1
michael@0 78 #expand _IS_ASAN = __IS_ASAN__ == 1
michael@0 79
michael@0 80
michael@0 81 if _IS_WIN32:
michael@0 82 import ctypes, ctypes.wintypes, time, msvcrt
michael@0 83 else:
michael@0 84 import errno
michael@0 85
michael@0 86
michael@0 87 def getGlobalLog():
michael@0 88 return _log
michael@0 89
michael@0 90 def resetGlobalLog(log):
michael@0 91 while _log.handlers:
michael@0 92 _log.removeHandler(_log.handlers[0])
michael@0 93 handler = logging.StreamHandler(log)
michael@0 94 _log.setLevel(logging.INFO)
michael@0 95 _log.addHandler(handler)
michael@0 96
michael@0 97 # We use the logging system here primarily because it'll handle multiple
michael@0 98 # threads, which is needed to process the output of the server and application
michael@0 99 # processes simultaneously.
michael@0 100 _log = logging.getLogger()
michael@0 101 resetGlobalLog(sys.stdout)
michael@0 102
michael@0 103
michael@0 104 #################
michael@0 105 # PROFILE SETUP #
michael@0 106 #################
michael@0 107
michael@0 108 class SyntaxError(Exception):
michael@0 109 "Signifies a syntax error on a particular line in server-locations.txt."
michael@0 110
michael@0 111 def __init__(self, lineno, msg = None):
michael@0 112 self.lineno = lineno
michael@0 113 self.msg = msg
michael@0 114
michael@0 115 def __str__(self):
michael@0 116 s = "Syntax error on line " + str(self.lineno)
michael@0 117 if self.msg:
michael@0 118 s += ": %s." % self.msg
michael@0 119 else:
michael@0 120 s += "."
michael@0 121 return s
michael@0 122
michael@0 123
michael@0 124 class Location:
michael@0 125 "Represents a location line in server-locations.txt."
michael@0 126
michael@0 127 def __init__(self, scheme, host, port, options):
michael@0 128 self.scheme = scheme
michael@0 129 self.host = host
michael@0 130 self.port = port
michael@0 131 self.options = options
michael@0 132
michael@0 133 class Automation(object):
michael@0 134 """
michael@0 135 Runs the browser from a script, and provides useful utilities
michael@0 136 for setting up the browser environment.
michael@0 137 """
michael@0 138
michael@0 139 DIST_BIN = _DIST_BIN
michael@0 140 IS_WIN32 = _IS_WIN32
michael@0 141 IS_MAC = _IS_MAC
michael@0 142 IS_LINUX = _IS_LINUX
michael@0 143 IS_CYGWIN = _IS_CYGWIN
michael@0 144 IS_CAMINO = _IS_CAMINO
michael@0 145 BIN_SUFFIX = _BIN_SUFFIX
michael@0 146 PERL = _PERL
michael@0 147
michael@0 148 UNIXISH = not IS_WIN32 and not IS_MAC
michael@0 149
michael@0 150 DEFAULT_APP = _DEFAULT_APP
michael@0 151 CERTS_SRC_DIR = _CERTS_SRC_DIR
michael@0 152 IS_TEST_BUILD = _IS_TEST_BUILD
michael@0 153 IS_DEBUG_BUILD = _IS_DEBUG_BUILD
michael@0 154 CRASHREPORTER = _CRASHREPORTER
michael@0 155 IS_ASAN = _IS_ASAN
michael@0 156
michael@0 157 # timeout, in seconds
michael@0 158 DEFAULT_TIMEOUT = 60.0
michael@0 159 DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER
michael@0 160 DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT
michael@0 161 DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT
michael@0 162 DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT
michael@0 163
michael@0 164 def __init__(self):
michael@0 165 self.log = _log
michael@0 166 self.lastTestSeen = "automation.py"
michael@0 167 self.haveDumpedScreen = False
michael@0 168
michael@0 169 def setServerInfo(self,
michael@0 170 webServer = _DEFAULT_WEB_SERVER,
michael@0 171 httpPort = _DEFAULT_HTTP_PORT,
michael@0 172 sslPort = _DEFAULT_SSL_PORT,
michael@0 173 webSocketPort = _DEFAULT_WEBSOCKET_PORT):
michael@0 174 self.webServer = webServer
michael@0 175 self.httpPort = httpPort
michael@0 176 self.sslPort = sslPort
michael@0 177 self.webSocketPort = webSocketPort
michael@0 178
michael@0 179 @property
michael@0 180 def __all__(self):
michael@0 181 return [
michael@0 182 "UNIXISH",
michael@0 183 "IS_WIN32",
michael@0 184 "IS_MAC",
michael@0 185 "log",
michael@0 186 "runApp",
michael@0 187 "Process",
michael@0 188 "addCommonOptions",
michael@0 189 "initializeProfile",
michael@0 190 "DIST_BIN",
michael@0 191 "DEFAULT_APP",
michael@0 192 "CERTS_SRC_DIR",
michael@0 193 "environment",
michael@0 194 "IS_TEST_BUILD",
michael@0 195 "IS_DEBUG_BUILD",
michael@0 196 "DEFAULT_TIMEOUT",
michael@0 197 ]
michael@0 198
michael@0 199 class Process(subprocess.Popen):
michael@0 200 """
michael@0 201 Represents our view of a subprocess.
michael@0 202 It adds a kill() method which allows it to be stopped explicitly.
michael@0 203 """
michael@0 204
michael@0 205 def __init__(self,
michael@0 206 args,
michael@0 207 bufsize=0,
michael@0 208 executable=None,
michael@0 209 stdin=None,
michael@0 210 stdout=None,
michael@0 211 stderr=None,
michael@0 212 preexec_fn=None,
michael@0 213 close_fds=False,
michael@0 214 shell=False,
michael@0 215 cwd=None,
michael@0 216 env=None,
michael@0 217 universal_newlines=False,
michael@0 218 startupinfo=None,
michael@0 219 creationflags=0):
michael@0 220 _log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(args))
michael@0 221 subprocess.Popen.__init__(self, args, bufsize, executable,
michael@0 222 stdin, stdout, stderr,
michael@0 223 preexec_fn, close_fds,
michael@0 224 shell, cwd, env,
michael@0 225 universal_newlines, startupinfo, creationflags)
michael@0 226 self.log = _log
michael@0 227
michael@0 228 def kill(self):
michael@0 229 if Automation().IS_WIN32:
michael@0 230 import platform
michael@0 231 pid = "%i" % self.pid
michael@0 232 if platform.release() == "2000":
michael@0 233 # Windows 2000 needs 'kill.exe' from the
michael@0 234 #'Windows 2000 Resource Kit tools'. (See bug 475455.)
michael@0 235 try:
michael@0 236 subprocess.Popen(["kill", "-f", pid]).wait()
michael@0 237 except:
michael@0 238 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid)
michael@0 239 else:
michael@0 240 # Windows XP and later.
michael@0 241 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait()
michael@0 242 else:
michael@0 243 os.kill(self.pid, signal.SIGKILL)
michael@0 244
michael@0 245 def readLocations(self, locationsPath = "server-locations.txt"):
michael@0 246 """
michael@0 247 Reads the locations at which the Mochitest HTTP server is available from
michael@0 248 server-locations.txt.
michael@0 249 """
michael@0 250
michael@0 251 locationFile = codecs.open(locationsPath, "r", "UTF-8")
michael@0 252
michael@0 253 # Perhaps more detail than necessary, but it's the easiest way to make sure
michael@0 254 # we get exactly the format we want. See server-locations.txt for the exact
michael@0 255 # format guaranteed here.
michael@0 256 lineRe = re.compile(r"^(?P<scheme>[a-z][-a-z0-9+.]*)"
michael@0 257 r"://"
michael@0 258 r"(?P<host>"
michael@0 259 r"\d+\.\d+\.\d+\.\d+"
michael@0 260 r"|"
michael@0 261 r"(?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)*"
michael@0 262 r"[a-z](?:[-a-z0-9]*[a-z0-9])?"
michael@0 263 r")"
michael@0 264 r":"
michael@0 265 r"(?P<port>\d+)"
michael@0 266 r"(?:"
michael@0 267 r"\s+"
michael@0 268 r"(?P<options>\S+(?:,\S+)*)"
michael@0 269 r")?$")
michael@0 270 locations = []
michael@0 271 lineno = 0
michael@0 272 seenPrimary = False
michael@0 273 for line in locationFile:
michael@0 274 lineno += 1
michael@0 275 if line.startswith("#") or line == "\n":
michael@0 276 continue
michael@0 277
michael@0 278 match = lineRe.match(line)
michael@0 279 if not match:
michael@0 280 raise SyntaxError(lineno)
michael@0 281
michael@0 282 options = match.group("options")
michael@0 283 if options:
michael@0 284 options = options.split(",")
michael@0 285 if "primary" in options:
michael@0 286 if seenPrimary:
michael@0 287 raise SyntaxError(lineno, "multiple primary locations")
michael@0 288 seenPrimary = True
michael@0 289 else:
michael@0 290 options = []
michael@0 291
michael@0 292 locations.append(Location(match.group("scheme"), match.group("host"),
michael@0 293 match.group("port"), options))
michael@0 294
michael@0 295 if not seenPrimary:
michael@0 296 raise SyntaxError(lineno + 1, "missing primary location")
michael@0 297
michael@0 298 return locations
michael@0 299
michael@0 300 def setupPermissionsDatabase(self, profileDir, permissions):
michael@0 301 # Included for reftest compatibility;
michael@0 302 # see https://bugzilla.mozilla.org/show_bug.cgi?id=688667
michael@0 303
michael@0 304 # Open database and create table
michael@0 305 permDB = sqlite3.connect(os.path.join(profileDir, "permissions.sqlite"))
michael@0 306 cursor = permDB.cursor();
michael@0 307
michael@0 308 cursor.execute("PRAGMA user_version=3");
michael@0 309
michael@0 310 # SQL copied from nsPermissionManager.cpp
michael@0 311 cursor.execute("""CREATE TABLE IF NOT EXISTS moz_hosts (
michael@0 312 id INTEGER PRIMARY KEY,
michael@0 313 host TEXT,
michael@0 314 type TEXT,
michael@0 315 permission INTEGER,
michael@0 316 expireType INTEGER,
michael@0 317 expireTime INTEGER,
michael@0 318 appId INTEGER,
michael@0 319 isInBrowserElement INTEGER)""")
michael@0 320
michael@0 321 # Insert desired permissions
michael@0 322 for perm in permissions.keys():
michael@0 323 for host,allow in permissions[perm]:
michael@0 324 cursor.execute("INSERT INTO moz_hosts values(NULL, ?, ?, ?, 0, 0, 0, 0)",
michael@0 325 (host, perm, 1 if allow else 2))
michael@0 326
michael@0 327 # Commit and close
michael@0 328 permDB.commit()
michael@0 329 cursor.close()
michael@0 330
michael@0 331 def initializeProfile(self, profileDir,
michael@0 332 extraPrefs=None,
michael@0 333 useServerLocations=False,
michael@0 334 prefsPath=_DEFAULT_PREFERENCE_FILE,
michael@0 335 appsPath=_DEFAULT_APPS_FILE,
michael@0 336 addons=None):
michael@0 337 " Sets up the standard testing profile."
michael@0 338
michael@0 339 extraPrefs = extraPrefs or []
michael@0 340
michael@0 341 # create the profile
michael@0 342 prefs = {}
michael@0 343 locations = None
michael@0 344 if useServerLocations:
michael@0 345 locations = ServerLocations()
michael@0 346 locations.read(os.path.abspath('server-locations.txt'), True)
michael@0 347 else:
michael@0 348 prefs['network.proxy.type'] = 0
michael@0 349
michael@0 350 prefs.update(Preferences.read_prefs(prefsPath))
michael@0 351
michael@0 352 for v in extraPrefs:
michael@0 353 thispref = v.split("=", 1)
michael@0 354 if len(thispref) < 2:
michael@0 355 print "Error: syntax error in --setpref=" + v
michael@0 356 sys.exit(1)
michael@0 357 prefs[thispref[0]] = thispref[1]
michael@0 358
michael@0 359
michael@0 360 interpolation = {"server": "%s:%s" % (self.webServer, self.httpPort)}
michael@0 361 prefs = json.loads(json.dumps(prefs) % interpolation)
michael@0 362 for pref in prefs:
michael@0 363 prefs[pref] = Preferences.cast(prefs[pref])
michael@0 364
michael@0 365 # load apps
michael@0 366 apps = None
michael@0 367 if appsPath and os.path.exists(appsPath):
michael@0 368 with open(appsPath, 'r') as apps_file:
michael@0 369 apps = json.load(apps_file)
michael@0 370
michael@0 371 proxy = {'remote': str(self.webServer),
michael@0 372 'http': str(self.httpPort),
michael@0 373 'https': str(self.sslPort),
michael@0 374 # use SSL port for legacy compatibility; see
michael@0 375 # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
michael@0 376 # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
michael@0 377 # 'ws': str(self.webSocketPort)
michael@0 378 'ws': str(self.sslPort)
michael@0 379 }
michael@0 380
michael@0 381 # return profile object
michael@0 382 profile = Profile(profile=profileDir,
michael@0 383 addons=addons,
michael@0 384 locations=locations,
michael@0 385 preferences=prefs,
michael@0 386 restore=False,
michael@0 387 apps=apps,
michael@0 388 proxy=proxy)
michael@0 389 return profile
michael@0 390
michael@0 391 def addCommonOptions(self, parser):
michael@0 392 "Adds command-line options which are common to mochitest and reftest."
michael@0 393
michael@0 394 parser.add_option("--setpref",
michael@0 395 action = "append", type = "string",
michael@0 396 default = [],
michael@0 397 dest = "extraPrefs", metavar = "PREF=VALUE",
michael@0 398 help = "defines an extra user preference")
michael@0 399
michael@0 400 def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
michael@0 401 pwfilePath = os.path.join(profileDir, ".crtdbpw")
michael@0 402 pwfile = open(pwfilePath, "w")
michael@0 403 pwfile.write("\n")
michael@0 404 pwfile.close()
michael@0 405
michael@0 406 # Create head of the ssltunnel configuration file
michael@0 407 sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
michael@0 408 sslTunnelConfig = open(sslTunnelConfigPath, "w")
michael@0 409
michael@0 410 sslTunnelConfig.write("httpproxy:1\n")
michael@0 411 sslTunnelConfig.write("certdbdir:%s\n" % certPath)
michael@0 412 sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
michael@0 413 sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
michael@0 414 sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
michael@0 415
michael@0 416 # Configure automatic certificate and bind custom certificates, client authentication
michael@0 417 locations = self.readLocations()
michael@0 418 locations.pop(0)
michael@0 419 for loc in locations:
michael@0 420 if loc.scheme == "https" and "nocert" not in loc.options:
michael@0 421 customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
michael@0 422 clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
michael@0 423 redirRE = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
michael@0 424 for option in loc.options:
michael@0 425 match = customCertRE.match(option)
michael@0 426 if match:
michael@0 427 customcert = match.group("nickname");
michael@0 428 sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
michael@0 429 (loc.host, loc.port, self.sslPort, customcert))
michael@0 430
michael@0 431 match = clientAuthRE.match(option)
michael@0 432 if match:
michael@0 433 clientauth = match.group("clientauth");
michael@0 434 sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
michael@0 435 (loc.host, loc.port, self.sslPort, clientauth))
michael@0 436
michael@0 437 match = redirRE.match(option)
michael@0 438 if match:
michael@0 439 redirhost = match.group("redirhost")
michael@0 440 sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
michael@0 441 (loc.host, loc.port, self.sslPort, redirhost))
michael@0 442
michael@0 443 sslTunnelConfig.close()
michael@0 444
michael@0 445 # Pre-create the certification database for the profile
michael@0 446 env = self.environment(xrePath = xrePath)
michael@0 447 certutil = os.path.join(utilityPath, "certutil" + self.BIN_SUFFIX)
michael@0 448 pk12util = os.path.join(utilityPath, "pk12util" + self.BIN_SUFFIX)
michael@0 449
michael@0 450 status = self.Process([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env = env).wait()
michael@0 451 automationutils.printstatus(status, "certutil")
michael@0 452 if status != 0:
michael@0 453 return status
michael@0 454
michael@0 455 # Walk the cert directory and add custom CAs and client certs
michael@0 456 files = os.listdir(certPath)
michael@0 457 for item in files:
michael@0 458 root, ext = os.path.splitext(item)
michael@0 459 if ext == ".ca":
michael@0 460 trustBits = "CT,,"
michael@0 461 if root.endswith("-object"):
michael@0 462 trustBits = "CT,,CT"
michael@0 463 status = self.Process([certutil, "-A", "-i", os.path.join(certPath, item),
michael@0 464 "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
michael@0 465 env = env).wait()
michael@0 466 automationutils.printstatus(status, "certutil")
michael@0 467 if ext == ".client":
michael@0 468 status = self.Process([pk12util, "-i", os.path.join(certPath, item), "-w",
michael@0 469 pwfilePath, "-d", profileDir],
michael@0 470 env = env).wait()
michael@0 471 automationutils.printstatus(status, "pk12util")
michael@0 472
michael@0 473 os.unlink(pwfilePath)
michael@0 474 return 0
michael@0 475
michael@0 476 def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None):
michael@0 477 if xrePath == None:
michael@0 478 xrePath = self.DIST_BIN
michael@0 479 if env == None:
michael@0 480 env = dict(os.environ)
michael@0 481
michael@0 482 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath))
michael@0 483 dmdLibrary = None
michael@0 484 preloadEnvVar = None
michael@0 485 if self.UNIXISH or self.IS_MAC:
michael@0 486 envVar = "LD_LIBRARY_PATH"
michael@0 487 preloadEnvVar = "LD_PRELOAD"
michael@0 488 if self.IS_MAC:
michael@0 489 envVar = "DYLD_LIBRARY_PATH"
michael@0 490 dmdLibrary = "libdmd.dylib"
michael@0 491 else: # unixish
michael@0 492 env['MOZILLA_FIVE_HOME'] = xrePath
michael@0 493 dmdLibrary = "libdmd.so"
michael@0 494 if envVar in env:
michael@0 495 ldLibraryPath = ldLibraryPath + ":" + env[envVar]
michael@0 496 env[envVar] = ldLibraryPath
michael@0 497 elif self.IS_WIN32:
michael@0 498 env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath)
michael@0 499 dmdLibrary = "dmd.dll"
michael@0 500 preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB"
michael@0 501
michael@0 502 if dmdPath and dmdLibrary and preloadEnvVar:
michael@0 503 env['DMD'] = '1'
michael@0 504 env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary)
michael@0 505
michael@0 506 if crashreporter and not debugger:
michael@0 507 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1'
michael@0 508 env['MOZ_CRASHREPORTER'] = '1'
michael@0 509 else:
michael@0 510 env['MOZ_CRASHREPORTER_DISABLE'] = '1'
michael@0 511
michael@0 512 # Crash on non-local network connections.
michael@0 513 env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1'
michael@0 514
michael@0 515 env['GNOME_DISABLE_CRASH_DIALOG'] = '1'
michael@0 516 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1'
michael@0 517 env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
michael@0 518
michael@0 519 # Set WebRTC logging in case it is not set yet
michael@0 520 env.setdefault('NSPR_LOG_MODULES', 'signaling:5,mtransport:3')
michael@0 521 env.setdefault('R_LOG_LEVEL', '5')
michael@0 522 env.setdefault('R_LOG_DESTINATION', 'stderr')
michael@0 523 env.setdefault('R_LOG_VERBOSE', '1')
michael@0 524
michael@0 525 # ASan specific environment stuff
michael@0 526 if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC):
michael@0 527 # Symbolizer support
michael@0 528 llvmsym = os.path.join(xrePath, "llvm-symbolizer")
michael@0 529 if os.path.isfile(llvmsym):
michael@0 530 env["ASAN_SYMBOLIZER_PATH"] = llvmsym
michael@0 531 self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym)
michael@0 532
michael@0 533 try:
michael@0 534 totalMemory = int(os.popen("free").readlines()[1].split()[1])
michael@0 535
michael@0 536 # Only 4 GB RAM or less available? Use custom ASan options to reduce
michael@0 537 # the amount of resources required to do the tests. Standard options
michael@0 538 # will otherwise lead to OOM conditions on the current test slaves.
michael@0 539 if totalMemory <= 1024 * 1024 * 4:
michael@0 540 self.log.info("INFO | automation.py | ASan running in low-memory configuration")
michael@0 541 env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5"
michael@0 542 else:
michael@0 543 self.log.info("INFO | automation.py | ASan running in default memory configuration")
michael@0 544 except OSError,err:
michael@0 545 self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror)
michael@0 546 except:
michael@0 547 self.log.info("Failed determine available memory, disabling ASan low-memory configuration")
michael@0 548
michael@0 549 return env
michael@0 550
michael@0 551 def killPid(self, pid):
michael@0 552 try:
michael@0 553 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM))
michael@0 554 except WindowsError:
michael@0 555 self.log.info("Failed to kill process %d." % pid)
michael@0 556
michael@0 557 if IS_WIN32:
michael@0 558 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe
michael@0 559 GetLastError = ctypes.windll.kernel32.GetLastError
michael@0 560
michael@0 561 def readWithTimeout(self, f, timeout):
michael@0 562 """
michael@0 563 Try to read a line of output from the file object |f|. |f| must be a
michael@0 564 pipe, like the |stdout| member of a subprocess.Popen object created
michael@0 565 with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout|
michael@0 566 is True if the read timed out, and False otherwise. If no output is
michael@0 567 received within |timeout| seconds, returns a blank line.
michael@0 568 """
michael@0 569
michael@0 570 if timeout is None:
michael@0 571 timeout = 0
michael@0 572
michael@0 573 x = msvcrt.get_osfhandle(f.fileno())
michael@0 574 l = ctypes.c_long()
michael@0 575 done = time.time() + timeout
michael@0 576
michael@0 577 buffer = ""
michael@0 578 while timeout == 0 or time.time() < done:
michael@0 579 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0:
michael@0 580 err = self.GetLastError()
michael@0 581 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE
michael@0 582 return ('', False)
michael@0 583 else:
michael@0 584 self.log.error("readWithTimeout got error: %d", err)
michael@0 585 # read a character at a time, checking for eol. Return once we get there.
michael@0 586 index = 0
michael@0 587 while index < l.value:
michael@0 588 char = f.read(1)
michael@0 589 buffer += char
michael@0 590 if char == '\n':
michael@0 591 return (buffer, False)
michael@0 592 index = index + 1
michael@0 593 time.sleep(0.01)
michael@0 594 return (buffer, True)
michael@0 595
michael@0 596 def isPidAlive(self, pid):
michael@0 597 STILL_ACTIVE = 259
michael@0 598 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
michael@0 599 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
michael@0 600 if not pHandle:
michael@0 601 return False
michael@0 602 pExitCode = ctypes.wintypes.DWORD()
michael@0 603 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode))
michael@0 604 ctypes.windll.kernel32.CloseHandle(pHandle)
michael@0 605 return pExitCode.value == STILL_ACTIVE
michael@0 606
michael@0 607 else:
michael@0 608
michael@0 609 def readWithTimeout(self, f, timeout):
michael@0 610 """Try to read a line of output from the file object |f|. If no output
michael@0 611 is received within |timeout| seconds, return a blank line.
michael@0 612 Returns a tuple (line, did_timeout), where |did_timeout| is True
michael@0 613 if the read timed out, and False otherwise."""
michael@0 614 (r, w, e) = select.select([f], [], [], timeout)
michael@0 615 if len(r) == 0:
michael@0 616 return ('', True)
michael@0 617 return (f.readline(), False)
michael@0 618
michael@0 619 def isPidAlive(self, pid):
michael@0 620 try:
michael@0 621 # kill(pid, 0) checks for a valid PID without actually sending a signal
michael@0 622 # The method throws OSError if the PID is invalid, which we catch below.
michael@0 623 os.kill(pid, 0)
michael@0 624
michael@0 625 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if
michael@0 626 # the process terminates before we get to this point.
michael@0 627 wpid, wstatus = os.waitpid(pid, os.WNOHANG)
michael@0 628 return wpid == 0
michael@0 629 except OSError, err:
michael@0 630 # Catch the errors we might expect from os.kill/os.waitpid,
michael@0 631 # and re-raise any others
michael@0 632 if err.errno == errno.ESRCH or err.errno == errno.ECHILD:
michael@0 633 return False
michael@0 634 raise
michael@0 635
michael@0 636 def dumpScreen(self, utilityPath):
michael@0 637 if self.haveDumpedScreen:
michael@0 638 self.log.info("Not taking screenshot here: see the one that was previously logged")
michael@0 639 return
michael@0 640
michael@0 641 self.haveDumpedScreen = True;
michael@0 642 automationutils.dumpScreen(utilityPath)
michael@0 643
michael@0 644
michael@0 645 def killAndGetStack(self, processPID, utilityPath, debuggerInfo):
michael@0 646 """Kill the process, preferrably in a way that gets us a stack trace.
michael@0 647 Also attempts to obtain a screenshot before killing the process."""
michael@0 648 if not debuggerInfo:
michael@0 649 self.dumpScreen(utilityPath)
michael@0 650 self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo)
michael@0 651
michael@0 652 def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo):
michael@0 653 """Kill the process, preferrably in a way that gets us a stack trace."""
michael@0 654 if self.CRASHREPORTER and not debuggerInfo:
michael@0 655 if not self.IS_WIN32:
michael@0 656 # ABRT will get picked up by Breakpad's signal handler
michael@0 657 os.kill(processPID, signal.SIGABRT)
michael@0 658 return
michael@0 659 else:
michael@0 660 # We should have a "crashinject" program in our utility path
michael@0 661 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe"))
michael@0 662 if os.path.exists(crashinject):
michael@0 663 status = subprocess.Popen([crashinject, str(processPID)]).wait()
michael@0 664 automationutils.printstatus(status, "crashinject")
michael@0 665 if status == 0:
michael@0 666 return
michael@0 667 self.log.info("Can't trigger Breakpad, just killing process")
michael@0 668 self.killPid(processPID)
michael@0 669
michael@0 670 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath):
michael@0 671 """ Look for timeout or crashes and return the status after the process terminates """
michael@0 672 stackFixerProcess = None
michael@0 673 stackFixerFunction = None
michael@0 674 didTimeout = False
michael@0 675 hitMaxTime = False
michael@0 676 if proc.stdout is None:
michael@0 677 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection")
michael@0 678 else:
michael@0 679 logsource = proc.stdout
michael@0 680
michael@0 681 if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath):
michael@0 682 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files)
michael@0 683 # This method is preferred for Tinderbox builds, since native symbols may have been stripped.
michael@0 684 sys.path.insert(0, utilityPath)
michael@0 685 import fix_stack_using_bpsyms as stackFixerModule
michael@0 686 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath)
michael@0 687 del sys.path[0]
michael@0 688 elif self.IS_DEBUG_BUILD and self.IS_MAC and False:
michael@0 689 # Run each line through a function in fix_macosx_stack.py (uses atos)
michael@0 690 sys.path.insert(0, utilityPath)
michael@0 691 import fix_macosx_stack as stackFixerModule
michael@0 692 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line)
michael@0 693 del sys.path[0]
michael@0 694 elif self.IS_DEBUG_BUILD and self.IS_LINUX:
michael@0 695 # Run logsource through fix-linux-stack.pl (uses addr2line)
michael@0 696 # This method is preferred for developer machines, so we don't have to run "make buildsymbols".
michael@0 697 stackFixerProcess = self.Process([self.PERL, os.path.join(utilityPath, "fix-linux-stack.pl")],
michael@0 698 stdin=logsource,
michael@0 699 stdout=subprocess.PIPE)
michael@0 700 logsource = stackFixerProcess.stdout
michael@0 701
michael@0 702 # With metro browser runs this script launches the metro test harness which launches the browser.
michael@0 703 # The metro test harness hands back the real browser process id via log output which we need to
michael@0 704 # pick up on and parse out. This variable tracks the real browser process id if we find it.
michael@0 705 browserProcessId = -1
michael@0 706
michael@0 707 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
michael@0 708 while line != "" and not didTimeout:
michael@0 709 if stackFixerFunction:
michael@0 710 line = stackFixerFunction(line)
michael@0 711 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
michael@0 712 if "TEST-START" in line and "|" in line:
michael@0 713 self.lastTestSeen = line.split("|")[1].strip()
michael@0 714 if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line:
michael@0 715 self.dumpScreen(utilityPath)
michael@0 716
michael@0 717 (line, didTimeout) = self.readWithTimeout(logsource, timeout)
michael@0 718
michael@0 719 if "METRO_BROWSER_PROCESS" in line:
michael@0 720 index = line.find("=")
michael@0 721 if index:
michael@0 722 browserProcessId = line[index+1:].rstrip()
michael@0 723 self.log.info("INFO | automation.py | metro browser sub process id detected: %s", browserProcessId)
michael@0 724
michael@0 725 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime):
michael@0 726 # Kill the application, but continue reading from stack fixer so as not to deadlock on stackFixerProcess.wait().
michael@0 727 hitMaxTime = True
michael@0 728 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime))
michael@0 729 self.killAndGetStack(proc.pid, utilityPath, debuggerInfo)
michael@0 730 if didTimeout:
michael@0 731 if line:
michael@0 732 self.log.info(line.rstrip().decode("UTF-8", "ignore"))
michael@0 733 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
michael@0 734 if browserProcessId == -1:
michael@0 735 browserProcessId = proc.pid
michael@0 736 self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo)
michael@0 737
michael@0 738 status = proc.wait()
michael@0 739 automationutils.printstatus(status, "Main app process")
michael@0 740 if status == 0:
michael@0 741 self.lastTestSeen = "Main app process exited normally"
michael@0 742 if status != 0 and not didTimeout and not hitMaxTime:
michael@0 743 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status)
michael@0 744 if stackFixerProcess is not None:
michael@0 745 fixerStatus = stackFixerProcess.wait()
michael@0 746 automationutils.printstatus(status, "stackFixerProcess")
michael@0 747 if fixerStatus != 0 and not didTimeout and not hitMaxTime:
michael@0 748 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Stack fixer process exited with code %d during test run", fixerStatus)
michael@0 749 return status
michael@0 750
michael@0 751 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
michael@0 752 """ build the application command line """
michael@0 753
michael@0 754 cmd = os.path.abspath(app)
michael@0 755 if self.IS_MAC and not self.IS_CAMINO and os.path.exists(cmd + "-bin"):
michael@0 756 # Prefer 'app-bin' in case 'app' is a shell script.
michael@0 757 # We can remove this hack once bug 673899 etc are fixed.
michael@0 758 cmd += "-bin"
michael@0 759
michael@0 760 args = []
michael@0 761
michael@0 762 if debuggerInfo:
michael@0 763 args.extend(debuggerInfo["args"])
michael@0 764 args.append(cmd)
michael@0 765 cmd = os.path.abspath(debuggerInfo["path"])
michael@0 766
michael@0 767 if self.IS_MAC:
michael@0 768 args.append("-foreground")
michael@0 769
michael@0 770 if self.IS_CYGWIN:
michael@0 771 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"")
michael@0 772 else:
michael@0 773 profileDirectory = profileDir + "/"
michael@0 774
michael@0 775 args.extend(("-no-remote", "-profile", profileDirectory))
michael@0 776 if testURL is not None:
michael@0 777 if self.IS_CAMINO:
michael@0 778 args.extend(("-url", testURL))
michael@0 779 else:
michael@0 780 args.append((testURL))
michael@0 781 args.extend(extraArgs)
michael@0 782 return cmd, args
michael@0 783
michael@0 784 def checkForZombies(self, processLog, utilityPath, debuggerInfo):
michael@0 785 """ Look for hung processes """
michael@0 786 if not os.path.exists(processLog):
michael@0 787 self.log.info('Automation Error: PID log not found: %s', processLog)
michael@0 788 # Whilst no hung process was found, the run should still display as a failure
michael@0 789 return True
michael@0 790
michael@0 791 foundZombie = False
michael@0 792 self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog)
michael@0 793 processList = []
michael@0 794 pidRE = re.compile(r'launched child process (\d+)$')
michael@0 795 processLogFD = open(processLog)
michael@0 796 for line in processLogFD:
michael@0 797 self.log.info(line.rstrip())
michael@0 798 m = pidRE.search(line)
michael@0 799 if m:
michael@0 800 processList.append(int(m.group(1)))
michael@0 801 processLogFD.close()
michael@0 802
michael@0 803 for processPID in processList:
michael@0 804 self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID)
michael@0 805 if self.isPidAlive(processPID):
michael@0 806 foundZombie = True
michael@0 807 self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID)
michael@0 808 self.killAndGetStack(processPID, utilityPath, debuggerInfo)
michael@0 809 return foundZombie
michael@0 810
michael@0 811 def checkForCrashes(self, minidumpDir, symbolsPath):
michael@0 812 return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen)
michael@0 813
michael@0 814 def runApp(self, testURL, env, app, profileDir, extraArgs,
michael@0 815 runSSLTunnel = False, utilityPath = None,
michael@0 816 xrePath = None, certPath = None,
michael@0 817 debuggerInfo = None, symbolsPath = None,
michael@0 818 timeout = -1, maxTime = None, onLaunch = None,
michael@0 819 webapprtChrome = False, hide_subtests=None, screenshotOnFail=False):
michael@0 820 """
michael@0 821 Run the app, log the duration it took to execute, return the status code.
michael@0 822 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds.
michael@0 823 """
michael@0 824
michael@0 825 if utilityPath == None:
michael@0 826 utilityPath = self.DIST_BIN
michael@0 827 if xrePath == None:
michael@0 828 xrePath = self.DIST_BIN
michael@0 829 if certPath == None:
michael@0 830 certPath = self.CERTS_SRC_DIR
michael@0 831 if timeout == -1:
michael@0 832 timeout = self.DEFAULT_TIMEOUT
michael@0 833
michael@0 834 # copy env so we don't munge the caller's environment
michael@0 835 env = dict(env);
michael@0 836 env["NO_EM_RESTART"] = "1"
michael@0 837 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
michael@0 838 os.close(tmpfd)
michael@0 839 env["MOZ_PROCESS_LOG"] = processLog
michael@0 840
michael@0 841 if self.IS_TEST_BUILD and runSSLTunnel:
michael@0 842 # create certificate database for the profile
michael@0 843 certificateStatus = self.fillCertificateDB(profileDir, certPath, utilityPath, xrePath)
michael@0 844 if certificateStatus != 0:
michael@0 845 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Certificate integration failed")
michael@0 846 return certificateStatus
michael@0 847
michael@0 848 # start ssltunnel to provide https:// URLs capability
michael@0 849 ssltunnel = os.path.join(utilityPath, "ssltunnel" + self.BIN_SUFFIX)
michael@0 850 ssltunnelProcess = self.Process([ssltunnel,
michael@0 851 os.path.join(profileDir, "ssltunnel.cfg")],
michael@0 852 env = self.environment(xrePath = xrePath))
michael@0 853 self.log.info("INFO | automation.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
michael@0 854
michael@0 855 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs)
michael@0 856 startTime = datetime.now()
michael@0 857
michael@0 858 if debuggerInfo and debuggerInfo["interactive"]:
michael@0 859 # If an interactive debugger is attached, don't redirect output,
michael@0 860 # don't use timeouts, and don't capture ctrl-c.
michael@0 861 timeout = None
michael@0 862 maxTime = None
michael@0 863 outputPipe = None
michael@0 864 signal.signal(signal.SIGINT, lambda sigid, frame: None)
michael@0 865 else:
michael@0 866 outputPipe = subprocess.PIPE
michael@0 867
michael@0 868 self.lastTestSeen = "automation.py"
michael@0 869 proc = self.Process([cmd] + args,
michael@0 870 env = self.environment(env, xrePath = xrePath,
michael@0 871 crashreporter = not debuggerInfo),
michael@0 872 stdout = outputPipe,
michael@0 873 stderr = subprocess.STDOUT)
michael@0 874 self.log.info("INFO | automation.py | Application pid: %d", proc.pid)
michael@0 875
michael@0 876 if onLaunch is not None:
michael@0 877 # Allow callers to specify an onLaunch callback to be fired after the
michael@0 878 # app is launched.
michael@0 879 onLaunch()
michael@0 880
michael@0 881 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath)
michael@0 882 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime))
michael@0 883
michael@0 884 # Do a final check for zombie child processes.
michael@0 885 zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo)
michael@0 886
michael@0 887 crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath)
michael@0 888
michael@0 889 if crashed or zombieProcesses:
michael@0 890 status = 1
michael@0 891
michael@0 892 if os.path.exists(processLog):
michael@0 893 os.unlink(processLog)
michael@0 894
michael@0 895 if self.IS_TEST_BUILD and runSSLTunnel:
michael@0 896 ssltunnelProcess.kill()
michael@0 897
michael@0 898 return status
michael@0 899
michael@0 900 def getExtensionIDFromRDF(self, rdfSource):
michael@0 901 """
michael@0 902 Retrieves the extension id from an install.rdf file (or string).
michael@0 903 """
michael@0 904 from xml.dom.minidom import parse, parseString, Node
michael@0 905
michael@0 906 if isinstance(rdfSource, file):
michael@0 907 document = parse(rdfSource)
michael@0 908 else:
michael@0 909 document = parseString(rdfSource)
michael@0 910
michael@0 911 # Find the <em:id> element. There can be multiple <em:id> tags
michael@0 912 # within <em:targetApplication> tags, so we have to check this way.
michael@0 913 for rdfChild in document.documentElement.childNodes:
michael@0 914 if rdfChild.nodeType == Node.ELEMENT_NODE and rdfChild.tagName == "Description":
michael@0 915 for descChild in rdfChild.childNodes:
michael@0 916 if descChild.nodeType == Node.ELEMENT_NODE and descChild.tagName == "em:id":
michael@0 917 return descChild.childNodes[0].data
michael@0 918
michael@0 919 return None
michael@0 920
michael@0 921 def installExtension(self, extensionSource, profileDir, extensionID = None):
michael@0 922 """
michael@0 923 Copies an extension into the extensions directory of the given profile.
michael@0 924 extensionSource - the source location of the extension files. This can be either
michael@0 925 a directory or a path to an xpi file.
michael@0 926 profileDir - the profile directory we are copying into. We will create the
michael@0 927 "extensions" directory there if it doesn't exist.
michael@0 928 extensionID - the id of the extension to be used as the containing directory for the
michael@0 929 extension, if extensionSource is a directory, i.e.
michael@0 930 this is the name of the folder in the <profileDir>/extensions/<extensionID>
michael@0 931 """
michael@0 932 if not os.path.isdir(profileDir):
michael@0 933 self.log.info("INFO | automation.py | Cannot install extension, invalid profileDir at: %s", profileDir)
michael@0 934 return
michael@0 935
michael@0 936 installRDFFilename = "install.rdf"
michael@0 937
michael@0 938 extensionsRootDir = os.path.join(profileDir, "extensions", "staged")
michael@0 939 if not os.path.isdir(extensionsRootDir):
michael@0 940 os.makedirs(extensionsRootDir)
michael@0 941
michael@0 942 if os.path.isfile(extensionSource):
michael@0 943 reader = automationutils.ZipFileReader(extensionSource)
michael@0 944
michael@0 945 for filename in reader.namelist():
michael@0 946 # Sanity check the zip file.
michael@0 947 if os.path.isabs(filename):
michael@0 948 self.log.info("INFO | automation.py | Cannot install extension, bad files in xpi")
michael@0 949 return
michael@0 950
michael@0 951 # We may need to dig the extensionID out of the zip file...
michael@0 952 if extensionID is None and filename == installRDFFilename:
michael@0 953 extensionID = self.getExtensionIDFromRDF(reader.read(filename))
michael@0 954
michael@0 955 # We must know the extensionID now.
michael@0 956 if extensionID is None:
michael@0 957 self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
michael@0 958 return
michael@0 959
michael@0 960 # Make the extension directory.
michael@0 961 extensionDir = os.path.join(extensionsRootDir, extensionID)
michael@0 962 os.mkdir(extensionDir)
michael@0 963
michael@0 964 # Extract all files.
michael@0 965 reader.extractall(extensionDir)
michael@0 966
michael@0 967 elif os.path.isdir(extensionSource):
michael@0 968 if extensionID is None:
michael@0 969 filename = os.path.join(extensionSource, installRDFFilename)
michael@0 970 if os.path.isfile(filename):
michael@0 971 with open(filename, "r") as installRDF:
michael@0 972 extensionID = self.getExtensionIDFromRDF(installRDF)
michael@0 973
michael@0 974 if extensionID is None:
michael@0 975 self.log.info("INFO | automation.py | Cannot install extension, missing extensionID")
michael@0 976 return
michael@0 977
michael@0 978 # Copy extension tree into its own directory.
michael@0 979 # "destination directory must not already exist".
michael@0 980 shutil.copytree(extensionSource, os.path.join(extensionsRootDir, extensionID))
michael@0 981
michael@0 982 else:
michael@0 983 self.log.info("INFO | automation.py | Cannot install extension, invalid extensionSource at: %s", extensionSource)
michael@0 984
michael@0 985 def elf_arm(self, filename):
michael@0 986 data = open(filename, 'rb').read(20)
michael@0 987 return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM
michael@0 988

mercurial