1 # Copyright 2015 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.
14 if sys
.platform
== 'win32':
15 import _winreg
as winreg
# pylint: disable=import-error
17 from catapult_base
import cloud_storage
18 from profile_creators
import profile_extender
19 from telemetry
.core
import exceptions
22 # Remote target upload directory in cloud storage for extensions.
23 REMOTE_DIR
= 'extension_set'
26 ZIP_NAME
= 'extensions.zip'
29 class InvalidExtensionArchiveError(exceptions
.Error
):
30 """Exception thrown when remote archive is invalid or malformed.
32 Remote archive should be located at REMOTE_DIR/ZIP_NAME. Upon failure,
33 prompts user to update remote archive using update_remote_extensions
37 def __init__(self
, msg
=''):
38 msg
+= ('\nTry running\n'
39 '\tpython update_remote_extensions.py -e extension_set.csv\n'
40 'in src/tools/perf/profile_creator subdirectory.')
41 super(InvalidExtensionArchiveError
, self
).__init
__(msg
)
44 class ExtensionProfileExtender(profile_extender
.ProfileExtender
):
45 """Creates a profile with many extensions."""
47 def __init__(self
, finder_options
):
48 super(ExtensionProfileExtender
, self
).__init
__(finder_options
)
50 finder_options
.browser_options
.disable_default_apps
= False
51 finder_options
.browser_options
.AppendExtraBrowserArgs(
52 '--prompt-for-external-extensions=0')
55 """Superclass override."""
56 # Download extensions from cloud and force-install extensions into profile.
57 local_extensions_dir
= os
.path
.join(self
.profile_path
,
58 'external_extensions_crx')
59 self
._DownloadRemoteExtensions
(cloud_storage
.PARTNER_BUCKET
,
61 atexit
.register(self
._CleanUpExtensions
)
62 self
._LoadExtensions
(local_extensions_dir
, self
.profile_path
)
65 self
._WaitForExtensionsToLoad
()
67 self
.TearDownBrowser()
69 def _DownloadRemoteExtensions(self
, remote_bucket
, local_extensions_dir
):
70 """Downloads and unzips archive of common extensions to disk.
73 remote_bucket: bucket to download remote archive from.
74 local_extensions_dir: destination extensions directory.
77 InvalidExtensionArchiveError if remote archive is not found.
79 # Force Unix directory separator for remote path.
80 remote_zip_path
= '%s/%s' % (REMOTE_DIR
, ZIP_NAME
)
81 local_zip_path
= os
.path
.join(local_extensions_dir
, ZIP_NAME
)
83 cloud_storage
.Get(remote_bucket
, remote_zip_path
, local_zip_path
)
85 raise InvalidExtensionArchiveError('Can\'t find archive at gs://%s/%s..'
86 % (remote_bucket
, remote_zip_path
))
88 with zipfile
.ZipFile(local_zip_path
, 'r') as extensions_zip
:
89 extensions_zip
.extractall(local_extensions_dir
)
91 os
.remove(local_zip_path
)
93 def _GetExtensionInfoFromCrx(self
, crx_file
):
94 """Retrieves version + name of extension from CRX archive."""
95 with zipfile
.ZipFile(crx_file
, 'r') as crx_zip
:
96 manifest_contents
= crx_zip
.read('manifest.json')
97 decoded_manifest
= json
.loads(manifest_contents
)
98 crx_version
= decoded_manifest
['version']
99 extension_name
= decoded_manifest
['name']
100 return (crx_version
, extension_name
)
102 def _LoadExtensions(self
, local_extensions_dir
, profile_dir
):
103 """Loads extensions in _local_extensions_dir into user profile.
105 Extensions are loaded according to platform specifications at
106 https://developer.chrome.com/extensions/external_extensions.html
109 local_extensions_dir: directory containing CRX files.
110 profile_dir: target profile directory for the extensions.
113 InvalidExtensionArchiveError if archive contains a non-CRX file.
115 ext_files
= os
.listdir(local_extensions_dir
)
116 external_ext_dir
= os
.path
.join(profile_dir
, 'External Extensions')
117 os
.makedirs(external_ext_dir
)
118 for ext_file
in ext_files
:
119 ext_path
= os
.path
.join(local_extensions_dir
, ext_file
)
120 if not ext_file
.endswith('.crx'):
121 raise InvalidExtensionArchiveError('Archive contains non-crx file %s.'
123 (version
, name
) = self
._GetExtensionInfoFromCrx
(ext_path
)
124 ext_id
= os
.path
.splitext(os
.path
.basename(ext_path
))[0]
126 'extension_id': ext_id
,
127 'external_crx': ext_path
,
128 'external_version': version
,
131 # Platform-specific external extension installation
132 if self
.os_name
== 'win': # Windows
133 key_path
= 'Software\\Google\\Chrome\\Extensions\\%s' % ext_id
134 self
._WriteRegistryValue
(key_path
, 'Path', ext_path
)
135 self
._WriteRegistryValue
(key_path
, 'Version', version
)
137 extension_json_path
= os
.path
.join(external_ext_dir
, '%s.json' % ext_id
)
138 with
open(extension_json_path
, 'w') as f
:
139 f
.write(json
.dumps(extension_info
))
140 self
._extensions
.append(ext_id
)
142 def _WriteRegistryValue(self
, key_path
, name
, value
):
143 """Writes (or overwrites) registry value specified to HKCU\\key_path."""
144 with winreg
.CreateKey(winreg
.HKEY_CURRENT_USER
, key_path
) as key
:
145 try: # Does registry value already exist?
146 path_value
= winreg
.QueryValueEx(key
, name
)
147 if path_value
!= value
:
149 'Overwriting registry value %s\\%s:'
150 '\n%s with %s' % (key_path
, name
, path_value
, value
))
153 winreg
.SetValueEx(key
, name
, 0, winreg
.REG_SZ
, value
)
155 def _CleanUpExtensions(self
):
156 """Cleans up registry keys or JSON files used to install extensions."""
157 if self
.os_name
== 'win':
158 for ext_id
in self
._extensions
:
159 winreg
.DeleteKey(winreg
.HKEY_CURRENT_USER
,
160 'Software\\Google\\Chrome\\Extensions\\%s' % ext_id
)
162 to_remove
= os
.path
.join(self
.profile_path
, 'External Extensions')
163 if os
.path
.exists(to_remove
):
164 shutil
.rmtree(to_remove
)
166 def _WaitForExtensionsToLoad(self
):
167 """Stall until browser has finished installing/loading all extensions."""
168 unloaded_extensions
= set(self
._extensions
)
169 while unloaded_extensions
:
170 loaded_extensions
= set([key
.extension_id
for key
in
171 self
.browser
.extensions
.keys()])
172 unloaded_extensions
= unloaded_extensions
- loaded_extensions
173 # There's no event signalling when browser finishes installing
174 # or loading an extension so re-check every 5 seconds.