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.
17 from telemetry
.page
import profile_creator
22 def _ExternalExtensionsPath():
23 """Returns the OS-dependent path at which to install the extension deployment
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' )
31 raise NotImplementedError('Extension install on %s is not yet supported' %
34 def _DownloadExtension(extension_id
, output_dir
):
35 """Download an extension to disk.
38 extension_id: the extension id.
39 output_dir: Directory to download into.
42 Extension file downloaded."""
43 extension_download_path
= os
.path
.join(output_dir
, "%s.crx" % extension_id
)
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)
50 with
open(extension_download_path
, "w") as f
:
51 f
.write(response
.read())
53 return extension_download_path
55 def _GetExtensionInfoFromCRX(crx_path
):
56 """Parse an extension archive and return information.
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.
64 crx_path: path to crx archive to look at.
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']
75 return (crx_version
, extension_name
)
77 class ExtensionsProfileCreator(profile_creator
.ProfileCreator
):
78 """Virtual base class for profile creators that install extensions.
80 Extensions are installed using the mechanism described in
81 https://developer.chrome.com/extensions/external_extensions.html .
83 Subclasses are meant to be run interactively.
87 super(ExtensionsProfileCreator
, self
).__init
__()
88 self
._page
_set
= page_sets
.Typical25()
90 # Directory into which the output profile is written.
91 self
._output
_profile
_path
= None
93 # List of extensions to install.
94 self
._extensions
_to
_install
= []
96 # Theme to install (if any).
97 self
._theme
_to
_install
= None
99 # Directory to download extension files into.
100 self
._extension
_download
_dir
= None
102 # Have the extensions been installed yet?
103 self
._extensions
_installed
= False
105 # List of files to delete after run.
106 self
._files
_to
_cleanup
= []
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
)
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
)
123 self
._extension
_download
_dir
= tempfile
.mkdtemp()
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
,
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
)
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
:
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
154 def CustomizeBrowserOptions(self
, options
):
155 self
._output
_profile
_path
= options
.output_profile_path
157 def WillRunTest(self
, options
):
158 """Run before browser starts.
160 Download extensions and write installation files."""
161 super(ExtensionsProfileCreator
, self
).WillRunTest(options
)
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
)
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 "
174 if (raw_input(prompt
).lower() != 'y'):
176 self
._PrepareExtensionInstallFiles
()
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
):
185 # Too many extensions: Managed environment may be installing additional
187 raise Exception("Unexpected number of extensions installed in browser",
188 installed_extensions
)
190 # Check that files on this list exist and have content.
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
)
198 self
._CleanupExtensionInstallFiles
()
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
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, user_story_runner closes all tabs -
210 # we are left with one open tab, wait for that to finish loading.
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
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()