• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
16 Example usage:
17 external_updater_notifier \
18     --history ~/updater/history \
19     --generate_change \
20     --recipients xxx@xxx.xxx \
21     googletest
22 """
23 
24 from datetime import timedelta, datetime
25 import argparse
26 import json
27 import os
28 import re
29 import subprocess
30 import time
31 
32 # pylint: disable=invalid-name
33 
34 def 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 
58 def _get_android_top():
59     return os.environ['ANDROID_BUILD_TOP']
60 
61 
62 CHANGE_URL_PATTERN = r'(https:\/\/[^\s]*android-review[^\s]*) Upgrade'
63 CHANGE_URL_RE = re.compile(CHANGE_URL_PATTERN)
64 
65 
66 def _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 
74 def _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 
112 COMMIT_PATTERN = r'^[a-f0-9]{40}$'
113 COMMIT_RE = re.compile(COMMIT_PATTERN)
114 
115 
116 def is_commit(commit: str) -> bool:
117     """Whether a string looks like a SHA1 hash."""
118     return bool(COMMIT_RE.match(commit))
119 
120 
121 NOTIFIED_TIME_KEY_NAME = 'latest_notified_time'
122 
123 
124 def _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 
137 def _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}).
154 stdout: {err.stdout}
155 stderr: {err.stderr}"""
156                 print(msg)
157 
158 
159 RESULT_FILE_PATH = '/tmp/update_check_result.json'
160 
161 
162 def 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 
180 def _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 
202 def _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 
217 def main():
218     """The main entry."""
219 
220     args = parse_args()
221     _check_updates(args)
222     send_notification(args)
223 
224 
225 if __name__ == '__main__':
226     main()
227