desktop_browser_backend.py revision a0e5c0de428e9dea6d07dd57c5594fb1f1c17c20
1# Copyright 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import datetime
6import glob
7import heapq
8import logging
9import os
10import os.path
11import random
12import re
13import shutil
14import subprocess as subprocess
15import sys
16import tempfile
17import time
18
19import py_utils
20from py_utils import cloud_storage  # pylint: disable=import-error
21import dependency_manager  # pylint: disable=import-error
22
23from telemetry.internal.util import binary_manager
24from telemetry.core import exceptions
25from telemetry.internal.backends import browser_backend
26from telemetry.internal.backends.chrome import chrome_browser_backend
27from telemetry.internal.util import path
28
29
30def ParseCrashpadDateTime(date_time_str):
31  # Python strptime does not support time zone parsing, strip it.
32  date_time_parts = date_time_str.split()
33  if len(date_time_parts) >= 3:
34    date_time_str = ' '.join(date_time_parts[:2])
35  return datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S')
36
37
38def GetSymbolBinaries(minidump, arch_name, os_name):
39  # Returns binary file where symbols are located.
40  minidump_dump = binary_manager.FetchPath('minidump_dump', arch_name, os_name)
41  assert minidump_dump
42
43  symbol_binaries = []
44
45  minidump_cmd = [minidump_dump, minidump]
46  try:
47    with open(os.devnull, 'wb') as DEVNULL:
48      minidump_output = subprocess.check_output(minidump_cmd, stderr=DEVNULL)
49  except subprocess.CalledProcessError as e:
50    # For some reason minidump_dump always fails despite successful dumping.
51    minidump_output = e.output
52
53  minidump_binary_re = re.compile(r'\W+\(code_file\)\W+=\W\"(.*)\"')
54  for minidump_line in minidump_output.splitlines():
55    line_match = minidump_binary_re.match(minidump_line)
56    if line_match:
57      binary_path = line_match.group(1)
58      if not os.path.isfile(binary_path):
59        continue
60
61      # Filter out system binaries.
62      if (binary_path.startswith('/usr/lib/') or
63          binary_path.startswith('/System/Library/') or
64          binary_path.startswith('/lib/')):
65        continue
66
67      # Filter out other binary file types which have no symbols.
68      if (binary_path.endswith('.pak') or
69          binary_path.endswith('.bin') or
70          binary_path.endswith('.dat') or
71          binary_path.endswith('.ttf')):
72        continue
73
74      symbol_binaries.append(binary_path)
75  return symbol_binaries
76
77
78def GenerateBreakpadSymbols(minidump, arch, os_name, symbols_dir, browser_dir):
79  logging.info('Dumping breakpad symbols.')
80  generate_breakpad_symbols_command = binary_manager.FetchPath(
81      'generate_breakpad_symbols', arch, os_name)
82  if not generate_breakpad_symbols_command:
83    return
84
85  for binary_path in GetSymbolBinaries(minidump, arch, os_name):
86    cmd = [
87        sys.executable,
88        generate_breakpad_symbols_command,
89        '--binary=%s' % binary_path,
90        '--symbols-dir=%s' % symbols_dir,
91        '--build-dir=%s' % browser_dir,
92        ]
93
94    try:
95      subprocess.check_call(cmd, stderr=open(os.devnull, 'w'))
96    except subprocess.CalledProcessError:
97      logging.warning('Failed to execute "%s"' % ' '.join(cmd))
98      return
99
100
101class DesktopBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
102  """The backend for controlling a locally-executed browser instance, on Linux,
103  Mac or Windows.
104  """
105  def __init__(self, desktop_platform_backend, browser_options, executable,
106               flash_path, is_content_shell, browser_directory):
107    super(DesktopBrowserBackend, self).__init__(
108        desktop_platform_backend,
109        supports_tab_control=not is_content_shell,
110        supports_extensions=not is_content_shell,
111        browser_options=browser_options)
112
113    # Initialize fields so that an explosion during init doesn't break in Close.
114    self._proc = None
115    self._tmp_profile_dir = None
116    self._tmp_output_file = None
117    self._most_recent_symbolized_minidump_paths = set([])
118    self._minidump_path_crashpad_retrieval = {}
119
120    self._executable = executable
121    if not self._executable:
122      raise Exception('Cannot create browser, no executable found!')
123
124    assert not flash_path or os.path.exists(flash_path)
125    self._flash_path = flash_path
126
127    self._is_content_shell = is_content_shell
128
129    extensions_to_load = browser_options.extensions_to_load
130
131    if len(extensions_to_load) > 0 and is_content_shell:
132      raise browser_backend.ExtensionsNotSupportedException(
133          'Content shell does not support extensions.')
134
135    self._browser_directory = browser_directory
136    self._port = None
137    self._tmp_minidump_dir = tempfile.mkdtemp()
138    if self.is_logging_enabled:
139      self._log_file_path = os.path.join(tempfile.mkdtemp(), 'chrome.log')
140    else:
141      self._log_file_path = None
142
143    self._SetupProfile()
144
145  @property
146  def is_logging_enabled(self):
147    return self.browser_options.logging_verbosity in [
148        self.browser_options.NON_VERBOSE_LOGGING,
149        self.browser_options.VERBOSE_LOGGING]
150
151  @property
152  def log_file_path(self):
153    return self._log_file_path
154
155  @property
156  def supports_uploading_logs(self):
157    return (self.browser_options.logs_cloud_bucket and self.log_file_path and
158            os.path.isfile(self.log_file_path))
159
160  def _SetupProfile(self):
161    if not self.browser_options.dont_override_profile:
162      if self._output_profile_path:
163        self._tmp_profile_dir = self._output_profile_path
164      else:
165        self._tmp_profile_dir = tempfile.mkdtemp()
166
167      profile_dir = self.browser_options.profile_dir
168      if profile_dir:
169        assert self._tmp_profile_dir != profile_dir
170        if self._is_content_shell:
171          logging.critical('Profiles cannot be used with content shell')
172          sys.exit(1)
173        logging.info("Using profile directory:'%s'." % profile_dir)
174        shutil.rmtree(self._tmp_profile_dir)
175        shutil.copytree(profile_dir, self._tmp_profile_dir)
176    # No matter whether we're using an existing profile directory or
177    # creating a new one, always delete the well-known file containing
178    # the active DevTools port number.
179    port_file = self._GetDevToolsActivePortPath()
180    if os.path.isfile(port_file):
181      try:
182        os.remove(port_file)
183      except Exception as e:
184        logging.critical('Unable to remove DevToolsActivePort file: %s' % e)
185        sys.exit(1)
186
187  def _GetDevToolsActivePortPath(self):
188    return os.path.join(self.profile_directory, 'DevToolsActivePort')
189
190  def _GetCdbPath(self):
191    # cdb.exe might have been co-located with the browser's executable
192    # during the build, but that's not a certainty. (This is only done
193    # in Chromium builds on the bots, which is why it's not a hard
194    # requirement.) See if it's available.
195    colocated_cdb = os.path.join(self._browser_directory, 'cdb', 'cdb.exe')
196    if path.IsExecutable(colocated_cdb):
197      return colocated_cdb
198    possible_paths = (
199        # Installed copies of the Windows SDK.
200        os.path.join('Windows Kits', '*', 'Debuggers', 'x86'),
201        os.path.join('Windows Kits', '*', 'Debuggers', 'x64'),
202        # Old copies of the Debugging Tools for Windows.
203        'Debugging Tools For Windows',
204        'Debugging Tools For Windows (x86)',
205        'Debugging Tools For Windows (x64)',
206        # The hermetic copy of the Windows toolchain in depot_tools.
207        os.path.join('win_toolchain', 'vs_files', '*', 'win_sdk',
208                     'Debuggers', 'x86'),
209        os.path.join('win_toolchain', 'vs_files', '*', 'win_sdk',
210                     'Debuggers', 'x64'),
211    )
212    for possible_path in possible_paths:
213      app_path = os.path.join(possible_path, 'cdb.exe')
214      app_path = path.FindInstalledWindowsApplication(app_path)
215      if app_path:
216        return app_path
217    return None
218
219  def HasBrowserFinishedLaunching(self):
220    # In addition to the functional check performed by the base class, quickly
221    # check if the browser process is still alive.
222    if not self.IsBrowserRunning():
223      raise exceptions.ProcessGoneException(
224          "Return code: %d" % self._proc.returncode)
225    # Start DevTools on an ephemeral port and wait for the well-known file
226    # containing the port number to exist.
227    port_file = self._GetDevToolsActivePortPath()
228    if not os.path.isfile(port_file):
229      # File isn't ready yet. Return false. Will retry.
230      return False
231    # Attempt to avoid reading the file until it's populated.
232    got_port = False
233    try:
234      if os.stat(port_file).st_size > 0:
235        with open(port_file) as f:
236          port_string = f.read()
237          self._port = int(port_string)
238          logging.info('Discovered ephemeral port %s' % self._port)
239          got_port = True
240    except Exception:
241      # Both stat and open can throw exceptions.
242      pass
243    if not got_port:
244      # File isn't ready yet. Return false. Will retry.
245      return False
246    return super(DesktopBrowserBackend, self).HasBrowserFinishedLaunching()
247
248  def GetBrowserStartupArgs(self):
249    args = super(DesktopBrowserBackend, self).GetBrowserStartupArgs()
250    self._port = 0
251    logging.info('Requested remote debugging port: %d' % self._port)
252    args.append('--remote-debugging-port=%i' % self._port)
253    args.append('--enable-crash-reporter-for-testing')
254    args.append('--disable-component-update')
255    if not self._is_content_shell:
256      args.append('--window-size=1280,1024')
257      if self._flash_path:
258        args.append('--ppapi-flash-path=%s' % self._flash_path)
259        # Also specify the version of Flash as a large version, so that it is
260        # not overridden by the bundled or component-updated version of Flash.
261        args.append('--ppapi-flash-version=99.9.999.999')
262      if not self.browser_options.dont_override_profile:
263        args.append('--user-data-dir=%s' % self._tmp_profile_dir)
264    else:
265      args.append('--data-path=%s' % self._tmp_profile_dir)
266
267    trace_config_file = (self.platform_backend.tracing_controller_backend
268                         .GetChromeTraceConfigFile())
269    if trace_config_file:
270      args.append('--trace-config-file=%s' % trace_config_file)
271    return args
272
273  def Start(self):
274    assert not self._proc, 'Must call Close() before Start()'
275
276    # macOS displays a blocking crash resume dialog that we need to suppress.
277    if self.browser.platform.GetOSName() == 'mac':
278      subprocess.call(['defaults', 'write', '-app', self._executable,
279                       'NSQuitAlwaysKeepsWindows', '-bool', 'false'])
280
281
282    args = [self._executable]
283    args.extend(self.GetBrowserStartupArgs())
284    if self.browser_options.startup_url:
285      args.append(self.browser_options.startup_url)
286    env = os.environ.copy()
287    env['CHROME_HEADLESS'] = '1'  # Don't upload minidumps.
288    env['BREAKPAD_DUMP_LOCATION'] = self._tmp_minidump_dir
289    if self.is_logging_enabled:
290      sys.stderr.write(
291        'Chrome log file will be saved in %s\n' % self.log_file_path)
292      env['CHROME_LOG_FILE'] = self.log_file_path
293    logging.info('Starting Chrome %s', args)
294    if not self.browser_options.show_stdout:
295      self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
296      self._proc = subprocess.Popen(
297          args, stdout=self._tmp_output_file, stderr=subprocess.STDOUT, env=env)
298    else:
299      self._proc = subprocess.Popen(args, env=env)
300
301    try:
302      self._WaitForBrowserToComeUp()
303      # browser is foregrounded by default on Windows and Linux, but not Mac.
304      if self.browser.platform.GetOSName() == 'mac':
305        subprocess.Popen([
306          'osascript', '-e', ('tell application "%s" to activate' %
307                              self._executable)])
308      self._InitDevtoolsClientBackend()
309      if self._supports_extensions:
310        self._WaitForExtensionsToLoad()
311    except:
312      self.Close()
313      raise
314
315  @property
316  def pid(self):
317    if self._proc:
318      return self._proc.pid
319    return None
320
321  @property
322  def browser_directory(self):
323    return self._browser_directory
324
325  @property
326  def profile_directory(self):
327    return self._tmp_profile_dir
328
329  def IsBrowserRunning(self):
330    return self._proc and self._proc.poll() == None
331
332  def GetStandardOutput(self):
333    if not self._tmp_output_file:
334      if self.browser_options.show_stdout:
335        # This can happen in the case that loading the Chrome binary fails.
336        # We print rather than using logging here, because that makes a
337        # recursive call to this function.
338        print >> sys.stderr, "Can't get standard output with --show-stdout"
339      return ''
340    self._tmp_output_file.flush()
341    try:
342      with open(self._tmp_output_file.name) as f:
343        return f.read()
344    except IOError:
345      return ''
346
347  def _MinidumpObtainedFromCrashpad(self, minidump):
348    if minidump in self._minidump_path_crashpad_retrieval:
349      return self._minidump_path_crashpad_retrieval[minidump]
350    # Default to crashpad where we hope to be eventually
351    return True
352
353  def _GetAllCrashpadMinidumps(self):
354    if not self._tmp_minidump_dir:
355      logging.warning('No _tmp_minidump_dir; browser already closed?')
356      return None
357    os_name = self.browser.platform.GetOSName()
358    arch_name = self.browser.platform.GetArchName()
359    try:
360      crashpad_database_util = binary_manager.FetchPath(
361          'crashpad_database_util', arch_name, os_name)
362      if not crashpad_database_util:
363        logging.warning('No crashpad_database_util found')
364        return None
365    except dependency_manager.NoPathFoundError:
366      logging.warning('No path to crashpad_database_util found')
367      return None
368
369    logging.info('Found crashpad_database_util')
370
371    report_output = subprocess.check_output([
372        crashpad_database_util, '--database=' + self._tmp_minidump_dir,
373        '--show-pending-reports', '--show-completed-reports',
374        '--show-all-report-info'])
375
376    last_indentation = -1
377    reports_list = []
378    report_dict = {}
379    for report_line in report_output.splitlines():
380      # Report values are grouped together by the same indentation level.
381      current_indentation = 0
382      for report_char in report_line:
383        if not report_char.isspace():
384          break
385        current_indentation += 1
386
387      # Decrease in indentation level indicates a new report is being printed.
388      if current_indentation >= last_indentation:
389        report_key, report_value = report_line.split(':', 1)
390        if report_value:
391          report_dict[report_key.strip()] = report_value.strip()
392      elif report_dict:
393        try:
394          report_time = ParseCrashpadDateTime(report_dict['Creation time'])
395          report_path = report_dict['Path'].strip()
396          reports_list.append((report_time, report_path))
397        except (ValueError, KeyError) as e:
398          logging.warning('Crashpad report expected valid keys'
399                          ' "Path" and "Creation time": %s', e)
400        finally:
401          report_dict = {}
402
403      last_indentation = current_indentation
404
405    # Include the last report.
406    if report_dict:
407      try:
408        report_time = ParseCrashpadDateTime(report_dict['Creation time'])
409        report_path = report_dict['Path'].strip()
410        reports_list.append((report_time, report_path))
411      except (ValueError, KeyError) as e:
412        logging.warning('Crashpad report expected valid keys'
413                          ' "Path" and "Creation time": %s', e)
414
415    return reports_list
416
417  def _GetMostRecentCrashpadMinidump(self):
418    reports_list = self._GetAllCrashpadMinidumps()
419    if reports_list:
420      _, most_recent_report_path = max(reports_list)
421      return most_recent_report_path
422
423    return None
424
425  def _GetBreakPadMinidumpPaths(self):
426    if not self._tmp_minidump_dir:
427      logging.warning('No _tmp_minidump_dir; browser already closed?')
428      return None
429    return glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp'))
430
431  def _GetMostRecentMinidump(self):
432    # Crashpad dump layout will be the standard eventually, check it first.
433    crashpad_dump = True
434    most_recent_dump = self._GetMostRecentCrashpadMinidump()
435
436    # Typical breakpad format is simply dump files in a folder.
437    if not most_recent_dump:
438      crashpad_dump = False
439      logging.info('No minidump found via crashpad_database_util')
440      dumps = self._GetBreakPadMinidumpPaths()
441      if dumps:
442        most_recent_dump = heapq.nlargest(1, dumps, os.path.getmtime)[0]
443        if most_recent_dump:
444          logging.info('Found minidump via globbing in minidump dir')
445
446    # As a sanity check, make sure the crash dump is recent.
447    if (most_recent_dump and
448        os.path.getmtime(most_recent_dump) < (time.time() - (5 * 60))):
449      logging.warning('Crash dump is older than 5 minutes. May not be correct.')
450
451    self._minidump_path_crashpad_retrieval[most_recent_dump] = crashpad_dump
452    return most_recent_dump
453
454  def _IsExecutableStripped(self):
455    if self.browser.platform.GetOSName() == 'mac':
456      try:
457        symbols = subprocess.check_output(['/usr/bin/nm', self._executable])
458      except subprocess.CalledProcessError as err:
459        logging.warning('Error when checking whether executable is stripped: %s'
460                        % err.output)
461        # Just assume that binary is stripped to skip breakpad symbol generation
462        # if this check failed.
463        return True
464      num_symbols = len(symbols.splitlines())
465      # We assume that if there are more than 10 symbols the executable is not
466      # stripped.
467      return num_symbols < 10
468    else:
469      return False
470
471  def _GetStackFromMinidump(self, minidump):
472    os_name = self.browser.platform.GetOSName()
473    if os_name == 'win':
474      cdb = self._GetCdbPath()
475      if not cdb:
476        logging.warning('cdb.exe not found.')
477        return None
478      # Move to the thread which triggered the exception (".ecxr"). Then include
479      # a description of the exception (".lastevent"). Also include all the
480      # threads' stacks ("~*kb30") as well as the ostensibly crashed stack
481      # associated with the exception context record ("kb30"). Note that stack
482      # dumps, including that for the crashed thread, may not be as precise as
483      # the one starting from the exception context record.
484      # Specify kb instead of k in order to get four arguments listed, for
485      # easier diagnosis from stacks.
486      output = subprocess.check_output([cdb, '-y', self._browser_directory,
487                                        '-c', '.ecxr;.lastevent;kb30;~*kb30;q',
488                                        '-z', minidump])
489      # The output we care about starts with "Last event:" or possibly
490      # other things we haven't seen yet. If we can't find the start of the
491      # last event entry, include output from the beginning.
492      info_start = 0
493      info_start_match = re.search("Last event:", output, re.MULTILINE)
494      if info_start_match:
495        info_start = info_start_match.start()
496      info_end = output.find('quit:')
497      return output[info_start:info_end]
498
499    arch_name = self.browser.platform.GetArchName()
500    stackwalk = binary_manager.FetchPath(
501        'minidump_stackwalk', arch_name, os_name)
502    if not stackwalk:
503      logging.warning('minidump_stackwalk binary not found.')
504      return None
505    # We only want this logic on linux platforms that are still using breakpad.
506    # See crbug.com/667475
507    if not self._MinidumpObtainedFromCrashpad(minidump):
508      with open(minidump, 'rb') as infile:
509        minidump += '.stripped'
510        with open(minidump, 'wb') as outfile:
511          outfile.write(''.join(infile.read().partition('MDMP')[1:]))
512
513    symbols_path = os.path.join(self._tmp_minidump_dir, 'symbols')
514    GenerateBreakpadSymbols(minidump, arch_name, os_name,
515                            symbols_path, self._browser_directory)
516
517    return subprocess.check_output([stackwalk, minidump, symbols_path],
518                                   stderr=open(os.devnull, 'w'))
519
520  def _UploadMinidumpToCloudStorage(self, minidump_path):
521    """ Upload minidump_path to cloud storage and return the cloud storage url.
522    """
523    remote_path = ('minidump-%s-%i.dmp' %
524                   (datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'),
525                    random.randint(0, 1000000)))
526    try:
527      return cloud_storage.Insert(cloud_storage.TELEMETRY_OUTPUT, remote_path,
528                                  minidump_path)
529    except cloud_storage.CloudStorageError as err:
530      logging.error('Cloud storage error while trying to upload dump: %s' %
531                    repr(err))
532      return '<Missing link>'
533
534  def GetStackTrace(self):
535    """Returns a stack trace if a valid minidump is found, will return a tuple
536       (valid, output) where valid will be True if a valid minidump was found
537       and output will contain either an error message or the attempt to
538       symbolize the minidump if one was found.
539    """
540    most_recent_dump = self._GetMostRecentMinidump()
541    if not most_recent_dump:
542      return (False, 'No crash dump found.')
543    logging.info('Minidump found: %s' % most_recent_dump)
544    return self._InternalSymbolizeMinidump(most_recent_dump)
545
546  def GetMostRecentMinidumpPath(self):
547    return self._GetMostRecentMinidump()
548
549  def GetAllMinidumpPaths(self):
550    reports_list = self._GetAllCrashpadMinidumps()
551    if reports_list:
552      for report in reports_list:
553        self._minidump_path_crashpad_retrieval[report[1]] = True
554      return [report[1] for report in reports_list]
555    else:
556      logging.info('No minidump found via crashpad_database_util')
557      dumps = self._GetBreakPadMinidumpPaths()
558      if dumps:
559        logging.info('Found minidump via globbing in minidump dir')
560        for dump in dumps:
561          self._minidump_path_crashpad_retrieval[dump] = False
562        return dumps
563      return []
564
565  def GetAllUnsymbolizedMinidumpPaths(self):
566    minidump_paths = set(self.GetAllMinidumpPaths())
567    # If we have already symbolized paths remove them from the list
568    unsymbolized_paths = (minidump_paths
569      - self._most_recent_symbolized_minidump_paths)
570    return list(unsymbolized_paths)
571
572  def SymbolizeMinidump(self, minidump_path):
573    return self._InternalSymbolizeMinidump(minidump_path)
574
575  def _InternalSymbolizeMinidump(self, minidump_path):
576    cloud_storage_link = self._UploadMinidumpToCloudStorage(minidump_path)
577
578    stack = self._GetStackFromMinidump(minidump_path)
579    if not stack:
580      error_message = ('Failed to symbolize minidump. Raw stack is uploaded to'
581                       ' cloud storage: %s.' % cloud_storage_link)
582      return (False, error_message)
583
584    self._most_recent_symbolized_minidump_paths.add(minidump_path)
585    return (True, stack)
586
587  def __del__(self):
588    self.Close()
589
590  def _TryCooperativeShutdown(self):
591    if self.browser.platform.IsCooperativeShutdownSupported():
592      # Ideally there would be a portable, cooperative shutdown
593      # mechanism for the browser. This seems difficult to do
594      # correctly for all embedders of the content API. The only known
595      # problem with unclean shutdown of the browser process is on
596      # Windows, where suspended child processes frequently leak. For
597      # now, just solve this particular problem. See Issue 424024.
598      if self.browser.platform.CooperativelyShutdown(self._proc, "chrome"):
599        try:
600          py_utils.WaitFor(lambda: not self.IsBrowserRunning(), timeout=5)
601          logging.info('Successfully shut down browser cooperatively')
602        except py_utils.TimeoutException as e:
603          logging.warning('Failed to cooperatively shutdown. ' +
604                          'Proceeding to terminate: ' + str(e))
605
606  def Background(self):
607    raise NotImplementedError
608
609  def Close(self):
610    super(DesktopBrowserBackend, self).Close()
611
612    # First, try to cooperatively shutdown.
613    if self.IsBrowserRunning():
614      self._TryCooperativeShutdown()
615
616    # Second, try to politely shutdown with SIGTERM.
617    if self.IsBrowserRunning():
618      self._proc.terminate()
619      try:
620        py_utils.WaitFor(lambda: not self.IsBrowserRunning(), timeout=5)
621        self._proc = None
622      except py_utils.TimeoutException:
623        logging.warning('Failed to gracefully shutdown.')
624
625    # Shutdown aggressively if all above failed.
626    if self.IsBrowserRunning():
627      logging.warning('Proceed to kill the browser.')
628      self._proc.kill()
629    self._proc = None
630
631    if self._output_profile_path:
632      # If we need the output then double check that it exists.
633      if not (self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir)):
634        raise Exception("No profile directory generated by Chrome: '%s'." %
635            self._tmp_profile_dir)
636    else:
637      # If we don't need the profile after the run then cleanup.
638      if self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir):
639        shutil.rmtree(self._tmp_profile_dir, ignore_errors=True)
640        self._tmp_profile_dir = None
641
642    if self._tmp_output_file:
643      self._tmp_output_file.close()
644      self._tmp_output_file = None
645
646    if self._tmp_minidump_dir:
647      shutil.rmtree(self._tmp_minidump_dir, ignore_errors=True)
648      self._tmp_minidump_dir = None
649