| OLD | NEW |
| (Empty) |
| 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. | |
| 4 import json | |
| 5 import logging | |
| 6 import os | |
| 7 import platform | |
| 8 import shutil | |
| 9 import socket | |
| 10 import sys | |
| 11 import tempfile | |
| 12 import time | |
| 13 import urllib2 | |
| 14 import zipfile | |
| 15 | |
| 16 from telemetry.core import util | |
| 17 from telemetry.page import page_set | |
| 18 from telemetry.page import profile_creator | |
| 19 | |
| 20 | |
| 21 def _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 | |
| 33 def _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 | |
| 54 def _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 | |
| 76 class 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.py') | |
| 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, 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() | |
| OLD | NEW |