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