1#!/usr/bin/env python 2# Copyright (c) 2018 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Script that triggers and waits for tasks on android-compile.skia.org""" 7 8import base64 9import hashlib 10import json 11import math 12import optparse 13import os 14import requests 15import subprocess 16import sys 17import time 18 19INFRA_BOTS_DIR = os.path.abspath(os.path.realpath(os.path.join( 20 os.path.dirname(os.path.abspath(__file__)), os.pardir))) 21sys.path.insert(0, INFRA_BOTS_DIR) 22import utils 23 24 25ANDROID_COMPILE_BUCKET = 'android-compile-tasks' 26 27GS_RETRIES = 5 28GS_RETRY_WAIT_BASE = 15 29 30POLLING_FREQUENCY_SECS = 10 31DEADLINE_SECS = 2* 60 * 60 # 2 hours. 32 33INFRA_FAILURE_ERROR_MSG = ( 34 '\n\n' 35 'Your run failed due to unknown infrastructure failures.\n' 36 'Please contact rmistry@ or the trooper from ' 37 'http://skia-tree-status.appspot.com/trooper\n' 38 'Sorry for the inconvenience!\n' 39) 40 41 42class AndroidCompileException(Exception): 43 pass 44 45 46def _create_task_dict(options): 47 """Creates a dict representation of the requested task.""" 48 params = {} 49 params['lunch_target'] = options.lunch_target 50 params['mmma_targets'] = options.mmma_targets 51 params['issue'] = options.issue 52 params['patchset'] = options.patchset 53 params['hash'] = options.hash 54 return params 55 56 57def _get_gs_bucket(): 58 """Returns the Google storage bucket with the gs:// prefix.""" 59 return 'gs://%s' % ANDROID_COMPILE_BUCKET 60 61 62def _write_to_storage(task): 63 """Writes the specified compile task to Google storage.""" 64 with utils.tmp_dir(): 65 json_file = os.path.join(os.getcwd(), _get_task_file_name(task)) 66 with open(json_file, 'w') as f: 67 json.dump(task, f) 68 subprocess.check_call(['gsutil', 'cp', json_file, '%s/' % _get_gs_bucket()]) 69 print 'Created %s/%s' % (_get_gs_bucket(), os.path.basename(json_file)) 70 71 72def _get_task_file_name(task): 73 """Returns the file name of the compile task. Eg: ${issue}-${patchset}.json""" 74 return '%s-%s-%s.json' % (task['lunch_target'], task['issue'], 75 task['patchset']) 76 77 78# Checks to see if task already exists in Google storage. 79# If the task has completed then the Google storage file is deleted. 80def _does_task_exist_in_storage(task): 81 """Checks to see if the corresponding file of the task exists in storage. 82 83 If the file exists and the task has already completed then the storage file is 84 deleted and False is returned. 85 """ 86 gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task)) 87 try: 88 output = subprocess.check_output(['gsutil', 'cat', gs_file]) 89 except subprocess.CalledProcessError: 90 print 'Task does not exist in Google storage' 91 return False 92 taskJSON = json.loads(output) 93 if taskJSON.get('done'): 94 print 'Task exists in Google storage and has completed.' 95 print 'Deleting it so that a new run can be scheduled.' 96 subprocess.check_call(['gsutil', 'rm', gs_file]) 97 return False 98 else: 99 print 'Tasks exists in Google storage and is still running.' 100 return True 101 102 103def _trigger_task(options): 104 """Triggers a task on the compile server by creating a file in storage.""" 105 task = _create_task_dict(options) 106 # Check to see if file already exists in Google Storage. 107 if not _does_task_exist_in_storage(task): 108 _write_to_storage(task) 109 return task 110 111 112def trigger_and_wait(options): 113 """Triggers a task on the compile server and waits for it to complete.""" 114 task = _trigger_task(options) 115 print 'Android Compile Task for %d/%d has been successfully added to %s.' % ( 116 options.issue, options.patchset, ANDROID_COMPILE_BUCKET) 117 print '%s will be polled every %d seconds.' % (ANDROID_COMPILE_BUCKET, 118 POLLING_FREQUENCY_SECS) 119 120 # Now poll the Google storage file till the task completes or till deadline 121 # is hit. 122 time_started_polling = time.time() 123 while True: 124 if (time.time() - time_started_polling) > DEADLINE_SECS: 125 raise AndroidCompileException( 126 'Task did not complete in the deadline of %s seconds.' % ( 127 DEADLINE_SECS)) 128 129 # Get the status of the task. 130 gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task)) 131 132 for retry in range(GS_RETRIES): 133 try: 134 output = subprocess.check_output(['gsutil', 'cat', gs_file]) 135 except subprocess.CalledProcessError: 136 raise AndroidCompileException('The %s file no longer exists.' % gs_file) 137 try: 138 ret = json.loads(output) 139 break 140 except ValueError, e: 141 print 'Received output that could not be converted to json: %s' % output 142 print e 143 if retry == (GS_RETRIES-1): 144 print '%d retries did not help' % GS_RETRIES 145 raise 146 waittime = GS_RETRY_WAIT_BASE * math.pow(2, retry) 147 print 'Retry in %d seconds.' % waittime 148 time.sleep(waittime) 149 150 if ret.get('infra_failure'): 151 if ret.get('error'): 152 raise AndroidCompileException('Run failed with:\n\n%s\n' % ret['error']) 153 else: 154 # Use a general purpose error message. 155 raise AndroidCompileException(INFRA_FAILURE_ERROR_MSG) 156 157 if ret.get('done'): 158 if not ret.get('is_master_branch', True): 159 print 'The Android Framework Compile bot only works for patches and' 160 print 'hashes from the master branch.' 161 return 0 162 elif ret['withpatch_success']: 163 print 'Your run was successfully completed.' 164 print 'With patch logs are here: %s' % ret['withpatch_log'] 165 return 0 166 elif ret['nopatch_success']: 167 raise AndroidCompileException('The build with the patch failed and the ' 168 'build without the patch succeeded. This means that the patch ' 169 'causes Android to fail compilation.\n\n' 170 'With patch logs are here: %s\n\n' 171 'No patch logs are here: %s\n\n' 172 'You can force sync of the checkout if needed here: %s\n\n' % ( 173 ret['withpatch_log'], ret['nopatch_log'], 174 'https://skia-android-compile.corp.goog/')) 175 else: 176 print ('Both with patch and no patch builds failed. This means that the' 177 ' Android tree is currently broken. Marking this bot as ' 178 'successful') 179 print 'With patch logs are here: %s' % ret['withpatch_log'] 180 print 'No patch logs are here: %s' % ret['nopatch_log'] 181 return 0 182 183 # Print status of the task. 184 print 'Task: %s\n' % pretty_task_str(ret) 185 time.sleep(POLLING_FREQUENCY_SECS) 186 187 188def pretty_task_str(task): 189 status = 'Not picked up by server yet' 190 if task.get('task_id'): 191 status = 'Running withpatch compilation' 192 if task.get('withpatch_log'): 193 status = 'Running nopatch compilation' 194 return '[id: %s, checkout: %s, status: %s]' % ( 195 task.get('task_id'), task.get('checkout'), status) 196 197 198def main(): 199 option_parser = optparse.OptionParser() 200 option_parser.add_option( 201 '', '--lunch_target', type=str, default='', 202 help='The lunch target the android compile bot should build with.') 203 option_parser.add_option( 204 '', '--mmma_targets', type=str, default='', 205 help='The comma-separated mmma targets the android compile bot should ' 206 'build.') 207 option_parser.add_option( 208 '', '--issue', type=int, default=0, 209 help='The Gerrit change number to get the patch from.') 210 option_parser.add_option( 211 '', '--patchset', type=int, default=0, 212 help='The Gerrit change patchset to use.') 213 option_parser.add_option( 214 '', '--hash', type=str, default='', 215 help='The Skia repo hash to compile against.') 216 options, _ = option_parser.parse_args() 217 sys.exit(trigger_and_wait(options)) 218 219 220if __name__ == '__main__': 221 main() 222