| Index: tools/telemetry/telemetry/internal/backends/chrome/desktop_browser_backend.py
|
| diff --git a/tools/telemetry/telemetry/internal/backends/chrome/desktop_browser_backend.py b/tools/telemetry/telemetry/internal/backends/chrome/desktop_browser_backend.py
|
| deleted file mode 100644
|
| index a7a8f0bf22ecb8b42f61b511af0f1e1759e2d514..0000000000000000000000000000000000000000
|
| --- a/tools/telemetry/telemetry/internal/backends/chrome/desktop_browser_backend.py
|
| +++ /dev/null
|
| @@ -1,571 +0,0 @@
|
| -# Copyright 2013 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -import datetime
|
| -import glob
|
| -import heapq
|
| -import logging
|
| -import os
|
| -import os.path
|
| -import random
|
| -import re
|
| -import shutil
|
| -import subprocess as subprocess
|
| -import sys
|
| -import tempfile
|
| -import time
|
| -
|
| -from catapult_base import cloud_storage # pylint: disable=import-error
|
| -
|
| -
|
| -from telemetry.internal.util import binary_manager
|
| -from telemetry.core import exceptions
|
| -from telemetry.core import util
|
| -from telemetry.internal.backends import browser_backend
|
| -from telemetry.internal.backends.chrome import chrome_browser_backend
|
| -from telemetry.internal.util import path
|
| -
|
| -
|
| -def ParseCrashpadDateTime(date_time_str):
|
| - # Python strptime does not support time zone parsing, strip it.
|
| - date_time_parts = date_time_str.split()
|
| - if len(date_time_parts) >= 3:
|
| - date_time_str = ' '.join(date_time_parts[:2])
|
| - return datetime.datetime.strptime(date_time_str, '%Y-%m-%d %H:%M:%S')
|
| -
|
| -
|
| -def GetSymbolBinary(executable, os_name):
|
| - # Returns binary file where symbols are located.
|
| - if os_name == 'mac':
|
| - version_dir = os.path.join(os.path.dirname(executable),
|
| - '..',
|
| - 'Versions')
|
| - for version_num in os.listdir(version_dir):
|
| - framework_file = os.path.join(version_dir,
|
| - version_num,
|
| - 'Chromium Framework.framework',
|
| - 'Chromium Framework')
|
| - if os.path.isfile(framework_file):
|
| - return framework_file
|
| -
|
| - return executable
|
| -
|
| -
|
| -class DesktopBrowserBackend(chrome_browser_backend.ChromeBrowserBackend):
|
| - """The backend for controlling a locally-executed browser instance, on Linux,
|
| - Mac or Windows.
|
| - """
|
| - def __init__(self, desktop_platform_backend, browser_options, executable,
|
| - flash_path, is_content_shell, browser_directory,
|
| - output_profile_path, extensions_to_load):
|
| - super(DesktopBrowserBackend, self).__init__(
|
| - desktop_platform_backend,
|
| - supports_tab_control=not is_content_shell,
|
| - supports_extensions=not is_content_shell,
|
| - browser_options=browser_options,
|
| - output_profile_path=output_profile_path,
|
| - extensions_to_load=extensions_to_load)
|
| -
|
| - # Initialize fields so that an explosion during init doesn't break in Close.
|
| - self._proc = None
|
| - self._tmp_profile_dir = None
|
| - self._tmp_output_file = None
|
| -
|
| - self._executable = executable
|
| - if not self._executable:
|
| - raise Exception('Cannot create browser, no executable found!')
|
| -
|
| - assert not flash_path or os.path.exists(flash_path)
|
| - self._flash_path = flash_path
|
| -
|
| - self._is_content_shell = is_content_shell
|
| -
|
| - if len(extensions_to_load) > 0 and is_content_shell:
|
| - raise browser_backend.ExtensionsNotSupportedException(
|
| - 'Content shell does not support extensions.')
|
| -
|
| - self._browser_directory = browser_directory
|
| - self._port = None
|
| - self._tmp_minidump_dir = tempfile.mkdtemp()
|
| - self._crash_service = None
|
| - if self.browser_options.enable_logging:
|
| - self._log_file_path = os.path.join(tempfile.mkdtemp(), 'chrome.log')
|
| - else:
|
| - self._log_file_path = None
|
| -
|
| - self._SetupProfile()
|
| -
|
| - @property
|
| - def log_file_path(self):
|
| - return self._log_file_path
|
| -
|
| - @property
|
| - def supports_uploading_logs(self):
|
| - return (self.browser_options.logs_cloud_bucket and self.log_file_path and
|
| - os.path.isfile(self.log_file_path))
|
| -
|
| - def _SetupProfile(self):
|
| - if not self.browser_options.dont_override_profile:
|
| - if self._output_profile_path:
|
| - self._tmp_profile_dir = self._output_profile_path
|
| - else:
|
| - self._tmp_profile_dir = tempfile.mkdtemp()
|
| -
|
| - profile_dir = self.browser_options.profile_dir
|
| - if profile_dir:
|
| - assert self._tmp_profile_dir != profile_dir
|
| - if self._is_content_shell:
|
| - logging.critical('Profiles cannot be used with content shell')
|
| - sys.exit(1)
|
| - logging.info("Using profile directory:'%s'." % profile_dir)
|
| - shutil.rmtree(self._tmp_profile_dir)
|
| - shutil.copytree(profile_dir, self._tmp_profile_dir)
|
| - # No matter whether we're using an existing profile directory or
|
| - # creating a new one, always delete the well-known file containing
|
| - # the active DevTools port number.
|
| - port_file = self._GetDevToolsActivePortPath()
|
| - if os.path.isfile(port_file):
|
| - try:
|
| - os.remove(port_file)
|
| - except Exception as e:
|
| - logging.critical('Unable to remove DevToolsActivePort file: %s' % e)
|
| - sys.exit(1)
|
| -
|
| - def _GetDevToolsActivePortPath(self):
|
| - return os.path.join(self.profile_directory, 'DevToolsActivePort')
|
| -
|
| - def _GetCrashServicePipeName(self):
|
| - # Ensure a unique pipe name by using the name of the temp dir.
|
| - pipe = r'\\.\pipe\%s_service' % os.path.basename(self._tmp_minidump_dir)
|
| - return pipe
|
| -
|
| - def _StartCrashService(self):
|
| - os_name = self.browser.platform.GetOSName()
|
| - if os_name != 'win':
|
| - return None
|
| - arch_name = self.browser.platform.GetArchName()
|
| - command = binary_manager.FetchPath('crash_service', arch_name, os_name)
|
| - if not command:
|
| - logging.warning('crash_service.exe not found for %s %s',
|
| - arch_name, os_name)
|
| - return None
|
| - if not os.path.exists(command):
|
| - logging.warning('crash_service.exe not found for %s %s',
|
| - arch_name, os_name)
|
| - return None
|
| -
|
| - try:
|
| - crash_service = subprocess.Popen([
|
| - command,
|
| - '--no-window',
|
| - '--dumps-dir=%s' % self._tmp_minidump_dir,
|
| - '--pipe-name=%s' % self._GetCrashServicePipeName()])
|
| - except Exception:
|
| - logging.error(
|
| - 'Failed to run %s --no-window --dump-dir=%s --pip-name=%s' % (
|
| - command, self._tmp_minidump_dir, self._GetCrashServicePipeName()))
|
| - logging.error('Running on platform: %s and arch: %s.', os_name, arch_name)
|
| - wmic_stdout, _ = subprocess.Popen(
|
| - ['wmic', 'process', 'get', 'CommandLine,Name,ProcessId,ParentProcessId',
|
| - '/format:csv'], stdout=subprocess.PIPE).communicate()
|
| - logging.error('Current running processes:\n%s' % wmic_stdout)
|
| - raise
|
| - return crash_service
|
| -
|
| - def _GetCdbPath(self):
|
| - possible_paths = (
|
| - 'Debugging Tools For Windows',
|
| - 'Debugging Tools For Windows (x86)',
|
| - 'Debugging Tools For Windows (x64)',
|
| - os.path.join('Windows Kits', '8.0', 'Debuggers', 'x86'),
|
| - os.path.join('Windows Kits', '8.0', 'Debuggers', 'x64'),
|
| - os.path.join('win_toolchain', 'vs2013_files', 'win8sdk', 'Debuggers',
|
| - 'x86'),
|
| - os.path.join('win_toolchain', 'vs2013_files', 'win8sdk', 'Debuggers',
|
| - 'x64'),
|
| - )
|
| - for possible_path in possible_paths:
|
| - app_path = os.path.join(possible_path, 'cdb.exe')
|
| - app_path = path.FindInstalledWindowsApplication(app_path)
|
| - if app_path:
|
| - return app_path
|
| - return None
|
| -
|
| - def HasBrowserFinishedLaunching(self):
|
| - # In addition to the functional check performed by the base class, quickly
|
| - # check if the browser process is still alive.
|
| - if not self.IsBrowserRunning():
|
| - raise exceptions.ProcessGoneException(
|
| - "Return code: %d" % self._proc.returncode)
|
| - # Start DevTools on an ephemeral port and wait for the well-known file
|
| - # containing the port number to exist.
|
| - port_file = self._GetDevToolsActivePortPath()
|
| - if not os.path.isfile(port_file):
|
| - # File isn't ready yet. Return false. Will retry.
|
| - return False
|
| - # Attempt to avoid reading the file until it's populated.
|
| - got_port = False
|
| - try:
|
| - if os.stat(port_file).st_size > 0:
|
| - with open(port_file) as f:
|
| - port_string = f.read()
|
| - self._port = int(port_string)
|
| - logging.info('Discovered ephemeral port %s' % self._port)
|
| - got_port = True
|
| - except Exception:
|
| - # Both stat and open can throw exceptions.
|
| - pass
|
| - if not got_port:
|
| - # File isn't ready yet. Return false. Will retry.
|
| - return False
|
| - return super(DesktopBrowserBackend, self).HasBrowserFinishedLaunching()
|
| -
|
| - def GetBrowserStartupArgs(self):
|
| - args = super(DesktopBrowserBackend, self).GetBrowserStartupArgs()
|
| - self._port = 0
|
| - logging.info('Requested remote debugging port: %d' % self._port)
|
| - args.append('--remote-debugging-port=%i' % self._port)
|
| - args.append('--enable-crash-reporter-for-testing')
|
| - if not self._is_content_shell:
|
| - args.append('--window-size=1280,1024')
|
| - if self._flash_path:
|
| - args.append('--ppapi-flash-path=%s' % self._flash_path)
|
| - if not self.browser_options.dont_override_profile:
|
| - args.append('--user-data-dir=%s' % self._tmp_profile_dir)
|
| - else:
|
| - args.append('--data-path=%s' % self._tmp_profile_dir)
|
| -
|
| - trace_config_file = (self.platform_backend.tracing_controller_backend
|
| - .GetChromeTraceConfigFile())
|
| - if trace_config_file:
|
| - args.append('--trace-config-file=%s' % trace_config_file)
|
| - return args
|
| -
|
| - def Start(self):
|
| - assert not self._proc, 'Must call Close() before Start()'
|
| -
|
| - args = [self._executable]
|
| - args.extend(self.GetBrowserStartupArgs())
|
| - if self.browser_options.startup_url:
|
| - args.append(self.browser_options.startup_url)
|
| - env = os.environ.copy()
|
| - env['CHROME_HEADLESS'] = '1' # Don't upload minidumps.
|
| - env['BREAKPAD_DUMP_LOCATION'] = self._tmp_minidump_dir
|
| - env['CHROME_BREAKPAD_PIPE_NAME'] = self._GetCrashServicePipeName()
|
| - if self.browser_options.enable_logging:
|
| - sys.stderr.write(
|
| - 'Chrome log file will be saved in %s\n' % self.log_file_path)
|
| - env['CHROME_LOG_FILE'] = self.log_file_path
|
| - self._crash_service = self._StartCrashService()
|
| - logging.info('Starting Chrome %s', args)
|
| - if not self.browser_options.show_stdout:
|
| - self._tmp_output_file = tempfile.NamedTemporaryFile('w', 0)
|
| - self._proc = subprocess.Popen(
|
| - args, stdout=self._tmp_output_file, stderr=subprocess.STDOUT, env=env)
|
| - else:
|
| - self._proc = subprocess.Popen(args, env=env)
|
| -
|
| - try:
|
| - self._WaitForBrowserToComeUp()
|
| - # browser is foregrounded by default on Windows and Linux, but not Mac.
|
| - if self.browser.platform.GetOSName() == 'mac':
|
| - subprocess.Popen([
|
| - 'osascript', '-e', ('tell application "%s" to activate' %
|
| - self._executable)])
|
| - self._InitDevtoolsClientBackend()
|
| - if self._supports_extensions:
|
| - self._WaitForExtensionsToLoad()
|
| - except:
|
| - self.Close()
|
| - raise
|
| -
|
| - @property
|
| - def pid(self):
|
| - if self._proc:
|
| - return self._proc.pid
|
| - return None
|
| -
|
| - @property
|
| - def browser_directory(self):
|
| - return self._browser_directory
|
| -
|
| - @property
|
| - def profile_directory(self):
|
| - return self._tmp_profile_dir
|
| -
|
| - def IsBrowserRunning(self):
|
| - return self._proc and self._proc.poll() == None
|
| -
|
| - def GetStandardOutput(self):
|
| - if not self._tmp_output_file:
|
| - if self.browser_options.show_stdout:
|
| - # This can happen in the case that loading the Chrome binary fails.
|
| - # We print rather than using logging here, because that makes a
|
| - # recursive call to this function.
|
| - print >> sys.stderr, "Can't get standard output with --show-stdout"
|
| - return ''
|
| - self._tmp_output_file.flush()
|
| - try:
|
| - with open(self._tmp_output_file.name) as f:
|
| - return f.read()
|
| - except IOError:
|
| - return ''
|
| -
|
| - def _GetMostRecentCrashpadMinidump(self):
|
| - os_name = self.browser.platform.GetOSName()
|
| - arch_name = self.browser.platform.GetArchName()
|
| - crashpad_database_util = binary_manager.FetchPath(
|
| - 'crashpad_database_util', arch_name, os_name)
|
| - if not crashpad_database_util:
|
| - return None
|
| -
|
| - report_output = subprocess.check_output([
|
| - crashpad_database_util, '--database=' + self._tmp_minidump_dir,
|
| - '--show-pending-reports', '--show-completed-reports',
|
| - '--show-all-report-info'])
|
| -
|
| - last_indentation = -1
|
| - reports_list = []
|
| - report_dict = {}
|
| - for report_line in report_output.splitlines():
|
| - # Report values are grouped together by the same indentation level.
|
| - current_indentation = 0
|
| - for report_char in report_line:
|
| - if not report_char.isspace():
|
| - break
|
| - current_indentation += 1
|
| -
|
| - # Decrease in indentation level indicates a new report is being printed.
|
| - if current_indentation >= last_indentation:
|
| - report_key, report_value = report_line.split(':', 1)
|
| - if report_value:
|
| - report_dict[report_key.strip()] = report_value.strip()
|
| - elif report_dict:
|
| - try:
|
| - report_time = ParseCrashpadDateTime(report_dict['Creation time'])
|
| - report_path = report_dict['Path'].strip()
|
| - reports_list.append((report_time, report_path))
|
| - except (ValueError, KeyError) as e:
|
| - logging.warning('Crashpad report expected valid keys'
|
| - ' "Path" and "Creation time": %s', e)
|
| - finally:
|
| - report_dict = {}
|
| -
|
| - last_indentation = current_indentation
|
| -
|
| - # Include the last report.
|
| - if report_dict:
|
| - try:
|
| - report_time = ParseCrashpadDateTime(report_dict['Creation time'])
|
| - report_path = report_dict['Path'].strip()
|
| - reports_list.append((report_time, report_path))
|
| - except (ValueError, KeyError) as e:
|
| - logging.warning('Crashpad report expected valid keys'
|
| - ' "Path" and "Creation time": %s', e)
|
| -
|
| - if reports_list:
|
| - _, most_recent_report_path = max(reports_list)
|
| - return most_recent_report_path
|
| -
|
| - return None
|
| -
|
| - def _GetMostRecentMinidump(self):
|
| - # Crashpad dump layout will be the standard eventually, check it first.
|
| - most_recent_dump = self._GetMostRecentCrashpadMinidump()
|
| -
|
| - # Typical breakpad format is simply dump files in a folder.
|
| - if not most_recent_dump:
|
| - dumps = glob.glob(os.path.join(self._tmp_minidump_dir, '*.dmp'))
|
| - if dumps:
|
| - most_recent_dump = heapq.nlargest(1, dumps, os.path.getmtime)[0]
|
| -
|
| - # As a sanity check, make sure the crash dump is recent.
|
| - if (most_recent_dump and
|
| - os.path.getmtime(most_recent_dump) < (time.time() - (5 * 60))):
|
| - logging.warning('Crash dump is older than 5 minutes. May not be correct.')
|
| -
|
| - return most_recent_dump
|
| -
|
| - def _IsExecutableStripped(self):
|
| - if self.browser.platform.GetOSName() == 'mac':
|
| - try:
|
| - symbols = subprocess.check_output(['/usr/bin/nm', self._executable])
|
| - except subprocess.CalledProcessError as err:
|
| - logging.warning('Error when checking whether executable is stripped: %s'
|
| - % err.output)
|
| - # Just assume that binary is stripped to skip breakpad symbol generation
|
| - # if this check failed.
|
| - return True
|
| - num_symbols = len(symbols.splitlines())
|
| - # We assume that if there are more than 10 symbols the executable is not
|
| - # stripped.
|
| - return num_symbols < 10
|
| - else:
|
| - return False
|
| -
|
| - def _GetStackFromMinidump(self, minidump):
|
| - os_name = self.browser.platform.GetOSName()
|
| - if os_name == 'win':
|
| - cdb = self._GetCdbPath()
|
| - if not cdb:
|
| - logging.warning('cdb.exe not found.')
|
| - return None
|
| - output = subprocess.check_output([cdb, '-y', self._browser_directory,
|
| - '-c', '.ecxr;k30;q', '-z', minidump])
|
| - # cdb output can start the stack with "ChildEBP", "Child-SP", and possibly
|
| - # other things we haven't seen yet. If we can't find the start of the
|
| - # stack, include output from the beginning.
|
| - stack_start = 0
|
| - stack_start_match = re.search("^Child(?:EBP|-SP)", output, re.MULTILINE)
|
| - if stack_start_match:
|
| - stack_start = stack_start_match.start()
|
| - stack_end = output.find('quit:')
|
| - return output[stack_start:stack_end]
|
| -
|
| - arch_name = self.browser.platform.GetArchName()
|
| - stackwalk = binary_manager.FetchPath(
|
| - 'minidump_stackwalk', arch_name, os_name)
|
| - if not stackwalk:
|
| - logging.warning('minidump_stackwalk binary not found.')
|
| - return None
|
| -
|
| - with open(minidump, 'rb') as infile:
|
| - minidump += '.stripped'
|
| - with open(minidump, 'wb') as outfile:
|
| - outfile.write(''.join(infile.read().partition('MDMP')[1:]))
|
| -
|
| - symbols_path = os.path.join(self._tmp_minidump_dir, 'symbols')
|
| -
|
| - symbols = glob.glob(os.path.join(self._browser_directory, '*.breakpad*'))
|
| - if symbols:
|
| - for symbol in sorted(symbols, key=os.path.getmtime, reverse=True):
|
| - if not os.path.isfile(symbol):
|
| - continue
|
| - with open(symbol, 'r') as f:
|
| - fields = f.readline().split()
|
| - if not fields:
|
| - continue
|
| - sha = fields[3]
|
| - binary = ' '.join(fields[4:])
|
| - symbol_path = os.path.join(symbols_path, binary, sha)
|
| - if os.path.exists(symbol_path):
|
| - continue
|
| - os.makedirs(symbol_path)
|
| - shutil.copyfile(symbol, os.path.join(symbol_path, binary + '.sym'))
|
| - else:
|
| -
|
| - # On some platforms generating the symbol table can be very time
|
| - # consuming, skip it if there's nothing to dump.
|
| - if self._IsExecutableStripped():
|
| - logging.info('%s appears to be stripped, skipping symbol dump.' % (
|
| - self._executable))
|
| - return
|
| -
|
| - logging.info('Dumping breakpad symbols.')
|
| - generate_breakpad_symbols_command = binary_manager.FetchPath(
|
| - 'generate_breakpad_symbols', arch_name, os_name)
|
| - if generate_breakpad_symbols_command is None:
|
| - return
|
| -
|
| - cmd = [
|
| - sys.executable,
|
| - generate_breakpad_symbols_command,
|
| - '--binary=%s' % GetSymbolBinary(self._executable,
|
| - self.browser.platform.GetOSName()),
|
| - '--symbols-dir=%s' % symbols_path,
|
| - '--build-dir=%s' % self._browser_directory,
|
| - ]
|
| -
|
| - try:
|
| - subprocess.check_output(cmd, stderr=open(os.devnull, 'w'))
|
| - except subprocess.CalledProcessError:
|
| - logging.warning('Failed to execute "%s"' % ' '.join(cmd))
|
| - return
|
| -
|
| - return subprocess.check_output([stackwalk, minidump, symbols_path],
|
| - stderr=open(os.devnull, 'w'))
|
| -
|
| - def _UploadMinidumpToCloudStorage(self, minidump_path):
|
| - """ Upload minidump_path to cloud storage and return the cloud storage url.
|
| - """
|
| - remote_path = ('minidump-%s-%i.dmp' %
|
| - (datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S'),
|
| - random.randint(0, 1000000)))
|
| - try:
|
| - return cloud_storage.Insert(cloud_storage.TELEMETRY_OUTPUT, remote_path,
|
| - minidump_path)
|
| - except cloud_storage.CloudStorageError as err:
|
| - logging.error('Cloud storage error while trying to upload dump: %s' %
|
| - repr(err))
|
| - return '<Missing link>'
|
| -
|
| - def GetStackTrace(self):
|
| - most_recent_dump = self._GetMostRecentMinidump()
|
| - if not most_recent_dump:
|
| - return 'No crash dump found.'
|
| - logging.info('Minidump found: %s' % most_recent_dump)
|
| - stack = self._GetStackFromMinidump(most_recent_dump)
|
| - if not stack:
|
| - cloud_storage_link = self._UploadMinidumpToCloudStorage(most_recent_dump)
|
| - return ('Failed to symbolize minidump. Raw stack is uploaded to cloud '
|
| - 'storage: %s.' % cloud_storage_link)
|
| - return stack
|
| -
|
| - def __del__(self):
|
| - self.Close()
|
| -
|
| - def _TryCooperativeShutdown(self):
|
| - if self.browser.platform.IsCooperativeShutdownSupported():
|
| - # Ideally there would be a portable, cooperative shutdown
|
| - # mechanism for the browser. This seems difficult to do
|
| - # correctly for all embedders of the content API. The only known
|
| - # problem with unclean shutdown of the browser process is on
|
| - # Windows, where suspended child processes frequently leak. For
|
| - # now, just solve this particular problem. See Issue 424024.
|
| - if self.browser.platform.CooperativelyShutdown(self._proc, "chrome"):
|
| - try:
|
| - util.WaitFor(lambda: not self.IsBrowserRunning(), timeout=5)
|
| - logging.info('Successfully shut down browser cooperatively')
|
| - except exceptions.TimeoutException as e:
|
| - logging.warning('Failed to cooperatively shutdown. ' +
|
| - 'Proceeding to terminate: ' + str(e))
|
| -
|
| - def Close(self):
|
| - super(DesktopBrowserBackend, self).Close()
|
| -
|
| - if self.IsBrowserRunning():
|
| - self._TryCooperativeShutdown()
|
| -
|
| - # Shutdown politely if the profile may be used again.
|
| - if self._output_profile_path and self.IsBrowserRunning():
|
| - self._proc.terminate()
|
| - try:
|
| - util.WaitFor(lambda: not self.IsBrowserRunning(), timeout=5)
|
| - self._proc = None
|
| - except exceptions.TimeoutException:
|
| - logging.warning('Failed to gracefully shutdown. Proceeding to kill.')
|
| -
|
| - # Shutdown aggressively if the above failed or if the profile is temporary.
|
| - if self.IsBrowserRunning():
|
| - self._proc.kill()
|
| - self._proc = None
|
| -
|
| - if self._crash_service:
|
| - self._crash_service.kill()
|
| - self._crash_service = None
|
| -
|
| - if self._output_profile_path:
|
| - # If we need the output then double check that it exists.
|
| - if not (self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir)):
|
| - raise Exception("No profile directory generated by Chrome: '%s'." %
|
| - self._tmp_profile_dir)
|
| - else:
|
| - # If we don't need the profile after the run then cleanup.
|
| - if self._tmp_profile_dir and os.path.exists(self._tmp_profile_dir):
|
| - shutil.rmtree(self._tmp_profile_dir, ignore_errors=True)
|
| - self._tmp_profile_dir = None
|
| -
|
| - if self._tmp_output_file:
|
| - self._tmp_output_file.close()
|
| - self._tmp_output_file = None
|
|
|