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