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') as f: 71 return f.read().strip() 72 73 74def _send_email(proj, latest_ver, recipient, upgrade_log): 75 print('Sending email for {}: {}'.format(proj, latest_ver)) 76 msg = "" 77 match = CHANGE_URL_RE.search(upgrade_log) 78 if match is not None: 79 subject = "[Succeeded]" 80 msg = 'An upgrade change is generated at:\n{}'.format( 81 match.group(1)) 82 else: 83 subject = "[Failed]" 84 msg = 'Failed to generate upgrade change. See logs below for details.' 85 86 subject += f" {proj} {latest_ver}" 87 owners = _read_owner_file(proj) 88 if owners: 89 msg += '\n\nOWNERS file: \n' 90 msg += owners 91 92 msg += '\n\n' 93 msg += upgrade_log 94 95 cc_recipient = '' 96 for line in owners.splitlines(): 97 line = line.strip() 98 if line.endswith('@google.com'): 99 cc_recipient += line 100 cc_recipient += ',' 101 102 subprocess.run(['sendgmr', 103 f'--to={recipient}', 104 f'--cc={cc_recipient}', 105 f'--subject={subject}'], 106 check=True, 107 stdout=subprocess.PIPE, 108 stderr=subprocess.PIPE, 109 input=msg, 110 encoding='ascii') 111 112 113COMMIT_PATTERN = r'^[a-f0-9]{40}$' 114COMMIT_RE = re.compile(COMMIT_PATTERN) 115 116 117def is_commit(commit: str) -> bool: 118 """Whether a string looks like a SHA1 hash.""" 119 return bool(COMMIT_RE.match(commit)) 120 121 122NOTIFIED_TIME_KEY_NAME = 'latest_notified_time' 123 124 125def _should_notify(latest_ver, proj_history): 126 if latest_ver in proj_history: 127 # Processed this version before. 128 return False 129 130 timestamp = proj_history.get(NOTIFIED_TIME_KEY_NAME, 0) 131 time_diff = datetime.today() - datetime.fromtimestamp(timestamp) 132 if is_commit(latest_ver) and time_diff <= timedelta(days=30): 133 return False 134 135 return True 136 137 138def _process_results(args, history, results): 139 for proj, res in results.items(): 140 if 'latest' not in res: 141 continue 142 latest_ver = res['latest'] 143 current_ver = res['current'] 144 if latest_ver == current_ver: 145 continue 146 proj_history = history.setdefault(proj, {}) 147 if _should_notify(latest_ver, proj_history): 148 upgrade_log = _upgrade(proj) if args.generate_change else "" 149 try: 150 _send_email(proj, latest_ver, args.recipients, upgrade_log) 151 proj_history[latest_ver] = int(time.time()) 152 proj_history[NOTIFIED_TIME_KEY_NAME] = int(time.time()) 153 except subprocess.CalledProcessError as err: 154 msg = """Failed to send email for {} ({}). 155stdout: {} 156stderr: {}""".format(proj, latest_ver, err.stdout, err.stderr) 157 print(msg) 158 159 160RESULT_FILE_PATH = '/tmp/update_check_result.json' 161 162 163def send_notification(args): 164 """Compare results and send notification.""" 165 results = {} 166 with open(RESULT_FILE_PATH, 'r') as f: 167 results = json.load(f) 168 history = {} 169 try: 170 with open(args.history, 'r') as f: 171 history = json.load(f) 172 except (FileNotFoundError, json.decoder.JSONDecodeError): 173 pass 174 175 _process_results(args, history, results) 176 177 with open(args.history, 'w') as f: 178 json.dump(history, f, sort_keys=True, indent=4) 179 180 181def _upgrade(proj): 182 # pylint: disable=subprocess-run-check 183 out = subprocess.run([ 184 'out/soong/host/linux-x86/bin/external_updater', 'update', 185 '--branch_and_commit', '--push_change', proj 186 ], 187 stdout=subprocess.PIPE, 188 stderr=subprocess.PIPE, 189 cwd=_get_android_top()) 190 stdout = out.stdout.decode('utf-8') 191 stderr = out.stderr.decode('utf-8') 192 return """ 193==================== 194| Debug Info | 195==================== 196-=-=-=-=stdout=-=-=-=- 197{} 198 199-=-=-=-=stderr=-=-=-=- 200{} 201""".format(stdout, stderr) 202 203 204def _check_updates(args): 205 params = [ 206 'out/soong/host/linux-x86/bin/external_updater', 'check', 207 '--json_output', RESULT_FILE_PATH, '--delay', '30' 208 ] 209 if args.all: 210 params.append('--all') 211 else: 212 params += args.paths 213 214 print(_get_android_top()) 215 # pylint: disable=subprocess-run-check 216 subprocess.run(params, cwd=_get_android_top()) 217 218 219def main(): 220 """The main entry.""" 221 222 args = parse_args() 223 _check_updates(args) 224 send_notification(args) 225 226 227if __name__ == '__main__': 228 main() 229