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: Actual Implementation for 6602 Created May 7, 2018, 2:57 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
« no previous file with comments | « tox.ini ('k') | watchextensions.ini.example » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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 def read_config(path=None):
44 config_path = path or [
45 os.path.expanduser('~/watchextensions.ini'),
46 '/etc/watchextensions.ini',
47 ]
48
49 config = ConfigParser()
50 if not config.read(config_path):
51 raise ConfigurationError('No configuration file was found. Please '
52 'provide ~/watchextensions.ini or '
53 '/etc/watchextensions.ini or specify a valid '
54 'path.')
55 return config
56
57
58 class ConfigurationError(Exception):
59 pass
60
61
62 class ExtWatcher(object):
63 section = 'extensions'
64
65 def __init__(self, ext_name, config, push, keep_repo):
66 self.logger = logging.getLogger(name=ext_name)
67 self.config = config
68
69 self.ext_name = ext_name
70 self.push = push
71 self.keep_repo = keep_repo
72
73 self._cws_ext_url = None
74 self._current_ext_version = None
75
76 self.downloaded_file = None
77
78 super(ExtWatcher, self).__init__()
79
80 def _git_cmd(self, cmds, relative=False):
81 base = ['git']
82 if not relative:
83 base += ['-C', self.tempdir]
84 suffix = []
85 if not any(x in cmds for x in ['status', 'add', 'diff']):
86 suffix += ['--quiet']
87 return subprocess.check_output(base + list(cmds) + suffix)
88
89 def _assure_local_repository(self):
90 self.logger.info('Cloning ' + self.repository)
91 self._git_cmd(['clone', '-b', 'master', '--single-branch',
92 self.repository, self.tempdir],
93 relative=True)
94
95 def _parse_config(self):
96 err_msg = '"{}" is not fully configured!'.format(self.ext_name)
97 try:
98 self.ext_id = self.config.get(self.section, self.ext_name + '_id')
99 self.repository = self.config.get(self.section,
100 self.ext_name + '_repository')
101 if not self.ext_id or not self.repository:
102 raise ConfigurationError(err_msg)
103 except NoOptionError:
104 raise ConfigurationError(err_msg)
105 except NoSectionError:
106 raise ConfigurationError('Section [{}] is missing!'
107 ''.format(self.section))
108
109 def __enter__(self):
110 self.tempdir = tempfile.mkdtemp(prefix='watched_extension_')
111 return self
112
113 def __exit__(self, exc_type, exc_value, exc_traceback):
114 if exc_value:
115 self.logger.error(exc_type)
116 self.logger.error(exc_value)
117
118 if not self.keep_repo:
119 shutil.rmtree(self.tempdir, ignore_errors=True)
120 else:
121 print('Repository for {} available at {}'.format(self.ext_name,
122 self.tempdir))
123 if self.downloaded_file:
124 os.remove(self.downloaded_file)
125
126 return True
127
128 def _fetch_latest_chrome_version(self):
129 omaha_url = 'https://omahaproxy.appspot.com/all.json?os=win'
130 response = urllib.urlopen(omaha_url).read()
131
132 data = json.loads(response.decode('utf-8'))
133
134 stable = [x for x in data[0]['versions'] if x['channel'] == 'stable']
135 return stable[0]['current_version']
136
137 @property
138 def cws_ext_url(self):
139 if not self._cws_ext_url:
140 service_url = 'https://clients2.google.com/service/update2/crx'
141 ext_url = service_url + '?prodversion={}&x=id%3D{}%26uc'.format(
142 self._fetch_latest_chrome_version(), self.ext_id,
143 )
144 self._cws_ext_url = ext_url
145
146 return self._cws_ext_url
147
148 @property
149 def current_ext_version(self):
150 if not self._current_ext_version:
151 updatecheck_url = self.cws_ext_url + '&response=updatecheck'
152 manifest = urllib.urlopen(updatecheck_url).read()
153 root = ElementTree.fromstring(manifest)
154
155 ns = {'g': 'http://www.google.com/update2/response'}
156 update = root.find('g:app/g:updatecheck', ns)
157
158 self._current_ext_version = update.attrib['version']
159 return self._current_ext_version
160
161 def _download_ext_crx(self):
162 download_url = self.cws_ext_url + '&response=redirect'
163 filename = '{}-{}.crx'.format(self.ext_id, self.current_ext_version)
164
165 urllib.urlretrieve(download_url, filename)
166 self.downloaded_file = filename
167 self.logger.info('Downloaded ' + filename)
168
169 def _unzip_to_repository(self):
170 with ZipFile(self.downloaded_file, 'r') as zip_fp:
171 zip_fp.extractall(self.tempdir)
172
173 def _get_tracked_version(self):
174 return [x.decode() for x in
175 self._git_cmd(['log', '--pretty=format:%s']).splitlines()]
176
177 def _track_new_contents(self, message):
178 status = self._git_cmd(['status'])
179 if b'nothing to commit' not in status:
180 self._git_cmd(['add', '--all'])
181 self._git_cmd(['commit', '-m', message])
182 self._git_cmd(['push', 'origin', 'master'])
183
184 def run(self):
185 self._parse_config()
186 self._assure_local_repository()
187
188 next_version = self.current_ext_version
189 if next_version not in self._get_tracked_version():
190 self.logger.info('New untracked version {} found!'
191 ''.format(next_version))
192 if self.push or self.keep_repo:
193 self._download_ext_crx()
194 self._unzip_to_repository()
195 if self.push:
196 self._track_new_contents(next_version)
197 else:
198 self.logger.info('No new version found.')
199
200
201 if __name__ == '__main__':
202 parser = argparse.ArgumentParser(
203 description=__doc__.strip('\n'),
204 formatter_class=argparse.RawDescriptionHelpFormatter,
205 )
206 parser.add_argument('-q', '--quiet', action='store_true', default=False,
207 help='Suppress informational output.')
208 parser.add_argument('-p', '--push', action='store_true', default=False,
209 help='Perfom a PUSH to the tracking repository.')
210 parser.add_argument('-k', '--keep-repository', action='store_true',
211 default=False, help='Keep the local repository')
212 parser.add_argument('-c', '--config-path', default=None,
213 help='Absolute path to a custom config file.')
214
215 args = parser.parse_args()
216 if args.quiet:
217 logging.disable(logging.INFO)
218
219 try:
220 config = read_config(args.config_path)
221
222 for ext_name in config.options('enabled'):
223 with ExtWatcher(ext_name, config, args.push,
224 args.keep_repository) as watcher:
225 watcher.run()
226 except ConfigurationError as e:
227 logging.error(e.message)
OLDNEW
« no previous file with comments | « tox.ini ('k') | watchextensions.ini.example » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld