blob: 4027a3133de720d02e2233307b5f27680d3d5c88 [file] [log] [blame]
# 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.
from __future__ import division
import logging
import os
import posixpath
import uuid
import sys
import tempfile
import threading
import time
from datetime import datetime
from py_utils import cloud_storage # pylint: disable=import-error
from telemetry import decorators
from telemetry.core import debug_data
from telemetry.core import exceptions
from telemetry.internal.backends import app_backend
from telemetry.internal.browser import web_contents
from telemetry.internal.results import artifact_logger
from telemetry.util import screenshot
class ExtensionsNotSupportedException(Exception):
pass
class BrowserBackend(app_backend.AppBackend):
"""A base class for browser backends."""
def __init__(self, platform_backend, browser_options,
supports_extensions, tab_list_backend):
assert browser_options.browser_type
super(BrowserBackend, self).__init__(browser_options.browser_type,
platform_backend)
self.browser_options = browser_options
self._supports_extensions = supports_extensions
self._tab_list_backend_class = tab_list_backend
self._dump_finder = None
self._tmp_minidump_dir = tempfile.mkdtemp()
self._symbolized_minidump_paths = set([])
self._periodic_screenshot_timer = None
self._collect_periodic_screenshots = False
def SetBrowser(self, browser):
super(BrowserBackend, self).SetApp(app=browser)
@property
def log_file_path(self):
# Specific browser backend is responsible for overriding this properly.
raise NotImplementedError
def GetLogFileContents(self):
if not self.log_file_path:
return 'No log file'
with open(self.log_file_path) as f:
return f.read()
def UploadLogsToCloudStorage(self):
""" Uploading log files produce by this browser instance to cloud storage.
Check supports_uploading_logs before calling this method.
"""
assert self.supports_uploading_logs
remote_path = (self.browser_options.logs_cloud_remote_path or
'log_%s' % uuid.uuid4())
cloud_url = cloud_storage.Insert(
bucket=self.browser_options.logs_cloud_bucket,
remote_path=remote_path,
local_path=self.log_file_path)
sys.stderr.write('Uploading browser log to %s\n' % cloud_url)
@property
def browser(self):
return self.app
@property
def browser_type(self):
return self.app_type
@property
def screenshot_timeout(self):
return None
@property
def supports_uploading_logs(self):
# Specific browser backend is responsible for overriding this properly.
return False
@property
def supports_extensions(self):
"""True if this browser backend supports extensions."""
return self._supports_extensions
@property
def supports_tab_control(self):
raise NotImplementedError()
@property
@decorators.Cache
def tab_list_backend(self):
return self._tab_list_backend_class(self)
@property
def supports_app_ui_interactions(self):
return False
def Start(self, startup_args):
raise NotImplementedError()
def IsBrowserRunning(self):
raise NotImplementedError()
def IsAppRunning(self):
return self.IsBrowserRunning()
def GetStandardOutput(self):
raise NotImplementedError()
def PullMinidumps(self):
"""Pulls any minidumps off a test device if necessary."""
pass
def CollectDebugData(self, log_level):
"""Collects various information that may be useful for debugging.
Specifically:
1. Captures a screenshot.
2. Collects stdout and system logs.
3. Attempts to symbolize all currently unsymbolized minidumps.
All collected information is stored as artifacts, and everything but the
screenshot is also included in the return value.
Platforms may override this to provide other debug information in addition
to the above set of information.
Args:
log_level: The logging level to use from the logging module, e.g.
logging.ERROR.
Returns:
A debug_data.DebugData object containing the collected data.
"""
suffix = artifact_logger.GetTimestampSuffix()
data = debug_data.DebugData()
self._CollectScreenshot(log_level, suffix + '.png')
self._CollectSystemLog(log_level, suffix + '.txt', data)
self._CollectStdout(log_level, suffix + '.txt', data)
self._SymbolizeAndLogMinidumps(log_level, data)
return data
def StartCollectingPeriodicScreenshots(self, frequency_ms):
self._collect_periodic_screenshots = True
self._CollectPeriodicScreenshots(datetime.now(), frequency_ms)
def StopCollectingPeriodicScreenshots(self):
self._collect_periodic_screenshots = False
self._periodic_screenshot_timer.cancel()
def _CollectPeriodicScreenshots(self, start_time, frequency_ms):
self._CollectScreenshot(logging.INFO, "periodic.png", start_time)
#2To3-division: this line is unchanged as result is expected floats.
self._periodic_screenshot_timer = threading.Timer(
frequency_ms / 1000.0,
self._CollectPeriodicScreenshots,
[start_time, frequency_ms])
if self._collect_periodic_screenshots:
self._periodic_screenshot_timer.start()
def _CollectScreenshot(self, log_level, suffix, start_time=None):
"""Helper function to handle the screenshot portion of CollectDebugData.
Attempts to take a screenshot at the OS level and save it as an artifact.
Args:
log_level: The logging level to use from the logging module, e.g.
logging.ERROR.
suffix: The suffix to prepend to the names of any created artifacts.
start_time: If set, prepend elaped time to screenshot path.
Should be time at which the test started, as a datetime.
This is done here because it may take a nonzero amount of time
to take a screenshot.
"""
screenshot_handle = screenshot.TryCaptureScreenShot(
self.browser.platform, timeout=self.screenshot_timeout)
if screenshot_handle:
with open(screenshot_handle.GetAbsPath(), 'rb') as infile:
if start_time:
# Prepend time since test started to path
test_time = datetime.now() - start_time
suffix = str(test_time.total_seconds()).replace(
'.', '_') + '-' + suffix
artifact_name = posixpath.join(
'debug_screenshots', 'screenshot-%s' % suffix)
logging.log(
log_level, 'Saving screenshot as artifact %s', artifact_name)
artifact_logger.CreateArtifact(artifact_name, infile.read())
else:
logging.log(log_level, 'Failed to capture screenshot')
def _CollectSystemLog(self, log_level, suffix, data):
"""Helper function to handle the system log part of CollectDebugData.
Attempts to retrieve the system log, save it as an artifact, and add it to
the given DebugData object.
Args:
log_level: The logging level to use from the logging module, e.g.
logging.ERROR.
suffix: The suffix to append to the names of any created artifacts.
data: The debug_data.DebugData object to add collected data to.
"""
system_log = self.browser.platform.GetSystemLog()
if system_log is None:
logging.log(log_level, 'Platform did not provide a system log')
return
artifact_name = posixpath.join('system_logs', 'system_log-%s' % suffix)
logging.log(log_level, 'Saving system log as artifact %s', artifact_name)
artifact_logger.CreateArtifact(artifact_name, system_log)
data.system_log = system_log
def _CollectStdout(self, log_level, suffix, data):
"""Helper function to handle the stdout part of CollectDebugData.
Attempts to retrieve stdout, save it as an artifact, and add it to the given
DebugData object.
Args:
log_level: The logging level to use from the logging module, e.g.
logging.ERROR.
suffix: The suffix to append to the names of any created artifacts.
data: The debug_data.DebugData object to add collected data to.
"""
stdout = self.browser.GetStandardOutput()
if stdout is None:
logging.log(log_level, 'Browser did not provide stdout')
return
artifact_name = posixpath.join('stdout', 'stdout-%s' % suffix)
logging.log(log_level, 'Saving stdout as artifact %s', artifact_name)
artifact_logger.CreateArtifact(artifact_name, stdout)
data.stdout = stdout
def _SymbolizeAndLogMinidumps(self, log_level, data):
"""Helper function to handle the minidump portion of CollectDebugData.
Attempts to find all unsymbolized minidumps, symbolize them, save the
results as artifacts, add them to the given DebugData object, and log the
results.
Args:
log_level: The logging level to use from the logging module, e.g.
logging.ERROR.
data: The debug_data.DebugData object to add collected data to.
"""
paths = self.GetAllUnsymbolizedMinidumpPaths()
# It's probable that CollectDebugData() is being called in response to a
# crash. Minidumps are usually written to disk in time, but there's no
# guarantee that is the case. So, if we don't find any minidumps, poll for
# a bit to ensure we don't miss them.
if not paths:
self.browser.GetRecentMinidumpPathWithTimeout(5)
paths = self.GetAllUnsymbolizedMinidumpPaths()
if not paths:
logging.log(log_level, 'No unsymbolized minidump paths')
return
logging.log(log_level, 'Unsymbolized minidump paths: ' + str(paths))
for unsymbolized_path in paths:
minidump_name = os.path.basename(unsymbolized_path)
artifact_name = posixpath.join('unsymbolized_minidumps', minidump_name)
logging.log(log_level, 'Saving minidump as artifact %s', artifact_name)
with open(unsymbolized_path, 'rb') as infile:
artifact_logger.CreateArtifact(artifact_name, infile.read())
valid, output = self.SymbolizeMinidump(unsymbolized_path)
# Store the symbolization attempt as an artifact.
artifact_name = posixpath.join('symbolize_attempts', minidump_name)
logging.log(log_level, 'Saving symbolization attempt as artifact %s',
artifact_name)
artifact_logger.CreateArtifact(artifact_name, output)
if valid:
logging.log(log_level, 'Symbolized minidump:\n%s', output)
data.symbolized_minidumps.append(output)
else:
logging.log(
log_level,
'Minidump symbolization failed, check artifact %s for output',
artifact_name)
def CleanupUnsymbolizedMinidumps(self, fatal=False):
"""Cleans up any unsymbolized minidumps so they aren't found later.
Args:
fatal: Whether the presence of unsymbolized minidumps should be considered
a fatal error or not. Typically, before a test should be non-fatal,
while after a test should be fatal.
"""
log_level = logging.ERROR if fatal else logging.WARNING
unsymbolized_paths = self.GetAllUnsymbolizedMinidumpPaths(log=False)
if not unsymbolized_paths:
return
culprit_test = 'current test' if fatal else 'a previous test'
logging.log(log_level,
'Found %d unsymbolized minidumps leftover from %s. Outputting '
'below: ', len(unsymbolized_paths), culprit_test)
self._SymbolizeAndLogMinidumps(log_level, debug_data.DebugData())
if fatal:
raise RuntimeError(
'Test left unsymbolized minidumps around after finishing.')
def IgnoreMinidump(self, path):
"""Ignores the given minidump, treating it as already symbolized.
Args:
path: The path to the minidump to ignore.
"""
self._symbolized_minidump_paths.add(path)
def GetMostRecentMinidumpPath(self):
"""Gets the most recent minidump that has been written to disk.
Returns:
The path to the most recent minidump on disk, or None if no minidumps are
found.
"""
self.PullMinidumps()
dump_path, explanation = self._dump_finder.GetMostRecentMinidump(
self._tmp_minidump_dir)
logging.info('\n'.join(explanation))
return dump_path
def GetRecentMinidumpPathWithTimeout(self, timeout_s, oldest_ts):
"""Get a path to a recent minidump, blocking until one is available.
Similar to GetMostRecentMinidumpPath, but does not assume that any pending
dumps have been written to disk yet. Instead, waits until a suitably fresh
minidump is found or the timeout is reached.
Args:
timeout_s: The timeout in seconds.
oldest_ts: The oldest allowable timestamp (in seconds since epoch) that a
minidump was created at for it to be considered fresh enough to
return. Defaults to a minute from the current time if not set.
Returns:
None if the timeout is hit or a str containing the path to the found
minidump if a suitable one is found.
"""
assert timeout_s > 0
assert oldest_ts >= 0
explanation = ['No explanation returned.']
start_time = time.time()
try:
while time.time() - start_time < timeout_s:
self.PullMinidumps()
dump_path, explanation = self._dump_finder.GetMostRecentMinidump(
self._tmp_minidump_dir)
if not dump_path or os.path.getmtime(dump_path) < oldest_ts:
continue
return dump_path
return None
finally:
logging.info('\n'.join(explanation))
def GetAllMinidumpPaths(self, log=True):
"""Get all paths to minidumps currently written to disk.
Args:
log: Whether to log the output from looking for minidumps or not.
Returns:
A list of paths to all found minidumps.
"""
self.PullMinidumps()
paths, explanation = self._dump_finder.GetAllMinidumpPaths(
self._tmp_minidump_dir)
if log:
logging.info('\n'.join(explanation))
return paths
def GetAllUnsymbolizedMinidumpPaths(self, log=True):
"""Get all paths to minidumps have have not yet been symbolized.
Args:
log: Whether to log the output from looking for minidumps or not.
Returns:
A list of paths to all found minidumps that have not been symbolized yet.
"""
minidump_paths = set(self.GetAllMinidumpPaths(log=log))
# If we have already symbolized paths remove them from the list
unsymbolized_paths = (
minidump_paths - self._symbolized_minidump_paths)
return list(unsymbolized_paths)
def SymbolizeMinidump(self, minidump_path):
"""Symbolizes the given minidump.
Args:
minidump_path: The path to the minidump to symbolize.
Returns:
A tuple (valid, output). |valid| is True if the minidump was symbolized,
otherwise False. |output| contains an error message if |valid| is False,
otherwise it contains the symbolized minidump.
"""
raise NotImplementedError()
def GetSystemInfo(self):
return None
@property
def supports_memory_dumping(self):
return False
def DumpMemory(self, timeout=None, detail_level=None):
raise NotImplementedError()
# pylint: disable=invalid-name
@property
def supports_overriding_memory_pressure_notifications(self):
return False
def SetMemoryPressureNotificationsSuppressed(
self, suppressed, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
raise NotImplementedError()
def SimulateMemoryPressureNotification(
self, pressure_level, timeout=web_contents.DEFAULT_WEB_CONTENTS_TIMEOUT):
raise NotImplementedError()
@property
def supports_cpu_metrics(self):
raise NotImplementedError()
@property
def supports_memory_metrics(self):
raise NotImplementedError()
@property
def supports_overview_mode(self): # pylint: disable=invalid-name
return False
def EnterOverviewMode(self, timeout): # pylint: disable=unused-argument
raise exceptions.StoryActionError('Overview mode is not supported')
def ExitOverviewMode(self, timeout): # pylint: disable=unused-argument
raise exceptions.StoryActionError('Overview mode is not supported')
def ExecuteBrowserCommand(
self, command_id, timeout): # pylint: disable=unused-argument
raise exceptions.StoryActionError('Execute browser command not supported')
def SetDownloadBehavior(
self, behavior, downloadPath, timeout): # pylint: disable=unused-argument
raise exceptions.StoryActionError('Set download behavior not supported')
def GetUIDevtoolsBackend(self, port): # pylint: disable=unused-argument
raise exceptions.StoryActionError('UI Devtools not supported')