blob: 38f6b36f2ee59a3539ee0b3c9d0092825df3dca2 [file] [log] [blame]
# Copyright 2014 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 json
import logging
import os
import platform
import shutil
import socket
import sys
import tempfile
import time
import urllib2
import zipfile
from telemetry.page import profile_creator
import page_sets
def _ExternalExtensionsPath():
"""Returns the OS-dependent path at which to install the extension deployment
files"""
if platform.system() == 'Darwin':
return os.path.join('/Library', 'Application Support', 'Google', 'Chrome',
'External Extensions')
elif platform.system() == 'Linux':
return os.path.join('/opt', 'google', 'chrome', 'extensions' )
else:
raise NotImplementedError('Extension install on %s is not yet supported' %
platform.system())
def _DownloadExtension(extension_id, output_dir):
"""Download an extension to disk.
Args:
extension_id: the extension id.
output_dir: Directory to download into.
Returns:
Extension file downloaded."""
extension_download_path = os.path.join(output_dir, "%s.crx" % extension_id)
extension_url = (
"https://clients2.google.com/service/update2/crx?response=redirect"
"&x=id%%3D%s%%26lang%%3Den-US%%26uc" % extension_id)
response = urllib2.urlopen(extension_url)
assert(response.getcode() == 200)
with open(extension_download_path, "w") as f:
f.write(response.read())
return extension_download_path
def _GetExtensionInfoFromCRX(crx_path):
"""Parse an extension archive and return information.
Note:
The extension name returned by this function may not be valid
(e.g. in the case of a localized extension name). It's use is just
meant to be informational.
Args:
crx_path: path to crx archive to look at.
Returns:
Tuple consisting of:
(crx_version, extension_name)"""
crx_zip = zipfile.ZipFile(crx_path)
manifest_contents = crx_zip.read('manifest.json')
decoded_manifest = json.loads(manifest_contents)
crx_version = decoded_manifest['version']
extension_name = decoded_manifest['name']
return (crx_version, extension_name)
class ExtensionsProfileCreator(profile_creator.ProfileCreator):
"""Virtual base class for profile creators that install extensions.
Extensions are installed using the mechanism described in
https://developer.chrome.com/extensions/external_extensions.html .
Subclasses are meant to be run interactively.
"""
def __init__(self):
super(ExtensionsProfileCreator, self).__init__()
self._page_set = page_sets.Typical25()
# Directory into which the output profile is written.
self._output_profile_path = None
# List of extensions to install.
self._extensions_to_install = []
# Theme to install (if any).
self._theme_to_install = None
# Directory to download extension files into.
self._extension_download_dir = None
# Have the extensions been installed yet?
self._extensions_installed = False
# List of files to delete after run.
self._files_to_cleanup = []
def _PrepareExtensionInstallFiles(self):
"""Download extension archives and create extension install files."""
extensions_to_install = self._extensions_to_install
if self._theme_to_install:
extensions_to_install = extensions_to_install + [self._theme_to_install]
num_extensions = len(extensions_to_install)
if not num_extensions:
raise ValueError("No extensions or themes to install:",
extensions_to_install)
# Create external extensions path if it doesn't exist already.
external_extensions_dir = _ExternalExtensionsPath()
if not os.path.isdir(external_extensions_dir):
os.makedirs(external_extensions_dir)
self._extension_download_dir = tempfile.mkdtemp()
for i in xrange(num_extensions):
extension_id = extensions_to_install[i]
logging.info("Downloading %s - %d/%d" % (
extension_id, (i + 1), num_extensions))
extension_path = _DownloadExtension(extension_id,
self._extension_download_dir)
(version, name) = _GetExtensionInfoFromCRX(extension_path)
extension_info = {'external_crx' : extension_path,
'external_version' : version,
'_comment' : name}
extension_json_path = os.path.join(external_extensions_dir,
"%s.json" % extension_id)
with open(extension_json_path, 'w') as f:
f.write(json.dumps(extension_info))
self._files_to_cleanup.append(extension_json_path)
def _CleanupExtensionInstallFiles(self):
"""Cleanup stray files before exiting."""
logging.info("Cleaning up stray files")
for filename in self._files_to_cleanup:
os.remove(filename)
if self._extension_download_dir:
# Simple sanity check to lessen the impact of a stray rmtree().
if len(self._extension_download_dir.split(os.sep)) < 3:
raise Exception("Path too shallow: %s" % self._extension_download_dir)
shutil.rmtree(self._extension_download_dir)
self._extension_download_dir = None
def CustomizeBrowserOptions(self, options):
self._output_profile_path = options.output_profile_path
def WillRunTest(self, options):
"""Run before browser starts.
Download extensions and write installation files."""
super(ExtensionsProfileCreator, self).WillRunTest(options)
# Running this script on a corporate network or other managed environment
# could potentially alter the profile contents.
hostname = socket.gethostname()
if hostname.endswith('corp.google.com'):
raise Exception("It appears you are connected to a corporate network "
"(hostname=%s). This script needs to be run off the corp "
"network." % hostname)
prompt = ("\n!!!This script must be run on a fresh OS installation, "
"disconnected from any corporate network. Are you sure you want to "
"continue? (y/N) ")
if (raw_input(prompt).lower() != 'y'):
sys.exit(-1)
self._PrepareExtensionInstallFiles()
def DidRunTest(self, browser, results):
"""Run before exit."""
super(ExtensionsProfileCreator, self).DidRunTest()
# Do some basic sanity checks to make sure the profile is complete.
installed_extensions = browser.extensions.keys()
if not len(installed_extensions) == len(self._extensions_to_install):
# Diagnosing errors:
# Too many extensions: Managed environment may be installing additional
# extensions.
raise Exception("Unexpected number of extensions installed in browser",
installed_extensions)
# Check that files on this list exist and have content.
expected_files = [
os.path.join('Default', 'Network Action Predictor')]
for filename in expected_files:
filename = os.path.join(self._output_profile_path, filename)
if not os.path.getsize(filename) > 0:
raise Exception("Profile not complete: %s is zero length." % filename)
self._CleanupExtensionInstallFiles()
def CanRunForPage(self, page):
# No matter how many pages in the pageset, just perform two test iterations.
return page.page_set.pages.index(page) < 2
def MeasurePage(self, _, tab, results):
# Profile setup works in 2 phases:
# Phase 1: When the first page is loaded: we wait for a timeout to allow
# all extensions to install and to prime safe browsing and other
# caches. Extensions may open tabs as part of the install process.
# Phase 2: When the second page loads, page_runner closes all tabs -
# we are left with one open tab, wait for that to finish loading.
# Sleep for a bit to allow safe browsing and other data to load +
# extensions to install.
if not self._extensions_installed:
sleep_seconds = 5 * 60
logging.info("Sleeping for %d seconds." % sleep_seconds)
time.sleep(sleep_seconds)
self._extensions_installed = True
else:
# Phase 2: Wait for tab to finish loading.
for i in xrange(len(tab.browser.tabs)):
t = tab.browser.tabs[i]
t.WaitForDocumentReadyStateToBeComplete()