Home | History | Annotate | Download | only in profile_creators
      1 # Copyright 2014 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 
      5 import json
      6 import logging
      7 import os
      8 import platform
      9 import shutil
     10 import socket
     11 import sys
     12 import tempfile
     13 import time
     14 import urllib2
     15 import zipfile
     16 
     17 from telemetry.page import profile_creator
     18 
     19 import page_sets
     20 
     21 
     22 def _ExternalExtensionsPath():
     23   """Returns the OS-dependent path at which to install the extension deployment
     24    files"""
     25   if platform.system() == 'Darwin':
     26     return os.path.join('/Library', 'Application Support', 'Google', 'Chrome',
     27         'External Extensions')
     28   elif platform.system() == 'Linux':
     29     return os.path.join('/opt', 'google', 'chrome', 'extensions' )
     30   else:
     31     raise NotImplementedError('Extension install on %s is not yet supported' %
     32         platform.system())
     33 
     34 def _DownloadExtension(extension_id, output_dir):
     35   """Download an extension to disk.
     36 
     37   Args:
     38     extension_id: the extension id.
     39     output_dir: Directory to download into.
     40 
     41   Returns:
     42     Extension file downloaded."""
     43   extension_download_path = os.path.join(output_dir, "%s.crx" % extension_id)
     44   extension_url = (
     45       "https://clients2.google.com/service/update2/crx?response=redirect"
     46       "&x=id%%3D%s%%26lang%%3Den-US%%26uc" % extension_id)
     47   response = urllib2.urlopen(extension_url)
     48   assert(response.getcode() == 200)
     49 
     50   with open(extension_download_path, "w") as f:
     51     f.write(response.read())
     52 
     53   return extension_download_path
     54 
     55 def _GetExtensionInfoFromCRX(crx_path):
     56   """Parse an extension archive and return information.
     57 
     58   Note:
     59     The extension name returned by this function may not be valid
     60   (e.g. in the case of a localized extension name).  It's use is just
     61   meant to be informational.
     62 
     63   Args:
     64     crx_path: path to crx archive to look at.
     65 
     66   Returns:
     67     Tuple consisting of:
     68     (crx_version, extension_name)"""
     69   crx_zip = zipfile.ZipFile(crx_path)
     70   manifest_contents = crx_zip.read('manifest.json')
     71   decoded_manifest = json.loads(manifest_contents)
     72   crx_version = decoded_manifest['version']
     73   extension_name = decoded_manifest['name']
     74 
     75   return (crx_version, extension_name)
     76 
     77 class ExtensionsProfileCreator(profile_creator.ProfileCreator):
     78   """Virtual base class for profile creators that install extensions.
     79 
     80   Extensions are installed using the mechanism described in
     81   https://developer.chrome.com/extensions/external_extensions.html .
     82 
     83   Subclasses are meant to be run interactively.
     84   """
     85 
     86   def __init__(self):
     87     super(ExtensionsProfileCreator, self).__init__()
     88     self._page_set = page_sets.Typical25()
     89 
     90     # Directory into which the output profile is written.
     91     self._output_profile_path = None
     92 
     93     # List of extensions to install.
     94     self._extensions_to_install = []
     95 
     96     # Theme to install (if any).
     97     self._theme_to_install = None
     98 
     99     # Directory to download extension files into.
    100     self._extension_download_dir = None
    101 
    102     # Have the extensions been installed yet?
    103     self._extensions_installed = False
    104 
    105     # List of files to delete after run.
    106     self._files_to_cleanup = []
    107 
    108   def _PrepareExtensionInstallFiles(self):
    109     """Download extension archives and create extension install files."""
    110     extensions_to_install = self._extensions_to_install
    111     if self._theme_to_install:
    112       extensions_to_install = extensions_to_install + [self._theme_to_install]
    113     num_extensions = len(extensions_to_install)
    114     if not num_extensions:
    115       raise ValueError("No extensions or themes to install:",
    116           extensions_to_install)
    117 
    118     # Create external extensions path if it doesn't exist already.
    119     external_extensions_dir = _ExternalExtensionsPath()
    120     if not os.path.isdir(external_extensions_dir):
    121       os.makedirs(external_extensions_dir)
    122 
    123     self._extension_download_dir = tempfile.mkdtemp()
    124 
    125     for i in xrange(num_extensions):
    126       extension_id = extensions_to_install[i]
    127       logging.info("Downloading %s - %d/%d" % (
    128           extension_id, (i + 1), num_extensions))
    129       extension_path = _DownloadExtension(extension_id,
    130           self._extension_download_dir)
    131       (version, name) = _GetExtensionInfoFromCRX(extension_path)
    132       extension_info = {'external_crx' : extension_path,
    133           'external_version' : version,
    134           '_comment' : name}
    135       extension_json_path = os.path.join(external_extensions_dir,
    136           "%s.json" % extension_id)
    137       with open(extension_json_path, 'w') as f:
    138         f.write(json.dumps(extension_info))
    139         self._files_to_cleanup.append(extension_json_path)
    140 
    141   def _CleanupExtensionInstallFiles(self):
    142     """Cleanup stray files before exiting."""
    143     logging.info("Cleaning up stray files")
    144     for filename in self._files_to_cleanup:
    145       os.remove(filename)
    146 
    147     if self._extension_download_dir:
    148       # Simple sanity check to lessen the impact of a stray rmtree().
    149       if len(self._extension_download_dir.split(os.sep)) < 3:
    150         raise Exception("Path too shallow: %s" % self._extension_download_dir)
    151       shutil.rmtree(self._extension_download_dir)
    152       self._extension_download_dir = None
    153 
    154   def CustomizeBrowserOptions(self, options):
    155     self._output_profile_path = options.output_profile_path
    156 
    157   def WillRunTest(self, options):
    158     """Run before browser starts.
    159 
    160     Download extensions and write installation files."""
    161     super(ExtensionsProfileCreator, self).WillRunTest(options)
    162 
    163     # Running this script on a corporate network or other managed environment
    164     # could potentially alter the profile contents.
    165     hostname = socket.gethostname()
    166     if hostname.endswith('corp.google.com'):
    167       raise Exception("It appears you are connected to a corporate network "
    168           "(hostname=%s).  This script needs to be run off the corp "
    169           "network." % hostname)
    170 
    171     prompt = ("\n!!!This script must be run on a fresh OS installation, "
    172         "disconnected from any corporate network. Are you sure you want to "
    173         "continue? (y/N) ")
    174     if (raw_input(prompt).lower() != 'y'):
    175       sys.exit(-1)
    176     self._PrepareExtensionInstallFiles()
    177 
    178   def DidRunTest(self, browser, results):
    179     """Run before exit."""
    180     super(ExtensionsProfileCreator, self).DidRunTest()
    181     # Do some basic sanity checks to make sure the profile is complete.
    182     installed_extensions = browser.extensions.keys()
    183     if not len(installed_extensions) == len(self._extensions_to_install):
    184       # Diagnosing errors:
    185       # Too many extensions: Managed environment may be installing additional
    186       # extensions.
    187       raise Exception("Unexpected number of extensions installed in browser",
    188           installed_extensions)
    189 
    190     # Check that files on this list exist and have content.
    191     expected_files = [
    192         os.path.join('Default', 'Network Action Predictor')]
    193     for filename in expected_files:
    194       filename = os.path.join(self._output_profile_path, filename)
    195       if not os.path.getsize(filename) > 0:
    196         raise Exception("Profile not complete: %s is zero length." % filename)
    197 
    198     self._CleanupExtensionInstallFiles()
    199 
    200   def CanRunForPage(self, page):
    201     # No matter how many pages in the pageset, just perform two test iterations.
    202     return page.page_set.pages.index(page) < 2
    203 
    204   def MeasurePage(self, _, tab, results):
    205     # Profile setup works in 2 phases:
    206     # Phase 1: When the first page is loaded: we wait for a timeout to allow
    207     #     all extensions to install and to prime safe browsing and other
    208     #     caches.  Extensions may open tabs as part of the install process.
    209     # Phase 2: When the second page loads, page_runner closes all tabs -
    210     #     we are left with one open tab, wait for that to finish loading.
    211 
    212     # Sleep for a bit to allow safe browsing and other data to load +
    213     # extensions to install.
    214     if not self._extensions_installed:
    215       sleep_seconds = 5 * 60
    216       logging.info("Sleeping for %d seconds." % sleep_seconds)
    217       time.sleep(sleep_seconds)
    218       self._extensions_installed = True
    219     else:
    220       # Phase 2: Wait for tab to finish loading.
    221       for i in xrange(len(tab.browser.tabs)):
    222         t = tab.browser.tabs[i]
    223         t.WaitForDocumentReadyStateToBeComplete()
    224