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: Actual Implementation for 6602 Created May 7, 2018, 2:57 p.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:
Left: Side by side diff | Download
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
1 # This Source Code Form is subject to the terms of the Mozilla Public 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 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/. 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4 """ 4 """
5 Track differences between versions of arbitrary extensions. 5 Track differences between versions of arbitrary extensions.
6 6
7 Running this module direcly will attempt to fetch new versions for every 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 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 9 from the Chrome Web Store. When no parameters are given, you will simply be
10 notified about whether a new version was found. 10 notified about whether a new version was found.
11 11
12 Providing -k/--keep-repository will leave the git repository (with unstaged 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, 13 differences between the latest tracked version and the newest) on your drive,
14 in order to examine the differences locally. 14 in order to examine the differences locally.
15 15
16 Providing -p/--push will stage all differences and push them to the configured 16 Providing -p/--push will stage all differences and push them to the configured
17 tracking repository. 17 tracking repository.
18
19 When the configuration files are incomplete or missing, a ConfigurationError
20 will be raised.
21 """ 18 """
22 from __future__ import print_function 19 from __future__ import print_function
23 20
24 import argparse 21 import argparse
25 from configparser import ConfigParser, NoSectionError, NoOptionError 22 from configparser import ConfigParser, NoSectionError, NoOptionError
26 import json 23 import json
27 import logging 24 import logging
28 import os 25 import os
29 import shutil 26 import shutil
30 import subprocess 27 import subprocess
28 import sys
31 import tempfile 29 import tempfile
32 from zipfile import ZipFile 30 from zipfile import ZipFile
33 31
34 try: # Python 3 only 32 try: # Python 3 only
35 from urllib import request as urllib 33 from urllib import request as urllib
36 except ImportError: # Python 2 only 34 except ImportError: # Python 2 only
37 import urllib 35 import urllib
38 from xml.etree import ElementTree 36 from xml.etree import ElementTree
39 37
40 logging.basicConfig(level=logging.INFO) 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
41 79
42 80
43 def read_config(path=None): 81 def read_config(path=None):
44 config_path = path or [ 82 config_path = path or [
45 os.path.expanduser('~/watchextensions.ini'), 83 os.path.expanduser('~/watchextensions.ini'),
46 '/etc/watchextensions.ini', 84 '/etc/watchextensions.ini',
47 ] 85 ]
48 86
49 config = ConfigParser() 87 config = ConfigParser()
50 if not config.read(config_path): 88 if not config.read(config_path):
51 raise ConfigurationError('No configuration file was found. Please ' 89 raise ConfigurationError('No configuration file was found. Please '
52 'provide ~/watchextensions.ini or ' 90 'provide ~/watchextensions.ini or '
53 '/etc/watchextensions.ini or specify a valid ' 91 '/etc/watchextensions.ini or specify a valid '
54 'path.') 92 'path.')
55 return config 93 return config
56
57
58 class ConfigurationError(Exception):
59 pass
60 94
61 95
62 class ExtWatcher(object): 96 class ExtWatcher(object):
63 section = 'extensions' 97 section = 'extensions'
64 98
65 def __init__(self, ext_name, config, push, keep_repo): 99 def __init__(self, ext_name, config, push, keep_repo):
66 self.logger = logging.getLogger(name=ext_name) 100 self.logger = logging.getLogger(name=ext_name)
67 self.config = config 101 self.config = config
68 102
69 self.ext_name = ext_name 103 self.ext_name = ext_name
70 self.push = push 104 self.push = push
71 self.keep_repo = keep_repo 105 self.keep_repo = keep_repo
72 106
73 self._cws_ext_url = None 107 self._cws_ext_url = None
74 self._current_ext_version = None 108 self._current_ext_version = None
75 109
76 self.downloaded_file = None 110 self.downloaded_file = None
77 111
78 super(ExtWatcher, self).__init__() 112 super(ExtWatcher, self).__init__()
79 113
80 def _git_cmd(self, cmds, relative=False): 114 def _git_cmd(self, cmds, relative=False):
81 base = ['git'] 115 base = ['git']
82 if not relative: 116 if not relative:
83 base += ['-C', self.tempdir] 117 base += ['-C', self.tempdir]
84 suffix = [] 118 suffix = []
85 if not any(x in cmds for x in ['status', 'add', 'diff']): 119 if not any(x in cmds for x in ['status', 'add', 'diff']):
86 suffix += ['--quiet'] 120 suffix += ['--quiet']
87 return subprocess.check_output(base + list(cmds) + suffix) 121 return subprocess.check_output(base + list(cmds) + suffix)
88 122
89 def _assure_local_repository(self): 123 def _clone_repository(self):
90 self.logger.info('Cloning ' + self.repository) 124 self.logger.info('Cloning ' + self.repository)
91 self._git_cmd(['clone', '-b', 'master', '--single-branch', 125 self._git_cmd(['clone', '-b', 'master', '--single-branch',
92 self.repository, self.tempdir], 126 self.repository, self.tempdir],
93 relative=True) 127 relative=True)
94 128
95 def _parse_config(self): 129 def _parse_config(self):
96 err_msg = '"{}" is not fully configured!'.format(self.ext_name) 130 err_msg = '"{}" is not fully configured!'.format(self.ext_name)
97 try: 131 try:
98 self.ext_id = self.config.get(self.section, self.ext_name + '_id') 132 self.ext_id = self.config.get(self.section, self.ext_name + '_id')
99 self.repository = self.config.get(self.section, 133 self.repository = self.config.get(self.section,
100 self.ext_name + '_repository') 134 self.ext_name + '_repository')
101 if not self.ext_id or not self.repository: 135 if not self.ext_id or not self.repository:
102 raise ConfigurationError(err_msg) 136 raise ConfigurationError(err_msg)
103 except NoOptionError: 137 except NoOptionError:
104 raise ConfigurationError(err_msg) 138 raise ConfigurationError(err_msg)
105 except NoSectionError: 139 except NoSectionError:
106 raise ConfigurationError('Section [{}] is missing!' 140 raise ConfigurationError('Section [{}] is missing!'
107 ''.format(self.section)) 141 ''.format(self.section))
108 142
143 self.logger.addFilter(SensitiveFilter([self.repository]))
144
109 def __enter__(self): 145 def __enter__(self):
110 self.tempdir = tempfile.mkdtemp(prefix='watched_extension_') 146 self.tempdir = tempfile.mkdtemp(prefix='watched_extension_')
111 return self 147 return self
112 148
113 def __exit__(self, exc_type, exc_value, exc_traceback): 149 def __exit__(self, exc_type, exc_value, exc_traceback):
114 if exc_value: 150 if exc_value:
115 self.logger.error(exc_type)
116 self.logger.error(exc_value) 151 self.logger.error(exc_value)
117 152
118 if not self.keep_repo: 153 if not self.keep_repo:
119 shutil.rmtree(self.tempdir, ignore_errors=True) 154 shutil.rmtree(self.tempdir, ignore_errors=True)
120 else: 155 else:
121 print('Repository for {} available at {}'.format(self.ext_name, 156 print('Repository for {} available at {}'.format(self.ext_name,
122 self.tempdir)) 157 self.tempdir))
123 if self.downloaded_file: 158 if self.downloaded_file:
124 os.remove(self.downloaded_file) 159 os.remove(self.downloaded_file)
125 160
126 return True 161 return True
127 162
128 def _fetch_latest_chrome_version(self): 163 def _fetch_latest_chrome_version(self):
129 omaha_url = 'https://omahaproxy.appspot.com/all.json?os=win' 164 response = urllib.urlopen(OMAHA_URL).read()
130 response = urllib.urlopen(omaha_url).read()
131 165
132 data = json.loads(response.decode('utf-8')) 166 data = json.loads(response.decode('utf-8'))
133 167
134 stable = [x for x in data[0]['versions'] if x['channel'] == 'stable'] 168 stable = [x for x in data[0]['versions'] if x['channel'] == 'stable']
135 return stable[0]['current_version'] 169 return stable[0]['current_version']
136 170
137 @property 171 @property
138 def cws_ext_url(self): 172 def cws_ext_url(self):
139 if not self._cws_ext_url: 173 if not self._cws_ext_url:
140 service_url = 'https://clients2.google.com/service/update2/crx' 174 ext_url = SERVICE_URL + '?prodversion={}&x=id%3D{}%26uc'.format(
141 ext_url = service_url + '?prodversion={}&x=id%3D{}%26uc'.format(
142 self._fetch_latest_chrome_version(), self.ext_id, 175 self._fetch_latest_chrome_version(), self.ext_id,
143 ) 176 )
144 self._cws_ext_url = ext_url 177 self._cws_ext_url = ext_url
145 178
146 return self._cws_ext_url 179 return self._cws_ext_url
147 180
148 @property 181 @property
149 def current_ext_version(self): 182 def current_ext_version(self):
150 if not self._current_ext_version: 183 if not self._current_ext_version:
151 updatecheck_url = self.cws_ext_url + '&response=updatecheck' 184 updatecheck_url = self.cws_ext_url + '&response=updatecheck'
(...skipping 24 matching lines...) Expand all
176 209
177 def _track_new_contents(self, message): 210 def _track_new_contents(self, message):
178 status = self._git_cmd(['status']) 211 status = self._git_cmd(['status'])
179 if b'nothing to commit' not in status: 212 if b'nothing to commit' not in status:
180 self._git_cmd(['add', '--all']) 213 self._git_cmd(['add', '--all'])
181 self._git_cmd(['commit', '-m', message]) 214 self._git_cmd(['commit', '-m', message])
182 self._git_cmd(['push', 'origin', 'master']) 215 self._git_cmd(['push', 'origin', 'master'])
183 216
184 def run(self): 217 def run(self):
185 self._parse_config() 218 self._parse_config()
186 self._assure_local_repository() 219 self._clone_repository()
187 220
188 next_version = self.current_ext_version 221 next_version = self.current_ext_version
189 if next_version not in self._get_tracked_version(): 222 if next_version not in self._get_tracked_version():
190 self.logger.info('New untracked version {} found!' 223 self.logger.info('New untracked version {} found!'
191 ''.format(next_version)) 224 ''.format(next_version))
192 if self.push or self.keep_repo: 225 if self.push or self.keep_repo:
193 self._download_ext_crx() 226 self._download_ext_crx()
194 self._unzip_to_repository() 227 self._unzip_to_repository()
195 if self.push: 228 if self.push:
196 self._track_new_contents(next_version) 229 self._track_new_contents(next_version)
(...skipping 20 matching lines...) Expand all
217 logging.disable(logging.INFO) 250 logging.disable(logging.INFO)
218 251
219 try: 252 try:
220 config = read_config(args.config_path) 253 config = read_config(args.config_path)
221 254
222 for ext_name in config.options('enabled'): 255 for ext_name in config.options('enabled'):
223 with ExtWatcher(ext_name, config, args.push, 256 with ExtWatcher(ext_name, config, args.push,
224 args.keep_repository) as watcher: 257 args.keep_repository) as watcher:
225 watcher.run() 258 watcher.run()
226 except ConfigurationError as e: 259 except ConfigurationError as e:
227 logging.error(e.message) 260 sys.exit(e.message)
LEFTRIGHT

Powered by Google App Engine
This is Rietveld