1# 2# This Source Code Form is subject to the terms of the Mozilla Public 3# License, v. 2.0. If a copy of the MPL was not distributed with this 4# file, You can obtain one at http://mozilla.org/MPL/2.0/. 5 6from __future__ import with_statement 7import logging 8import os 9import re 10import select 11import signal 12import subprocess 13import sys 14import tempfile 15from datetime import datetime, timedelta 16 17SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(sys.argv[0]))) 18sys.path.insert(0, SCRIPT_DIR) 19 20# -------------------------------------------------------------- 21# TODO: this is a hack for mozbase without virtualenv, remove with bug 849900 22# These paths refer to relative locations to test.zip, not the OBJDIR or SRCDIR 23here = os.path.dirname(os.path.realpath(__file__)) 24mozbase = os.path.realpath(os.path.join(os.path.dirname(here), 'mozbase')) 25 26if os.path.isdir(mozbase): 27 for package in os.listdir(mozbase): 28 package_path = os.path.join(mozbase, package) 29 if package_path not in sys.path: 30 sys.path.append(package_path) 31 32import mozcrash 33from mozscreenshot import printstatus, dump_screen 34 35 36# --------------------------------------------------------------- 37 38_DEFAULT_PREFERENCE_FILE = os.path.join(SCRIPT_DIR, 'prefs_general.js') 39_DEFAULT_APPS_FILE = os.path.join(SCRIPT_DIR, 'webapps_mochitest.json') 40 41_DEFAULT_WEB_SERVER = "127.0.0.1" 42_DEFAULT_HTTP_PORT = 8888 43_DEFAULT_SSL_PORT = 4443 44_DEFAULT_WEBSOCKET_PORT = 9988 45 46# from nsIPrincipal.idl 47_APP_STATUS_NOT_INSTALLED = 0 48_APP_STATUS_INSTALLED = 1 49_APP_STATUS_PRIVILEGED = 2 50_APP_STATUS_CERTIFIED = 3 51 52#expand _DIST_BIN = __XPC_BIN_PATH__ 53#expand _IS_WIN32 = len("__WIN32__") != 0 54#expand _IS_MAC = __IS_MAC__ != 0 55#expand _IS_LINUX = __IS_LINUX__ != 0 56#ifdef IS_CYGWIN 57#expand _IS_CYGWIN = __IS_CYGWIN__ == 1 58#else 59_IS_CYGWIN = False 60#endif 61#expand _BIN_SUFFIX = __BIN_SUFFIX__ 62 63#expand _DEFAULT_APP = "./" + __BROWSER_PATH__ 64#expand _CERTS_SRC_DIR = __CERTS_SRC_DIR__ 65#expand _IS_TEST_BUILD = __IS_TEST_BUILD__ 66#expand _IS_DEBUG_BUILD = __IS_DEBUG_BUILD__ 67#expand _CRASHREPORTER = __CRASHREPORTER__ == 1 68#expand _IS_ASAN = __IS_ASAN__ == 1 69 70 71if _IS_WIN32: 72 import ctypes, ctypes.wintypes, time, msvcrt 73else: 74 import errno 75 76def resetGlobalLog(log): 77 while _log.handlers: 78 _log.removeHandler(_log.handlers[0]) 79 handler = logging.StreamHandler(log) 80 _log.setLevel(logging.INFO) 81 _log.addHandler(handler) 82 83# We use the logging system here primarily because it'll handle multiple 84# threads, which is needed to process the output of the server and application 85# processes simultaneously. 86_log = logging.getLogger() 87resetGlobalLog(sys.stdout) 88 89 90################# 91# PROFILE SETUP # 92################# 93 94class Automation(object): 95 """ 96 Runs the browser from a script, and provides useful utilities 97 for setting up the browser environment. 98 """ 99 100 DIST_BIN = _DIST_BIN 101 IS_WIN32 = _IS_WIN32 102 IS_MAC = _IS_MAC 103 IS_LINUX = _IS_LINUX 104 IS_CYGWIN = _IS_CYGWIN 105 BIN_SUFFIX = _BIN_SUFFIX 106 107 UNIXISH = not IS_WIN32 and not IS_MAC 108 109 DEFAULT_APP = _DEFAULT_APP 110 CERTS_SRC_DIR = _CERTS_SRC_DIR 111 IS_TEST_BUILD = _IS_TEST_BUILD 112 IS_DEBUG_BUILD = _IS_DEBUG_BUILD 113 CRASHREPORTER = _CRASHREPORTER 114 IS_ASAN = _IS_ASAN 115 116 # timeout, in seconds 117 DEFAULT_TIMEOUT = 60.0 118 DEFAULT_WEB_SERVER = _DEFAULT_WEB_SERVER 119 DEFAULT_HTTP_PORT = _DEFAULT_HTTP_PORT 120 DEFAULT_SSL_PORT = _DEFAULT_SSL_PORT 121 DEFAULT_WEBSOCKET_PORT = _DEFAULT_WEBSOCKET_PORT 122 123 def __init__(self): 124 self.log = _log 125 self.lastTestSeen = "automation.py" 126 self.haveDumpedScreen = False 127 128 def setServerInfo(self, 129 webServer = _DEFAULT_WEB_SERVER, 130 httpPort = _DEFAULT_HTTP_PORT, 131 sslPort = _DEFAULT_SSL_PORT, 132 webSocketPort = _DEFAULT_WEBSOCKET_PORT): 133 self.webServer = webServer 134 self.httpPort = httpPort 135 self.sslPort = sslPort 136 self.webSocketPort = webSocketPort 137 138 @property 139 def __all__(self): 140 return [ 141 "UNIXISH", 142 "IS_WIN32", 143 "IS_MAC", 144 "log", 145 "runApp", 146 "Process", 147 "DIST_BIN", 148 "DEFAULT_APP", 149 "CERTS_SRC_DIR", 150 "environment", 151 "IS_TEST_BUILD", 152 "IS_DEBUG_BUILD", 153 "DEFAULT_TIMEOUT", 154 ] 155 156 class Process(subprocess.Popen): 157 """ 158 Represents our view of a subprocess. 159 It adds a kill() method which allows it to be stopped explicitly. 160 """ 161 162 def __init__(self, 163 args, 164 bufsize=0, 165 executable=None, 166 stdin=None, 167 stdout=None, 168 stderr=None, 169 preexec_fn=None, 170 close_fds=False, 171 shell=False, 172 cwd=None, 173 env=None, 174 universal_newlines=False, 175 startupinfo=None, 176 creationflags=0): 177 _log.info("INFO | automation.py | Launching: %s", subprocess.list2cmdline(args)) 178 subprocess.Popen.__init__(self, args, bufsize, executable, 179 stdin, stdout, stderr, 180 preexec_fn, close_fds, 181 shell, cwd, env, 182 universal_newlines, startupinfo, creationflags) 183 self.log = _log 184 185 def kill(self): 186 if Automation().IS_WIN32: 187 import platform 188 pid = "%i" % self.pid 189 if platform.release() == "2000": 190 # Windows 2000 needs 'kill.exe' from the 191 #'Windows 2000 Resource Kit tools'. (See bug 475455.) 192 try: 193 subprocess.Popen(["kill", "-f", pid]).wait() 194 except: 195 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Missing 'kill' utility to kill process with pid=%s. Kill it manually!", pid) 196 else: 197 # Windows XP and later. 198 subprocess.Popen(["taskkill", "/F", "/PID", pid]).wait() 199 else: 200 os.kill(self.pid, signal.SIGKILL) 201 202 def environment(self, env=None, xrePath=None, crashreporter=True, debugger=False, dmdPath=None, lsanPath=None): 203 if xrePath == None: 204 xrePath = self.DIST_BIN 205 if env == None: 206 env = dict(os.environ) 207 208 ldLibraryPath = os.path.abspath(os.path.join(SCRIPT_DIR, xrePath)) 209 dmdLibrary = None 210 preloadEnvVar = None 211 if self.UNIXISH or self.IS_MAC: 212 envVar = "LD_LIBRARY_PATH" 213 preloadEnvVar = "LD_PRELOAD" 214 if self.IS_MAC: 215 envVar = "DYLD_LIBRARY_PATH" 216 dmdLibrary = "libdmd.dylib" 217 else: # unixish 218 env['MOZILLA_FIVE_HOME'] = xrePath 219 dmdLibrary = "libdmd.so" 220 if envVar in env: 221 ldLibraryPath = ldLibraryPath + ":" + env[envVar] 222 env[envVar] = ldLibraryPath 223 elif self.IS_WIN32: 224 env["PATH"] = env["PATH"] + ";" + str(ldLibraryPath) 225 dmdLibrary = "dmd.dll" 226 preloadEnvVar = "MOZ_REPLACE_MALLOC_LIB" 227 228 if dmdPath and dmdLibrary and preloadEnvVar: 229 env[preloadEnvVar] = os.path.join(dmdPath, dmdLibrary) 230 231 if crashreporter and not debugger: 232 env['MOZ_CRASHREPORTER_NO_REPORT'] = '1' 233 env['MOZ_CRASHREPORTER'] = '1' 234 else: 235 env['MOZ_CRASHREPORTER_DISABLE'] = '1' 236 237 # Crash on non-local network connections by default. 238 # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily 239 # enable non-local connections for the purposes of local testing. Don't 240 # override the user's choice here. See bug 1049688. 241 env.setdefault('MOZ_DISABLE_NONLOCAL_CONNECTIONS', '1') 242 243 env['GNOME_DISABLE_CRASH_DIALOG'] = '1' 244 env['XRE_NO_WINDOWS_CRASH_DIALOG'] = '1' 245 246 # Set WebRTC logging in case it is not set yet 247 env.setdefault('MOZ_LOG', 'signaling:3,mtransport:4,DataChannel:4,jsep:4,MediaPipelineFactory:4') 248 env.setdefault('R_LOG_LEVEL', '6') 249 env.setdefault('R_LOG_DESTINATION', 'stderr') 250 env.setdefault('R_LOG_VERBOSE', '1') 251 252 # ASan specific environment stuff 253 if self.IS_ASAN and (self.IS_LINUX or self.IS_MAC): 254 # Symbolizer support 255 llvmsym = os.path.join(xrePath, "llvm-symbolizer") 256 if os.path.isfile(llvmsym): 257 env["ASAN_SYMBOLIZER_PATH"] = llvmsym 258 self.log.info("INFO | automation.py | ASan using symbolizer at %s", llvmsym) 259 else: 260 self.log.info("TEST-UNEXPECTED-FAIL | automation.py | Failed to find ASan symbolizer at %s", llvmsym) 261 262 try: 263 totalMemory = int(os.popen("free").readlines()[1].split()[1]) 264 265 # Only 4 GB RAM or less available? Use custom ASan options to reduce 266 # the amount of resources required to do the tests. Standard options 267 # will otherwise lead to OOM conditions on the current test slaves. 268 if totalMemory <= 1024 * 1024 * 4: 269 self.log.info("INFO | automation.py | ASan running in low-memory configuration") 270 env["ASAN_OPTIONS"] = "quarantine_size=50331648:malloc_context_size=5" 271 else: 272 self.log.info("INFO | automation.py | ASan running in default memory configuration") 273 except OSError,err: 274 self.log.info("Failed determine available memory, disabling ASan low-memory configuration: %s", err.strerror) 275 except: 276 self.log.info("Failed determine available memory, disabling ASan low-memory configuration") 277 278 return env 279 280 def killPid(self, pid): 281 try: 282 os.kill(pid, getattr(signal, "SIGKILL", signal.SIGTERM)) 283 except WindowsError: 284 self.log.info("Failed to kill process %d." % pid) 285 286 if IS_WIN32: 287 PeekNamedPipe = ctypes.windll.kernel32.PeekNamedPipe 288 GetLastError = ctypes.windll.kernel32.GetLastError 289 290 def readWithTimeout(self, f, timeout): 291 """ 292 Try to read a line of output from the file object |f|. |f| must be a 293 pipe, like the |stdout| member of a subprocess.Popen object created 294 with stdout=PIPE. Returns a tuple (line, did_timeout), where |did_timeout| 295 is True if the read timed out, and False otherwise. If no output is 296 received within |timeout| seconds, returns a blank line. 297 """ 298 299 if timeout is None: 300 timeout = 0 301 302 x = msvcrt.get_osfhandle(f.fileno()) 303 l = ctypes.c_long() 304 done = time.time() + timeout 305 306 buffer = "" 307 while timeout == 0 or time.time() < done: 308 if self.PeekNamedPipe(x, None, 0, None, ctypes.byref(l), None) == 0: 309 err = self.GetLastError() 310 if err == 38 or err == 109: # ERROR_HANDLE_EOF || ERROR_BROKEN_PIPE 311 return ('', False) 312 else: 313 self.log.error("readWithTimeout got error: %d", err) 314 # read a character at a time, checking for eol. Return once we get there. 315 index = 0 316 while index < l.value: 317 char = f.read(1) 318 buffer += char 319 if char == '\n': 320 return (buffer, False) 321 index = index + 1 322 time.sleep(0.01) 323 return (buffer, True) 324 325 def isPidAlive(self, pid): 326 STILL_ACTIVE = 259 327 PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 328 pHandle = ctypes.windll.kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) 329 if not pHandle: 330 return False 331 pExitCode = ctypes.wintypes.DWORD() 332 ctypes.windll.kernel32.GetExitCodeProcess(pHandle, ctypes.byref(pExitCode)) 333 ctypes.windll.kernel32.CloseHandle(pHandle) 334 return pExitCode.value == STILL_ACTIVE 335 336 else: 337 338 def readWithTimeout(self, f, timeout): 339 """Try to read a line of output from the file object |f|. If no output 340 is received within |timeout| seconds, return a blank line. 341 Returns a tuple (line, did_timeout), where |did_timeout| is True 342 if the read timed out, and False otherwise.""" 343 (r, w, e) = select.select([f], [], [], timeout) 344 if len(r) == 0: 345 return ('', True) 346 return (f.readline(), False) 347 348 def isPidAlive(self, pid): 349 try: 350 # kill(pid, 0) checks for a valid PID without actually sending a signal 351 # The method throws OSError if the PID is invalid, which we catch below. 352 os.kill(pid, 0) 353 354 # Wait on it to see if it's a zombie. This can throw OSError.ECHILD if 355 # the process terminates before we get to this point. 356 wpid, wstatus = os.waitpid(pid, os.WNOHANG) 357 return wpid == 0 358 except OSError, err: 359 # Catch the errors we might expect from os.kill/os.waitpid, 360 # and re-raise any others 361 if err.errno == errno.ESRCH or err.errno == errno.ECHILD: 362 return False 363 raise 364 365 def dumpScreen(self, utilityPath): 366 if self.haveDumpedScreen: 367 self.log.info("Not taking screenshot here: see the one that was previously logged") 368 return 369 370 self.haveDumpedScreen = True; 371 dump_screen(utilityPath, self.log) 372 373 374 def killAndGetStack(self, processPID, utilityPath, debuggerInfo): 375 """Kill the process, preferrably in a way that gets us a stack trace. 376 Also attempts to obtain a screenshot before killing the process.""" 377 if not debuggerInfo: 378 self.dumpScreen(utilityPath) 379 self.killAndGetStackNoScreenshot(processPID, utilityPath, debuggerInfo) 380 381 def killAndGetStackNoScreenshot(self, processPID, utilityPath, debuggerInfo): 382 """Kill the process, preferrably in a way that gets us a stack trace.""" 383 if self.CRASHREPORTER and not debuggerInfo: 384 if not self.IS_WIN32: 385 # ABRT will get picked up by Breakpad's signal handler 386 os.kill(processPID, signal.SIGABRT) 387 return 388 else: 389 # We should have a "crashinject" program in our utility path 390 crashinject = os.path.normpath(os.path.join(utilityPath, "crashinject.exe")) 391 if os.path.exists(crashinject): 392 status = subprocess.Popen([crashinject, str(processPID)]).wait() 393 printstatus("crashinject", status) 394 if status == 0: 395 return 396 self.log.info("Can't trigger Breakpad, just killing process") 397 self.killPid(processPID) 398 399 def waitForFinish(self, proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, outputHandler=None): 400 """ Look for timeout or crashes and return the status after the process terminates """ 401 stackFixerFunction = None 402 didTimeout = False 403 hitMaxTime = False 404 if proc.stdout is None: 405 self.log.info("TEST-INFO: Not logging stdout or stderr due to debugger connection") 406 else: 407 logsource = proc.stdout 408 409 if self.IS_DEBUG_BUILD and symbolsPath and os.path.exists(symbolsPath): 410 # Run each line through a function in fix_stack_using_bpsyms.py (uses breakpad symbol files) 411 # This method is preferred for Tinderbox builds, since native symbols may have been stripped. 412 sys.path.insert(0, utilityPath) 413 import fix_stack_using_bpsyms as stackFixerModule 414 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line, symbolsPath) 415 del sys.path[0] 416 elif self.IS_DEBUG_BUILD and self.IS_MAC: 417 # Run each line through a function in fix_macosx_stack.py (uses atos) 418 sys.path.insert(0, utilityPath) 419 import fix_macosx_stack as stackFixerModule 420 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line) 421 del sys.path[0] 422 elif self.IS_DEBUG_BUILD and self.IS_LINUX: 423 # Run each line through a function in fix_linux_stack.py (uses addr2line) 424 # This method is preferred for developer machines, so we don't have to run "make buildsymbols". 425 sys.path.insert(0, utilityPath) 426 import fix_linux_stack as stackFixerModule 427 stackFixerFunction = lambda line: stackFixerModule.fixSymbols(line) 428 del sys.path[0] 429 430 # With metro browser runs this script launches the metro test harness which launches the browser. 431 # The metro test harness hands back the real browser process id via log output which we need to 432 # pick up on and parse out. This variable tracks the real browser process id if we find it. 433 browserProcessId = -1 434 435 (line, didTimeout) = self.readWithTimeout(logsource, timeout) 436 while line != "" and not didTimeout: 437 if stackFixerFunction: 438 line = stackFixerFunction(line) 439 440 if outputHandler is None: 441 self.log.info(line.rstrip().decode("UTF-8", "ignore")) 442 else: 443 outputHandler(line) 444 445 if "TEST-START" in line and "|" in line: 446 self.lastTestSeen = line.split("|")[1].strip() 447 if not debuggerInfo and "TEST-UNEXPECTED-FAIL" in line and "Test timed out" in line: 448 self.dumpScreen(utilityPath) 449 450 (line, didTimeout) = self.readWithTimeout(logsource, timeout) 451 452 if not hitMaxTime and maxTime and datetime.now() - startTime > timedelta(seconds = maxTime): 453 # Kill the application. 454 hitMaxTime = True 455 self.log.info("TEST-UNEXPECTED-FAIL | %s | application ran for longer than allowed maximum time of %d seconds", self.lastTestSeen, int(maxTime)) 456 self.log.error("Force-terminating active process(es)."); 457 self.killAndGetStack(proc.pid, utilityPath, debuggerInfo) 458 if didTimeout: 459 if line: 460 self.log.info(line.rstrip().decode("UTF-8", "ignore")) 461 self.log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout)) 462 self.log.error("Force-terminating active process(es)."); 463 if browserProcessId == -1: 464 browserProcessId = proc.pid 465 self.killAndGetStack(browserProcessId, utilityPath, debuggerInfo) 466 467 status = proc.wait() 468 printstatus("Main app process", status) 469 if status == 0: 470 self.lastTestSeen = "Main app process exited normally" 471 if status != 0 and not didTimeout and not hitMaxTime: 472 self.log.info("TEST-UNEXPECTED-FAIL | %s | Exited with code %d during test run", self.lastTestSeen, status) 473 return status 474 475 def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs): 476 """ build the application command line """ 477 478 cmd = os.path.abspath(app) 479 if self.IS_MAC and os.path.exists(cmd + "-bin"): 480 # Prefer 'app-bin' in case 'app' is a shell script. 481 # We can remove this hack once bug 673899 etc are fixed. 482 cmd += "-bin" 483 484 args = [] 485 486 if debuggerInfo: 487 args.extend(debuggerInfo.args) 488 args.append(cmd) 489 cmd = os.path.abspath(debuggerInfo.path) 490 491 if self.IS_MAC: 492 args.append("-foreground") 493 494 if self.IS_CYGWIN: 495 profileDirectory = commands.getoutput("cygpath -w \"" + profileDir + "/\"") 496 else: 497 profileDirectory = profileDir + "/" 498 499 args.extend(("-no-remote", "-profile", profileDirectory)) 500 if testURL is not None: 501 args.append((testURL)) 502 args.extend(extraArgs) 503 return cmd, args 504 505 def checkForZombies(self, processLog, utilityPath, debuggerInfo): 506 """ Look for hung processes """ 507 if not os.path.exists(processLog): 508 self.log.info('Automation Error: PID log not found: %s', processLog) 509 # Whilst no hung process was found, the run should still display as a failure 510 return True 511 512 foundZombie = False 513 self.log.info('INFO | zombiecheck | Reading PID log: %s', processLog) 514 processList = [] 515 pidRE = re.compile(r'launched child process (\d+)$') 516 processLogFD = open(processLog) 517 for line in processLogFD: 518 self.log.info(line.rstrip()) 519 m = pidRE.search(line) 520 if m: 521 processList.append(int(m.group(1))) 522 processLogFD.close() 523 524 for processPID in processList: 525 self.log.info("INFO | zombiecheck | Checking for orphan process with PID: %d", processPID) 526 if self.isPidAlive(processPID): 527 foundZombie = True 528 self.log.info("TEST-UNEXPECTED-FAIL | zombiecheck | child process %d still alive after shutdown", processPID) 529 self.killAndGetStack(processPID, utilityPath, debuggerInfo) 530 return foundZombie 531 532 def checkForCrashes(self, minidumpDir, symbolsPath): 533 return mozcrash.check_for_crashes(minidumpDir, symbolsPath, test_name=self.lastTestSeen) 534 535 def runApp(self, testURL, env, app, profileDir, extraArgs, utilityPath = None, 536 xrePath = None, certPath = None, 537 debuggerInfo = None, symbolsPath = None, 538 timeout = -1, maxTime = None, onLaunch = None, 539 detectShutdownLeaks = False, screenshotOnFail=False, testPath=None, bisectChunk=None, 540 valgrindPath=None, valgrindArgs=None, valgrindSuppFiles=None, outputHandler=None): 541 """ 542 Run the app, log the duration it took to execute, return the status code. 543 Kills the app if it runs for longer than |maxTime| seconds, or outputs nothing for |timeout| seconds. 544 """ 545 546 if utilityPath == None: 547 utilityPath = self.DIST_BIN 548 if xrePath == None: 549 xrePath = self.DIST_BIN 550 if certPath == None: 551 certPath = self.CERTS_SRC_DIR 552 if timeout == -1: 553 timeout = self.DEFAULT_TIMEOUT 554 555 # copy env so we don't munge the caller's environment 556 env = dict(env); 557 env["NO_EM_RESTART"] = "1" 558 tmpfd, processLog = tempfile.mkstemp(suffix='pidlog') 559 os.close(tmpfd) 560 env["MOZ_PROCESS_LOG"] = processLog 561 562 563 cmd, args = self.buildCommandLine(app, debuggerInfo, profileDir, testURL, extraArgs) 564 startTime = datetime.now() 565 566 if debuggerInfo and debuggerInfo.interactive: 567 # If an interactive debugger is attached, don't redirect output, 568 # don't use timeouts, and don't capture ctrl-c. 569 timeout = None 570 maxTime = None 571 outputPipe = None 572 signal.signal(signal.SIGINT, lambda sigid, frame: None) 573 else: 574 outputPipe = subprocess.PIPE 575 576 self.lastTestSeen = "automation.py" 577 proc = self.Process([cmd] + args, 578 env = self.environment(env, xrePath = xrePath, 579 crashreporter = not debuggerInfo), 580 stdout = outputPipe, 581 stderr = subprocess.STDOUT) 582 self.log.info("INFO | automation.py | Application pid: %d", proc.pid) 583 584 if onLaunch is not None: 585 # Allow callers to specify an onLaunch callback to be fired after the 586 # app is launched. 587 onLaunch() 588 589 status = self.waitForFinish(proc, utilityPath, timeout, maxTime, startTime, debuggerInfo, symbolsPath, 590 outputHandler=outputHandler) 591 self.log.info("INFO | automation.py | Application ran for: %s", str(datetime.now() - startTime)) 592 593 # Do a final check for zombie child processes. 594 zombieProcesses = self.checkForZombies(processLog, utilityPath, debuggerInfo) 595 596 crashed = self.checkForCrashes(os.path.join(profileDir, "minidumps"), symbolsPath) 597 598 if crashed or zombieProcesses: 599 status = 1 600 601 if os.path.exists(processLog): 602 os.unlink(processLog) 603 604 return status 605 606 def elf_arm(self, filename): 607 data = open(filename, 'rb').read(20) 608 return data[:4] == "\x7fELF" and ord(data[18]) == 40 # EM_ARM 609 610