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

Side by Side Diff: watchextensions.py

Issue 29762573: Issue 6602 - Introduce watchextensions
Patch Set: Created May 8, 2018, 2:22 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
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 When the configuration files are incomplete or missing, a ConfigurationError
Vasily Kuznetsov 2018/05/15 16:49:31 Nit: this line is probably not very useful for the
tlucas 2018/05/16 09:52:38 Acknowledged.
20 will be raised.
21 """
22 from __future__ import print_function
23
24 import argparse
25 from configparser import ConfigParser, NoSectionError, NoOptionError
26 import json
27 import logging
28 import os
29 import shutil
30 import subprocess
31 import tempfile
32 from zipfile import ZipFile
33
34 try: # Python 3 only
35 from urllib import request as urllib
36 except ImportError: # Python 2 only
37 import urllib
38 from xml.etree import ElementTree
39
40 logging.basicConfig(level=logging.INFO)
41
42
43 class SensitiveFilter(logging.Filter):
Vasily Kuznetsov 2018/05/15 16:49:32 If I understand it correctly, the purpose of this
tlucas 2018/05/16 09:52:38 You are right about the purpose - I added a docume
Vasily Kuznetsov 2018/05/16 17:54:55 I was thinking about avoiding this filter class al
44 def __init__(self, patterns):
45 self.patterns = patterns
46 super(SensitiveFilter, self).__init__()
47
48 def filter(self, record):
49 msg = record.msg
50 if isinstance(msg, BaseException):
51 msg = str(msg)
52 record.msg = self.mask(msg)
53 return True
54
55 def mask(self, msg):
56 try:
57 for pattern in self.patterns:
58 msg = msg.replace(
59 pattern,
60 '/'.join(['******', pattern.rsplit('/', 1)[-1]]),
61 )
62 except AttributeError:
63 pass
64 return msg
65
66
67 def read_config(path=None):
68 config_path = path or [
69 os.path.expanduser('~/watchextensions.ini'),
70 '/etc/watchextensions.ini',
71 ]
72
73 config = ConfigParser()
74 if not config.read(config_path):
75 raise ConfigurationError('No configuration file was found. Please '
76 'provide ~/watchextensions.ini or '
77 '/etc/watchextensions.ini or specify a valid '
78 'path.')
79 return config
80
81
82 class ConfigurationError(Exception):
Vasily Kuznetsov 2018/05/15 16:49:31 Nit: moving this to above `read_config` would make
tlucas 2018/05/16 09:52:37 Done.
83 pass
84
85
86 class ExtWatcher(object):
87 section = 'extensions'
88
89 def __init__(self, ext_name, config, push, keep_repo):
90 self.logger = logging.getLogger(name=ext_name)
91 self.config = config
92
93 self.ext_name = ext_name
94 self.push = push
95 self.keep_repo = keep_repo
96
97 self._cws_ext_url = None
98 self._current_ext_version = None
99
100 self.downloaded_file = None
101
102 super(ExtWatcher, self).__init__()
103
104 def _git_cmd(self, cmds, relative=False):
105 base = ['git']
106 if not relative:
107 base += ['-C', self.tempdir]
108 suffix = []
109 if not any(x in cmds for x in ['status', 'add', 'diff']):
110 suffix += ['--quiet']
111 return subprocess.check_output(base + list(cmds) + suffix)
112
113 def _assure_local_repository(self):
Vasily Kuznetsov 2018/05/15 16:49:31 This name seems a bit strange. Shouldn't it be som
tlucas 2018/05/16 09:52:38 You are right. "_assure_local_repository" is an ar
114 self.logger.info('Cloning ' + self.repository)
115 self._git_cmd(['clone', '-b', 'master', '--single-branch',
116 self.repository, self.tempdir],
117 relative=True)
118
119 def _parse_config(self):
120 err_msg = '"{}" is not fully configured!'.format(self.ext_name)
121 try:
122 self.ext_id = self.config.get(self.section, self.ext_name + '_id')
123 self.repository = self.config.get(self.section,
124 self.ext_name + '_repository')
125 if not self.ext_id or not self.repository:
126 raise ConfigurationError(err_msg)
127 except NoOptionError:
128 raise ConfigurationError(err_msg)
129 except NoSectionError:
130 raise ConfigurationError('Section [{}] is missing!'
131 ''.format(self.section))
132
133 self.logger.addFilter(SensitiveFilter([self.repository]))
134
135 def __enter__(self):
136 self.tempdir = tempfile.mkdtemp(prefix='watched_extension_')
137 return self
138
139 def __exit__(self, exc_type, exc_value, exc_traceback):
140 if exc_value:
141 self.logger.error(exc_value)
142
143 if not self.keep_repo:
144 shutil.rmtree(self.tempdir, ignore_errors=True)
145 else:
146 print('Repository for {} available at {}'.format(self.ext_name,
147 self.tempdir))
148 if self.downloaded_file:
149 os.remove(self.downloaded_file)
150
151 return True
152
153 def _fetch_latest_chrome_version(self):
154 omaha_url = 'https://omahaproxy.appspot.com/all.json?os=win'
Vasily Kuznetsov 2018/05/15 16:49:31 Maybe this should be a constant somewhere at the t
tlucas 2018/05/16 09:52:38 Done.
155 response = urllib.urlopen(omaha_url).read()
156
157 data = json.loads(response.decode('utf-8'))
158
159 stable = [x for x in data[0]['versions'] if x['channel'] == 'stable']
160 return stable[0]['current_version']
161
162 @property
163 def cws_ext_url(self):
164 if not self._cws_ext_url:
Vasily Kuznetsov 2018/05/15 16:49:31 You could just slap a `functools.lru_cache` decora
tlucas 2018/05/16 09:52:38 I'll use this comment to also reply to what you as
165 service_url = 'https://clients2.google.com/service/update2/crx'
166 ext_url = service_url + '?prodversion={}&x=id%3D{}%26uc'.format(
167 self._fetch_latest_chrome_version(), self.ext_id,
168 )
169 self._cws_ext_url = ext_url
170
171 return self._cws_ext_url
172
173 @property
174 def current_ext_version(self):
175 if not self._current_ext_version:
176 updatecheck_url = self.cws_ext_url + '&response=updatecheck'
177 manifest = urllib.urlopen(updatecheck_url).read()
178 root = ElementTree.fromstring(manifest)
179
180 ns = {'g': 'http://www.google.com/update2/response'}
181 update = root.find('g:app/g:updatecheck', ns)
182
183 self._current_ext_version = update.attrib['version']
184 return self._current_ext_version
185
186 def _download_ext_crx(self):
187 download_url = self.cws_ext_url + '&response=redirect'
188 filename = '{}-{}.crx'.format(self.ext_id, self.current_ext_version)
189
190 urllib.urlretrieve(download_url, filename)
191 self.downloaded_file = filename
192 self.logger.info('Downloaded ' + filename)
193
194 def _unzip_to_repository(self):
195 with ZipFile(self.downloaded_file, 'r') as zip_fp:
196 zip_fp.extractall(self.tempdir)
197
198 def _get_tracked_version(self):
199 return [x.decode() for x in
200 self._git_cmd(['log', '--pretty=format:%s']).splitlines()]
201
202 def _track_new_contents(self, message):
203 status = self._git_cmd(['status'])
204 if b'nothing to commit' not in status:
205 self._git_cmd(['add', '--all'])
206 self._git_cmd(['commit', '-m', message])
207 self._git_cmd(['push', 'origin', 'master'])
208
209 def run(self):
210 self._parse_config()
211 self._assure_local_repository()
212
213 next_version = self.current_ext_version
214 if next_version not in self._get_tracked_version():
215 self.logger.info('New untracked version {} found!'
216 ''.format(next_version))
217 if self.push or self.keep_repo:
218 self._download_ext_crx()
219 self._unzip_to_repository()
220 if self.push:
221 self._track_new_contents(next_version)
222 else:
223 self.logger.info('No new version found.')
224
225
226 if __name__ == '__main__':
227 parser = argparse.ArgumentParser(
228 description=__doc__.strip('\n'),
229 formatter_class=argparse.RawDescriptionHelpFormatter,
230 )
231 parser.add_argument('-q', '--quiet', action='store_true', default=False,
232 help='Suppress informational output.')
233 parser.add_argument('-p', '--push', action='store_true', default=False,
234 help='Perfom a PUSH to the tracking repository.')
235 parser.add_argument('-k', '--keep-repository', action='store_true',
236 default=False, help='Keep the local repository')
237 parser.add_argument('-c', '--config-path', default=None,
238 help='Absolute path to a custom config file.')
239
240 args = parser.parse_args()
241 if args.quiet:
242 logging.disable(logging.INFO)
243
244 try:
245 config = read_config(args.config_path)
246
247 for ext_name in config.options('enabled'):
248 with ExtWatcher(ext_name, config, args.push,
249 args.keep_repository) as watcher:
250 watcher.run()
251 except ConfigurationError as e:
252 logging.error(e.message)
Vasily Kuznetsov 2018/05/15 16:49:31 Perhaps `sys.exit(e.message)` would be more approp
tlucas 2018/05/16 09:52:37 Done.
OLDNEW

Powered by Google App Engine
This is Rietveld