1# Copyright (C) 2018 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Send notification email if new version is found. 15 16Example usage: 17external_updater_notifier \ 18 --history ~/updater/history \ 19 --generate_change \ 20 --recipients xxx@xxx.xxx \ 21 googletest 22""" 23 24from datetime import timedelta, datetime 25import argparse 26import json 27import os 28import re 29import subprocess 30import time 31 32# pylint: disable=invalid-name 33 34def parse_args(): 35 """Parses commandline arguments.""" 36 37 parser = argparse.ArgumentParser( 38 description='Check updates for third party projects in external/.') 39 parser.add_argument('--history', 40 help='Path of history file. If doesn' 41 't exist, a new one will be created.') 42 parser.add_argument( 43 '--recipients', 44 help='Comma separated recipients of notification email.') 45 parser.add_argument( 46 '--generate_change', 47 help='If set, an upgrade change will be uploaded to Gerrit.', 48 action='store_true', 49 required=False) 50 parser.add_argument('paths', nargs='*', help='Paths of the project.') 51 parser.add_argument('--all', 52 action='store_true', 53 help='Checks all projects.') 54 55 return parser.parse_args() 56 57 58def _get_android_top(): 59 return os.environ['ANDROID_BUILD_TOP'] 60 61 62CHANGE_URL_PATTERN = r'(https:\/\/[^\s]*android-review[^\s]*) Upgrade' 63CHANGE_URL_RE = re.compile(CHANGE_URL_PATTERN) 64 65 66def _read_owner_file(proj): 67 owner_file = os.path.join(_get_android_top(), 'external', proj, 'OWNERS') 68 if not os.path.isfile(owner_file): 69 return None 70 with open(owner_file, 'r', encoding='utf-8') as f: 71 return f.read().strip() 72 73 74def _send_email(proj, latest_ver, recipient, upgrade_log): 75 print(f'Sending email for {proj}: {latest_ver}') 76 msg = "" 77 match = CHANGE_URL_RE.search(upgrade_log) 78 if match is not None: 79 subject = "[Succeeded]" 80 msg = f'An upgrade change is generated at:\n{match.group(1)}' 81 else: 82 subject = "[Failed]" 83 msg = 'Failed to generate upgrade change. See logs below for details.' 84 85 subject += f" {proj} {latest_ver}" 86 owners = _read_owner_file(proj) 87 if owners: 88 msg += '\n\nOWNERS file: \n' 89 msg += owners 90 91 msg += '\n\n' 92 msg += upgrade_log 93 94 cc_recipient = '' 95 for line in owners.splitlines(): 96 line = line.strip() 97 if line.endswith('@google.com'): 98 cc_recipient += line 99 cc_recipient += ',' 100 101 subprocess.run(['sendgmr', 102 f'--to={recipient}', 103 f'--cc={cc_recipient}', 104 f'--subject={subject}'], 105 check=True, 106 stdout=subprocess.PIPE, 107 stderr=subprocess.PIPE, 108 input=msg, 109 encoding='ascii') 110 111 112COMMIT_PATTERN = r'^[a-f0-9]{40}$' 113COMMIT_RE = re.compile(COMMIT_PATTERN) 114 115 116def is_commit(commit: str) -> bool: 117 """Whether a string looks like a SHA1 hash.""" 118 return bool(COMMIT_RE.match(commit)) 119 120 121NOTIFIED_TIME_KEY_NAME = 'latest_notified_time' 122 123 124def _should_notify(latest_ver, proj_history): 125 if latest_ver in proj_history: 126 # Processed this version before. 127 return False 128 129 timestamp = proj_history.get(NOTIFIED_TIME_KEY_NAME, 0) 130 time_diff = datetime.today() - datetime.fromtimestamp(timestamp) 131 if is_commit(latest_ver) and time_diff <= timedelta(days=30): 132 return False 133 134 return True 135 136 137def _process_results(args, history, results): 138 for proj, res in results.items(): 139 if 'latest' not in res: 140 continue 141 latest_ver = res['latest'] 142 current_ver = res['current'] 143 if latest_ver == current_ver: 144 continue 145 proj_history = history.setdefault(proj, {}) 146 if _should_notify(latest_ver, proj_history): 147 upgrade_log = _upgrade(proj) if args.generate_change else "" 148 try: 149 _send_email(proj, latest_ver, args.recipients, upgrade_log) 150 proj_history[latest_ver] = int(time.time()) 151 proj_history[NOTIFIED_TIME_KEY_NAME] = int(time.time()) 152 except subprocess.CalledProcessError as err: 153 msg = f"""Failed to send email for {proj} ({latest_ver}). 154stdout: {err.stdout} 155stderr: {err.stderr}""" 156 print(msg) 157 158 159RESULT_FILE_PATH = '/tmp/update_check_result.json' 160 161 162def send_notification(args): 163 """Compare results and send notification.""" 164 results = {} 165 with open(RESULT_FILE_PATH, 'r', encoding='utf-8') as f: 166 results = json.load(f) 167 history = {} 168 try: 169 with open(args.history, 'r', encoding='utf-8') as f: 170 history = json.load(f) 171 except (FileNotFoundError, json.decoder.JSONDecodeError): 172 pass 173 174 _process_results(args, history, results) 175 176 with open(args.history, 'w', encoding='utf-8') as f: 177 json.dump(history, f, sort_keys=True, indent=4) 178 179 180def _upgrade(proj): 181 # pylint: disable=subprocess-run-check 182 out = subprocess.run([ 183 'out/soong/host/linux-x86/bin/external_updater', 'update', proj 184 ], 185 stdout=subprocess.PIPE, 186 stderr=subprocess.PIPE, 187 cwd=_get_android_top()) 188 stdout = out.stdout.decode('utf-8') 189 stderr = out.stderr.decode('utf-8') 190 return f""" 191==================== 192| Debug Info | 193==================== 194-=-=-=-=stdout=-=-=-=- 195{stdout} 196 197-=-=-=-=stderr=-=-=-=- 198{stderr} 199""" 200 201 202def _check_updates(args): 203 params = [ 204 'out/soong/host/linux-x86/bin/external_updater', 'check', 205 '--json_output', RESULT_FILE_PATH, '--delay', '30' 206 ] 207 if args.all: 208 params.append('--all') 209 else: 210 params += args.paths 211 212 print(_get_android_top()) 213 # pylint: disable=subprocess-run-check 214 subprocess.run(params, cwd=_get_android_top()) 215 216 217def main(): 218 """The main entry.""" 219 220 args = parse_args() 221 _check_updates(args) 222 send_notification(args) 223 224 225if __name__ == '__main__': 226 main() 227