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 5import json 6import logging 7import os 8import platform 9import shutil 10import socket 11import sys 12import tempfile 13import time 14import urllib2 15import zipfile 16 17from telemetry.page import profile_creator 18 19import page_sets 20 21 22def _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 34def _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 55def _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 77class 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 ValidateAndMeasurePage(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