Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: watchextensions.py

Issue 29762573: Issue 6602 - Introduce watchextensions
Left Patch Set: Created April 26, 2018, 11:03 a.m.
Right Patch Set: Created May 16, 2018, 9:47 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Right: Side by side diff | Download
« no previous file with change/comment | « tox.ini ('k') | watchextensions.ini.example » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
(no file at all)
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 """
5 Track differences between versions of arbitrary extensions.
6
7 Running this module direcly will attempt to fetch new versions for every
8 in ~/extwatcher.ini or /etc/extwatcher.ini configured and enabled extension
9 from the Chrome Web Store. When no parameters are given, you will simply be
10 notified about whether a new version was found.
11
12 Providing -k/--keep-repository will leave the git repository (with unstaged
13 differences between the latest tracked version and the newest) on your drive,
14 in order to examine the differences locally.
15
16 Providing -p/--push will stage all differences and push them to the configured
17 tracking repository.
18 """
19 from __future__ import print_function
20
21 import argparse
22 from configparser import ConfigParser, NoSectionError, NoOptionError
23 import json
24 import logging
25 import os
26 import shutil
27 import subprocess
28 import sys
29 import tempfile
30 from zipfile import ZipFile
31
32 try: # Python 3 only
33 from urllib import request as urllib
34 except ImportError: # Python 2 only
35 import urllib
36 from xml.etree import ElementTree
37
38 logging.basicConfig(level=logging.INFO)
39
40 OMAHA_URL = 'https://omahaproxy.appspot.com/all.json?os=win'
41 SERVICE_URL = 'https://clients2.google.com/service/update2/crx'
42
43
44 class SensitiveFilter(logging.Filter):
45 """Filter sensitive ouput from the logs.
46
47 This class is meant to be registered in python's logging facility. It will
48 mask potentially sensitive data from the message that is to be logged.
49
50
51 """
52
53 def __init__(self, patterns):
54 """Initialize a SensitiveFilter with the desired patterns."""
55 self.patterns = patterns
56 super(SensitiveFilter, self).__init__()
57
58 def filter(self, record):
59 msg = record.msg
60 if isinstance(msg, BaseException):
61 msg = str(msg)
62 record.msg = self.mask(msg)
63 return True
64
65 def mask(self, msg):
66 try:
67 for pattern in self.patterns:
68 msg = msg.replace(
69 pattern,
70 '/'.join(['******', pattern.rsplit('/', 1)[-1]]),
71 )
72 except AttributeError:
73 pass
74 return msg
75
76
77 class ConfigurationError(Exception):
78 pass
79
80
81 def read_config(path=None):
82 config_path = path or [
83 os.path.expanduser('~/watchextensions.ini'),
84 '/etc/watchextensions.ini',
85 ]
86
87 config = ConfigParser()
88 if not config.read(config_path):
89 raise ConfigurationError('No configuration file was found. Please '
90 'provide ~/watchextensions.ini or '
91 '/etc/watchextensions.ini or specify a valid '
92 'path.')
93 return config
94
95
96 class ExtWatcher(object):
97 section = 'extensions'
98
99 def __init__(self, ext_name, config, push, keep_repo):
100 self.logger = logging.getLogger(name=ext_name)
101 self.config = config
102
103 self.ext_name = ext_name
104 self.push = push
105 self.keep_repo = keep_repo
106
107 self._cws_ext_url = None
108 self._current_ext_version = None
109
110 self.downloaded_file = None
111
112 super(ExtWatcher, self).__init__()
113
114 def _git_cmd(self, cmds, relative=False):
115 base = ['git']
116 if not relative:
117 base += ['-C', self.tempdir]
118 suffix = []
119 if not any(x in cmds for x in ['status', 'add', 'diff']):
120 suffix += ['--quiet']
121 return subprocess.check_output(base + list(cmds) + suffix)
122
123 def _clone_repository(self):
124 self.logger.info('Cloning ' + self.repository)
125 self._git_cmd(['clone', '-b', 'master', '--single-branch',
126 self.repository, self.tempdir],
127 relative=True)
128
129 def _parse_config(self):
130 err_msg = '"{}" is not fully configured!'.format(self.ext_name)
131 try:
132 self.ext_id = self.config.get(self.section, self.ext_name + '_id')
133 self.repository = self.config.get(self.section,
134 self.ext_name + '_repository')
135 if not self.ext_id or not self.repository:
136 raise ConfigurationError(err_msg)
137 except NoOptionError:
138 raise ConfigurationError(err_msg)
139 except NoSectionError:
140 raise ConfigurationError('Section [{}] is missing!'
141 ''.format(self.section))
142
143 self.logger.addFilter(SensitiveFilter([self.repository]))
144
145 def __enter__(self):
146 self.tempdir = tempfile.mkdtemp(prefix='watched_extension_')
147 return self
148
149 def __exit__(self, exc_type, exc_value, exc_traceback):
150 if exc_value:
151 self.logger.error(exc_value)
152
153 if not self.keep_repo:
154 shutil.rmtree(self.tempdir, ignore_errors=True)
155 else:
156 print('Repository for {} available at {}'.format(self.ext_name,
157 self.tempdir))
158 if self.downloaded_file:
159 os.remove(self.downloaded_file)
160
161 return True
162
163 def _fetch_latest_chrome_version(self):
164 response = urllib.urlopen(OMAHA_URL).read()
165
166 data = json.loads(response.decode('utf-8'))
167
168 stable = [x for x in data[0]['versions'] if x['channel'] == 'stable']
169 return stable[0]['current_version']
170
171 @property
172 def cws_ext_url(self):
173 if not self._cws_ext_url:
174 ext_url = SERVICE_URL + '?prodversion={}&x=id%3D{}%26uc'.format(
175 self._fetch_latest_chrome_version(), self.ext_id,
176 )
177 self._cws_ext_url = ext_url
178
179 return self._cws_ext_url
180
181 @property
182 def current_ext_version(self):
183 if not self._current_ext_version:
184 updatecheck_url = self.cws_ext_url + '&response=updatecheck'
185 manifest = urllib.urlopen(updatecheck_url).read()
186 root = ElementTree.fromstring(manifest)
187
188 ns = {'g': 'http://www.google.com/update2/response'}
189 update = root.find('g:app/g:updatecheck', ns)
190
191 self._current_ext_version = update.attrib['version']
192 return self._current_ext_version
193
194 def _download_ext_crx(self):
195 download_url = self.cws_ext_url + '&response=redirect'
196 filename = '{}-{}.crx'.format(self.ext_id, self.current_ext_version)
197
198 urllib.urlretrieve(download_url, filename)
199 self.downloaded_file = filename
200 self.logger.info('Downloaded ' + filename)
201
202 def _unzip_to_repository(self):
203 with ZipFile(self.downloaded_file, 'r') as zip_fp:
204 zip_fp.extractall(self.tempdir)
205
206 def _get_tracked_version(self):
207 return [x.decode() for x in
208 self._git_cmd(['log', '--pretty=format:%s']).splitlines()]
209
210 def _track_new_contents(self, message):
211 status = self._git_cmd(['status'])
212 if b'nothing to commit' not in status:
213 self._git_cmd(['add', '--all'])
214 self._git_cmd(['commit', '-m', message])
215 self._git_cmd(['push', 'origin', 'master'])
216
217 def run(self):
218 self._parse_config()
219 self._clone_repository()
220
221 next_version = self.current_ext_version
222 if next_version not in self._get_tracked_version():
223 self.logger.info('New untracked version {} found!'
224 ''.format(next_version))
225 if self.push or self.keep_repo:
226 self._download_ext_crx()
227 self._unzip_to_repository()
228 if self.push:
229 self._track_new_contents(next_version)
230 else:
231 self.logger.info('No new version found.')
232
233
234 if __name__ == '__main__':
235 parser = argparse.ArgumentParser(
236 description=__doc__.strip('\n'),
237 formatter_class=argparse.RawDescriptionHelpFormatter,
238 )
239 parser.add_argument('-q', '--quiet', action='store_true', default=False,
240 help='Suppress informational output.')
241 parser.add_argument('-p', '--push', action='store_true', default=False,
242 help='Perfom a PUSH to the tracking repository.')
243 parser.add_argument('-k', '--keep-repository', action='store_true',
244 default=False, help='Keep the local repository')
245 parser.add_argument('-c', '--config-path', default=None,
246 help='Absolute path to a custom config file.')
247
248 args = parser.parse_args()
249 if args.quiet:
250 logging.disable(logging.INFO)
251
252 try:
253 config = read_config(args.config_path)
254
255 for ext_name in config.options('enabled'):
256 with ExtWatcher(ext_name, config, args.push,
257 args.keep_repository) as watcher:
258 watcher.run()
259 except ConfigurationError as e:
260 sys.exit(e.message)
LEFTRIGHT

Powered by Google App Engine
This is Rietveld