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 [email protected] \ 21 googletest 22""" 23 24from datetime import timedelta, datetime 25import argparse 26import json 27import os 28import re 29import subprocess 30import time 31 32import git_utils 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( 40 '--history', 41 help='Path of history file. If doesn' 42 't exist, a new one will be created.') 43 parser.add_argument( 44 '--recipients', 45 help='Comma separated recipients of notification email.') 46 parser.add_argument( 47 '--generate_change', 48 help='If set, an upgrade change will be uploaded to Gerrit.', 49 action='store_true', required=False) 50 parser.add_argument( 51 'paths', nargs='*', 52 help='Paths of the project.') 53 parser.add_argument( 54 '--all', action='store_true', 55 help='Checks all projects.') 56 57 return parser.parse_args() 58 59 60def _get_android_top(): 61 return os.environ['ANDROID_BUILD_TOP'] 62 63 64CHANGE_URL_PATTERN = r'(https:\/\/[^\s]*android-review[^\s]*) Upgrade' 65CHANGE_URL_RE = re.compile(CHANGE_URL_PATTERN) 66 67 68def _read_owner_file(proj): 69 owner_file = os.path.join(_get_android_top(), 'external', proj, 'OWNERS') 70 if not os.path.isfile(owner_file): 71 return None 72 with open(owner_file, 'r') as f: 73 return f.read().strip() 74 75 76def _send_email(proj, latest_ver, recipient, upgrade_log): 77 print('Sending email for {}: {}'.format(proj, latest_ver)) 78 msg = "New version: {}".format(latest_ver) 79 match = CHANGE_URL_RE.search(upgrade_log) 80 if match is not None: 81 msg += '\n\nAn upgrade change is generated at:\n{}'.format( 82 match.group(1)) 83 84 owners = _read_owner_file(proj) 85 if owners: 86 msg += '\n\nOWNERS file: \n' 87 msg += owners 88 89 msg += '\n\n' 90 msg += upgrade_log 91 92 subprocess.run(['sendgmr', '--to=' + recipient, 93 '--subject=' + proj], check=True, 94 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 95 input=msg, encoding='ascii') 96 97 98NOTIFIED_TIME_KEY_NAME = 'latest_notified_time' 99 100 101def _should_notify(latest_ver, proj_history): 102 if latest_ver in proj_history: 103 # Processed this version before. 104 return False 105 106 timestamp = proj_history.get(NOTIFIED_TIME_KEY_NAME, 0) 107 time_diff = datetime.today() - datetime.fromtimestamp(timestamp) 108 if git_utils.is_commit(latest_ver) and time_diff <= timedelta(days=30): 109 return False 110 111 return True 112 113 114def _process_results(args, history, results): 115 for proj, res in results.items(): 116 if 'latest' not in res: 117 continue 118 latest_ver = res['latest'] 119 current_ver = res['current'] 120 if latest_ver == current_ver: 121 continue 122 proj_history = history.setdefault(proj, {}) 123 if _should_notify(latest_ver, proj_history): 124 upgrade_log = _upgrade(proj) if args.generate_change else "" 125 try: 126 _send_email(proj, latest_ver, args.recipients, upgrade_log) 127 proj_history[latest_ver] = int(time.time()) 128 proj_history[NOTIFIED_TIME_KEY_NAME] = int(time.time()) 129 except subprocess.CalledProcessError as err: 130 msg = """Failed to send email for {} ({}). 131stdout: {} 132stderr: {}""".format(proj, latest_ver, err.stdout, err.stderr) 133 print(msg) 134 135 136RESULT_FILE_PATH = '/tmp/update_check_result.json' 137 138 139def send_notification(args): 140 """Compare results and send notification.""" 141 results = {} 142 with open(RESULT_FILE_PATH, 'r') as f: 143 results = json.load(f) 144 history = {} 145 try: 146 with open(args.history, 'r') as f: 147 history = json.load(f) 148 except FileNotFoundError: 149 pass 150 151 _process_results(args, history, results) 152 153 with open(args.history, 'w') as f: 154 json.dump(history, f, sort_keys=True, indent=4) 155 156 157def _upgrade(proj): 158 out = subprocess.run(['out/soong/host/linux-x86/bin/external_updater', 159 'update', '--branch_and_commit', '--push_change', 160 proj], 161 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 162 cwd=_get_android_top()) 163 stdout = out.stdout.decode('utf-8') 164 stderr = out.stderr.decode('utf-8') 165 return """ 166==================== 167| Debug Info | 168==================== 169-=-=-=-=stdout=-=-=-=- 170{} 171 172-=-=-=-=stderr=-=-=-=- 173{} 174""".format(stdout, stderr) 175 176 177def _check_updates(args): 178 params = ['out/soong/host/linux-x86/bin/external_updater', 179 'check', '--json_output', RESULT_FILE_PATH, 180 '--delay', '30'] 181 if args.all: 182 params.append('--all') 183 else: 184 params += args.paths 185 186 print(_get_android_top()) 187 subprocess.run(params, cwd=_get_android_top()) 188 189 190def main(): 191 """The main entry.""" 192 193 args = parse_args() 194 _check_updates(args) 195 send_notification(args) 196 197 198if __name__ == '__main__': 199 main() 200