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