• 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
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