text: #!/usr/bin/env python
  # Copyright (c) 2018 The Chromium Authors. All rights reserved.
  # Use of this source code is governed by a BSD-style license that can be
  # found in the LICENSE file.
  
  """Script that triggers and waits for tasks on android-compile.skia.org"""
  
  import base64
  import hashlib
  import json
  import math
  import optparse
  import os
  import requests
  import subprocess
  import sys
  import time
  
  INFRA_BOTS_DIR = os.path.abspath(os.path.realpath(os.path.join(
      os.path.dirname(os.path.abspath(__file__)), os.pardir)))
  sys.path.insert(0, INFRA_BOTS_DIR)
  import utils
  
  
  ANDROID_COMPILE_BUCKET = 'android-compile-tasks'
  
  GS_RETRIES = 5
  GS_RETRY_WAIT_BASE = 15
  
  POLLING_FREQUENCY_SECS = 10
  DEADLINE_SECS = 60 * 60  # 60 minutes.
  
  INFRA_FAILURE_ERROR_MSG = (
        '\n\n'
        'Your run failed due to unknown infrastructure failures.\n'
        'Please contact rmistry@ or the trooper from '
        'http://skia-tree-status.appspot.com/trooper\n'
        'Sorry for the inconvenience!\n'
  )
  
  
  class AndroidCompileException(Exception):
    pass
  
  
  def _create_task_dict(options):
    """Creates a dict representation of the requested task."""
    params = {}
    params['lunch_target'] = options.lunch_target
    params['mmma_targets'] = options.mmma_targets
    params['issue'] = options.issue
    params['patchset'] = options.patchset
    params['hash'] = options.hash
    return params
  
  
  def _get_gs_bucket():
    """Returns the Google storage bucket with the gs:// prefix."""
    return 'gs://%s' % ANDROID_COMPILE_BUCKET
  
  
  def _write_to_storage(task):
    """Writes the specified compile task to Google storage."""
    with utils.tmp_dir():
      json_file = os.path.join(os.getcwd(), _get_task_file_name(task))
      with open(json_file, 'w') as f:
        json.dump(task, f)
      subprocess.check_call(['gsutil', 'cp', json_file, '%s/' % _get_gs_bucket()])
      print 'Created %s/%s' % (_get_gs_bucket(), os.path.basename(json_file))
  
  
  def _get_task_file_name(task):
    """Returns the file name of the compile task. Eg: ${issue}-${patchset}.json"""
    return '%s-%s-%s.json' % (task['lunch_target'], task['issue'],
                              task['patchset'])
  
  
  # Checks to see if task already exists in Google storage.
  # If the task has completed then the Google storage file is deleted.
  def _does_task_exist_in_storage(task):
    """Checks to see if the corresponding file of the task exists in storage.
  
    If the file exists and the task has already completed then the storage file is
    deleted and False is returned.
    """
    gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task))
    try:
      output = subprocess.check_output(['gsutil', 'cat', gs_file])
    except subprocess.CalledProcessError:
      print 'Task does not exist in Google storage'
      return False
    taskJSON = json.loads(output)
    if taskJSON.get('done'):
      print 'Task exists in Google storage and has completed.'
      print 'Deleting it so that a new run can be scheduled.'
      subprocess.check_call(['gsutil', 'rm', gs_file])
      return False
    else:
      print 'Tasks exists in Google storage and is still running.'
      return True
  
  
  def _trigger_task(options):
    """Triggers a task on the compile server by creating a file in storage."""
    task = _create_task_dict(options)
    # Check to see if file already exists in Google Storage.
    if not _does_task_exist_in_storage(task):
      _write_to_storage(task)
    return task
  
  
  def trigger_and_wait(options):
    """Triggers a task on the compile server and waits for it to complete."""
    task = _trigger_task(options)
    print 'Android Compile Task for %d/%d has been successfully added to %s.' % (
        options.issue, options.patchset, ANDROID_COMPILE_BUCKET)
    print '%s will be polled every %d seconds.' % (ANDROID_COMPILE_BUCKET,
                                                   POLLING_FREQUENCY_SECS)
  
    # Now poll the Google storage file till the task completes or till deadline
    # is hit.
    time_started_polling = time.time()
    while True:
      if (time.time() - time_started_polling) > DEADLINE_SECS:
        raise AndroidCompileException(
            'Task did not complete in the deadline of %s seconds.' % (
                DEADLINE_SECS))
  
      # Get the status of the task.
      gs_file = '%s/%s' % (_get_gs_bucket(), _get_task_file_name(task))
  
      for retry in range(GS_RETRIES):
        try:
          output = subprocess.check_output(['gsutil', 'cat', gs_file])
        except subprocess.CalledProcessError:
          raise AndroidCompileException('The %s file no longer exists.' % gs_file)
        try:
          ret = json.loads(output)
          break
        except ValueError, e:
          print 'Received output that could not be converted to json: %s' % output
          print e
          if retry == (GS_RETRIES-1):
            print '%d retries did not help' % GS_RETRIES
            raise
          waittime = GS_RETRY_WAIT_BASE * math.pow(2, retry)
          print 'Retry in %d seconds.' % waittime
          time.sleep(waittime)
  
      if ret.get('infra_failure'):
        if ret.get('error'):
          raise AndroidCompileException('Run failed with:\n\n%s\n' % ret['error'])
        else:
          # Use a general purpose error message.
          raise AndroidCompileException(INFRA_FAILURE_ERROR_MSG)
  
      if ret.get('done'):
        if not ret.get('is_master_branch', True):
          print 'The Android Framework Compile bot only works for patches and'
          print 'hashes from the master branch.'
          return 0
        elif ret['withpatch_success']:
          print 'Your run was successfully completed.'
          print 'With patch logs are here: %s' % ret['withpatch_log']
          return 0
        elif ret['nopatch_success']:
          raise AndroidCompileException('The build with the patch failed and the '
                 'build without the patch succeeded. This means that the patch '
                 'causes Android to fail compilation.\n\n'
                 'With patch logs are here: %s\n\n'
                 'No patch logs are here: %s\n\n'
                 'You can force sync of the checkout if needed here: %s\n\n' % (
                     ret['withpatch_log'], ret['nopatch_log'],
                     'https://skia-android-compile.corp.goog/'))
        else:
          print ('Both with patch and no patch builds failed. This means that the'
                 ' Android tree is currently broken. Marking this bot as '
                 'successful')
          print 'With patch logs are here: %s' % ret['withpatch_log']
          print 'No patch logs are here: %s' % ret['nopatch_log']
          return 0
  
      # Print status of the task.
      print 'Task: %s\n' % pretty_task_str(ret)
      time.sleep(POLLING_FREQUENCY_SECS)
  
  
  def pretty_task_str(task):
    status = 'Not picked up by server yet'
    if task.get('task_id'):
      status = 'Running withpatch compilation'
    if task.get('withpatch_log'):
      status = 'Running nopatch compilation'
    return '[id: %s, checkout: %s, status: %s]' % (
        task.get('task_id'), task.get('checkout'), status)
  
  
  def main():
    option_parser = optparse.OptionParser()
    option_parser.add_option(
        '', '--lunch_target', type=str, default='',
        help='The lunch target the android compile bot should build with.')
    option_parser.add_option(
        '', '--mmma_targets', type=str, default='',
        help='The comma-separated mmma targets the android compile bot should '
             'build.')
    option_parser.add_option(
        '', '--issue', type=int, default=0,
        help='The Gerrit change number to get the patch from.')
    option_parser.add_option(
        '', '--patchset', type=int, default=0,
        help='The Gerrit change patchset to use.')
    option_parser.add_option(
        '', '--hash', type=str, default='',
        help='The Skia repo hash to compile against.')
    options, _ = option_parser.parse_args()
    sys.exit(trigger_and_wait(options))
  
  
  if __name__ == '__main__':
    main()
  