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. 4import json 5import logging 6import os 7import platform 8import shutil 9import socket 10import sys 11import tempfile 12import time 13import urllib2 14import zipfile 15 16from telemetry.core import util 17from telemetry.page import page_set 18from telemetry.page import profile_creator 19 20 21def _ExternalExtensionsPath(): 22 """Returns the OS-dependent path at which to install the extension deployment 23 files""" 24 if platform.system() == 'Darwin': 25 return os.path.join('/Library', 'Application Support', 'Google', 'Chrome', 26 'External Extensions') 27 elif platform.system() == 'Linux': 28 return os.path.join('/opt', 'google', 'chrome', 'extensions' ) 29 else: 30 raise NotImplementedError('Extension install on %s is not yet supported' % 31 platform.system()) 32 33def _DownloadExtension(extension_id, output_dir): 34 """Download an extension to disk. 35 36 Args: 37 extension_id: the extension id. 38 output_dir: Directory to download into. 39 40 Returns: 41 Extension file downloaded.""" 42 extension_download_path = os.path.join(output_dir, "%s.crx" % extension_id) 43 extension_url = ( 44 "https://clients2.google.com/service/update2/crx?response=redirect" 45 "&x=id%%3D%s%%26lang%%3Den-US%%26uc" % extension_id) 46 response = urllib2.urlopen(extension_url) 47 assert(response.getcode() == 200) 48 49 with open(extension_download_path, "w") as f: 50 f.write(response.read()) 51 52 return extension_download_path 53 54def _GetExtensionInfoFromCRX(crx_path): 55 """Parse an extension archive and return information. 56 57 Note: 58 The extension name returned by this function may not be valid 59 (e.g. in the case of a localized extension name). It's use is just 60 meant to be informational. 61 62 Args: 63 crx_path: path to crx archive to look at. 64 65 Returns: 66 Tuple consisting of: 67 (crx_version, extension_name)""" 68 crx_zip = zipfile.ZipFile(crx_path) 69 manifest_contents = crx_zip.read('manifest.json') 70 decoded_manifest = json.loads(manifest_contents) 71 crx_version = decoded_manifest['version'] 72 extension_name = decoded_manifest['name'] 73 74 return (crx_version, extension_name) 75 76class ExtensionsProfileCreator(profile_creator.ProfileCreator): 77 """Virtual base class for profile creators that install extensions. 78 79 Extensions are installed using the mechanism described in 80 https://developer.chrome.com/extensions/external_extensions.html . 81 82 Subclasses are meant to be run interactively. 83 """ 84 85 def __init__(self): 86 super(ExtensionsProfileCreator, self).__init__() 87 typical_25 = os.path.join(util.GetBaseDir(), 'page_sets', 'typical_25.json') 88 self._page_set = page_set.PageSet.FromFile(typical_25) 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): 158 """Run before browser starts. 159 160 Download extensions and write installation files.""" 161 162 # Running this script on a corporate network or other managed environment 163 # could potentially alter the profile contents. 164 hostname = socket.gethostname() 165 if hostname.endswith('corp.google.com'): 166 raise Exception("It appears you are connected to a corporate network " 167 "(hostname=%s). This script needs to be run off the corp " 168 "network." % hostname) 169 170 prompt = ("\n!!!This script must be run on a fresh OS installation, " 171 "disconnected from any corporate network. Are you sure you want to " 172 "continue? (y/N) ") 173 if (raw_input(prompt).lower() != 'y'): 174 sys.exit(-1) 175 self._PrepareExtensionInstallFiles() 176 177 def DidRunTest(self, browser, results): 178 """Run before exit.""" 179 # Do some basic sanity checks to make sure the profile is complete. 180 installed_extensions = browser.extensions.keys() 181 if not len(installed_extensions) == len(self._extensions_to_install): 182 # Diagnosing errors: 183 # Too many extensions: Managed environment may be installing additional 184 # extensions. 185 raise Exception("Unexpected number of extensions installed in browser", 186 installed_extensions) 187 188 # Check that files on this list exist and have content. 189 expected_files = [ 190 os.path.join('Default', 'Network Action Predictor')] 191 for filename in expected_files: 192 filename = os.path.join(self._output_profile_path, filename) 193 if not os.path.getsize(filename) > 0: 194 raise Exception("Profile not complete: %s is zero length." % filename) 195 196 self._CleanupExtensionInstallFiles() 197 198 def CanRunForPage(self, page): 199 # No matter how many pages in the pageset, just perform two test iterations. 200 return page.page_set.pages.index(page) < 2 201 202 def MeasurePage(self, _, tab, results): 203 # Profile setup works in 2 phases: 204 # Phase 1: When the first page is loaded: we wait for a timeout to allow 205 # all extensions to install and to prime safe browsing and other 206 # caches. Extensions may open tabs as part of the install process. 207 # Phase 2: When the second page loads, page_runner closes all tabs - 208 # we are left with one open tab, wait for that to finish loading. 209 210 # Sleep for a bit to allow safe browsing and other data to load + 211 # extensions to install. 212 if not self._extensions_installed: 213 sleep_seconds = 5 * 60 214 logging.info("Sleeping for %d seconds." % sleep_seconds) 215 time.sleep(sleep_seconds) 216 self._extensions_installed = True 217 else: 218 # Phase 2: Wait for tab to finish loading. 219 for i in xrange(len(tab.browser.tabs)): 220 t = tab.browser.tabs[i] 221 t.WaitForDocumentReadyStateToBeComplete() 222