tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

provision_devices.py (22301B)


      1 #!/usr/bin/env vpython3
      2 #
      3 # Copyright 2013 The Chromium Authors
      4 # Use of this source code is governed by a BSD-style license that can be
      5 # found in the LICENSE file.
      6 
      7 """Provisions Android devices with settings required for bots.
      8 
      9 Usage:
     10  ./provision_devices.py [-d <device serial number>]
     11 """
     12 
     13 import argparse
     14 import datetime
     15 import json
     16 import logging
     17 import os
     18 import posixpath
     19 import re
     20 import subprocess
     21 import sys
     22 import time
     23 
     24 # Import _strptime before threaded code. datetime.datetime.strptime is
     25 # threadsafe except for the initial import of the _strptime module.
     26 # See crbug.com/584730 and https://bugs.python.org/issue7980.
     27 import _strptime  # pylint: disable=unused-import
     28 
     29 import devil_chromium
     30 from devil.android import battery_utils
     31 from devil.android import device_denylist
     32 from devil.android import device_errors
     33 from devil.android import device_temp_file
     34 from devil.android import device_utils
     35 from devil.android.sdk import keyevent
     36 from devil.android.sdk import version_codes
     37 from devil.constants import exit_codes
     38 from devil.utils import run_tests_helper
     39 from devil.utils import timeout_retry
     40 from pylib import constants
     41 from pylib import device_settings
     42 from pylib.constants import host_paths
     43 
     44 _SYSTEM_WEBVIEW_PATHS = ['/system/app/webview', '/system/app/WebViewGoogle']
     45 _CHROME_PACKAGE_REGEX = re.compile('.*chrom.*')
     46 _TOMBSTONE_REGEX = re.compile('tombstone.*')
     47 
     48 
     49 class _DEFAULT_TIMEOUTS:
     50  # L can take a while to reboot after a wipe.
     51  LOLLIPOP = 600
     52  PRE_LOLLIPOP = 180
     53 
     54  HELP_TEXT = '{}s on L, {}s on pre-L'.format(LOLLIPOP, PRE_LOLLIPOP)
     55 
     56 
     57 class _PHASES:
     58  WIPE = 'wipe'
     59  PROPERTIES = 'properties'
     60  FINISH = 'finish'
     61 
     62  ALL = [WIPE, PROPERTIES, FINISH]
     63 
     64 
     65 def ProvisionDevices(args):
     66  denylist = (device_denylist.Denylist(args.denylist_file)
     67              if args.denylist_file else None)
     68  devices = [
     69      d for d in device_utils.DeviceUtils.HealthyDevices(denylist)
     70      if not args.emulators or d.is_emulator
     71  ]
     72  if args.device:
     73    devices = [d for d in devices if d == args.device]
     74  if not devices:
     75    raise device_errors.DeviceUnreachableError(args.device)
     76  parallel_devices = device_utils.DeviceUtils.parallel(devices)
     77  if args.emulators:
     78    parallel_devices.pMap(SetProperties, args)
     79  else:
     80    parallel_devices.pMap(ProvisionDevice, denylist, args)
     81  if args.auto_reconnect:
     82    _LaunchHostHeartbeat()
     83  denylisted_devices = denylist.Read() if denylist else []
     84  if args.output_device_denylist:
     85    with open(args.output_device_denylist, 'w') as f:
     86      json.dump(denylisted_devices, f)
     87  if all(d in denylisted_devices for d in devices):
     88    raise device_errors.NoDevicesError
     89  return 0
     90 
     91 
     92 def ProvisionDevice(device, denylist, options):
     93  def should_run_phase(phase_name):
     94    return not options.phases or phase_name in options.phases
     95 
     96  def run_phase(phase_func, reboot_timeout, reboot=True):
     97    try:
     98      device.WaitUntilFullyBooted(timeout=reboot_timeout, retries=0)
     99    except device_errors.CommandTimeoutError:
    100      logging.error('Device did not finish booting. Will try to reboot.')
    101      device.Reboot(timeout=reboot_timeout)
    102    phase_func(device, options)
    103    if reboot:
    104      device.Reboot(False, retries=0)
    105      device.adb.WaitForDevice()
    106 
    107  try:
    108    if options.reboot_timeout:
    109      reboot_timeout = options.reboot_timeout
    110    elif device.build_version_sdk >= version_codes.LOLLIPOP:
    111      reboot_timeout = _DEFAULT_TIMEOUTS.LOLLIPOP
    112    else:
    113      reboot_timeout = _DEFAULT_TIMEOUTS.PRE_LOLLIPOP
    114 
    115    if should_run_phase(_PHASES.WIPE):
    116      if (options.chrome_specific_wipe or device.IsUserBuild() or
    117          device.build_version_sdk >= version_codes.MARSHMALLOW):
    118        run_phase(WipeChromeData, reboot_timeout)
    119      else:
    120        run_phase(WipeDevice, reboot_timeout)
    121 
    122    if should_run_phase(_PHASES.PROPERTIES):
    123      run_phase(SetProperties, reboot_timeout)
    124 
    125    if should_run_phase(_PHASES.FINISH):
    126      run_phase(FinishProvisioning, reboot_timeout, reboot=False)
    127 
    128    if options.chrome_specific_wipe:
    129      package = "com.google.android.gms"
    130      version_name = device.GetApplicationVersion(package)
    131      logging.info("Version name for %s is %s", package, version_name)
    132 
    133    CheckExternalStorage(device)
    134 
    135  except device_errors.CommandTimeoutError:
    136    logging.exception('Timed out waiting for device %s. Adding to denylist.',
    137                      str(device))
    138    if denylist:
    139      denylist.Extend([str(device)], reason='provision_timeout')
    140 
    141  except (device_errors.CommandFailedError,
    142          device_errors.DeviceUnreachableError):
    143    logging.exception('Failed to provision device %s. Adding to denylist.',
    144                      str(device))
    145    if denylist:
    146      denylist.Extend([str(device)], reason='provision_failure')
    147 
    148 
    149 def CheckExternalStorage(device):
    150  """Checks that storage is writable and if not makes it writable.
    151 
    152  Arguments:
    153    device: The device to check.
    154  """
    155  try:
    156    with device_temp_file.DeviceTempFile(
    157        device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f:
    158      device.WriteFile(f.name, 'test')
    159  except device_errors.CommandFailedError:
    160    logging.info('External storage not writable. Remounting / as RW')
    161    device.RunShellCommand(['mount', '-o', 'remount,rw', '/'],
    162                           check_return=True, as_root=True)
    163    device.EnableRoot()
    164    with device_temp_file.DeviceTempFile(
    165        device.adb, suffix='.sh', dir=device.GetExternalStoragePath()) as f:
    166      device.WriteFile(f.name, 'test')
    167 
    168 def WipeChromeData(device, options):
    169  """Wipes chrome specific data from device
    170 
    171  (1) uninstall any app whose name matches *chrom*, except
    172      com.android.chrome, which is the chrome stable package. Doing so also
    173      removes the corresponding dirs under /data/data/ and /data/app/
    174  (2) remove any dir under /data/app-lib/ whose name matches *chrom*
    175  (3) remove any files under /data/tombstones/ whose name matches "tombstone*"
    176  (4) remove /data/local.prop if there is any
    177  (5) remove /data/local/chrome-command-line if there is any
    178  (6) remove anything under /data/local/.config/ if the dir exists
    179      (this is telemetry related)
    180  (7) remove anything under /data/local/tmp/
    181 
    182  Arguments:
    183    device: the device to wipe
    184  """
    185  if options.skip_wipe:
    186    return
    187 
    188  try:
    189    if device.IsUserBuild():
    190      _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX,
    191                        constants.PACKAGE_INFO['chrome_stable'].package)
    192      device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(),
    193                             check_return=True)
    194      device.RunShellCommand('rm -rf /data/local/tmp/*', check_return=True)
    195    else:
    196      device.EnableRoot()
    197      _UninstallIfMatch(device, _CHROME_PACKAGE_REGEX,
    198                        constants.PACKAGE_INFO['chrome_stable'].package)
    199      _WipeUnderDirIfMatch(device, '/data/app-lib/', _CHROME_PACKAGE_REGEX)
    200      _WipeUnderDirIfMatch(device, '/data/tombstones/', _TOMBSTONE_REGEX)
    201 
    202      _WipeFileOrDir(device, '/data/local.prop')
    203      _WipeFileOrDir(device, '/data/local/chrome-command-line')
    204      _WipeFileOrDir(device, '/data/local/.config/')
    205      _WipeFileOrDir(device, '/data/local/tmp/')
    206      device.RunShellCommand('rm -rf %s/*' % device.GetExternalStoragePath(),
    207                             check_return=True)
    208  except device_errors.CommandFailedError:
    209    logging.exception('Possible failure while wiping the device. '
    210                      'Attempting to continue.')
    211 
    212 
    213 def WipeDevice(device, options):
    214  """Wipes data from device, keeping only the adb_keys for authorization.
    215 
    216  After wiping data on a device that has been authorized, adb can still
    217  communicate with the device, but after reboot the device will need to be
    218  re-authorized because the adb keys file is stored in /data/misc/adb/.
    219  Thus, adb_keys file is rewritten so the device does not need to be
    220  re-authorized.
    221 
    222  Arguments:
    223    device: the device to wipe
    224  """
    225  if options.skip_wipe:
    226    return
    227 
    228  try:
    229    device.EnableRoot()
    230    device_authorized = device.FileExists(constants.ADB_KEYS_FILE)
    231    if device_authorized:
    232      adb_keys = device.ReadFile(constants.ADB_KEYS_FILE,
    233                                 as_root=True).splitlines()
    234    device.RunShellCommand(['wipe', 'data'],
    235                           as_root=True, check_return=True)
    236    device.adb.WaitForDevice()
    237 
    238    if device_authorized:
    239      adb_keys_set = set(adb_keys)
    240      for adb_key_file in options.adb_key_files or []:
    241        try:
    242          with open(adb_key_file, 'r') as f:
    243            adb_public_keys = f.readlines()
    244          adb_keys_set.update(adb_public_keys)
    245        except IOError:
    246          logging.warning('Unable to find adb keys file %s.', adb_key_file)
    247      _WriteAdbKeysFile(device, '\n'.join(adb_keys_set))
    248  except device_errors.CommandFailedError:
    249    logging.exception('Possible failure while wiping the device. '
    250                      'Attempting to continue.')
    251 
    252 
    253 def _WriteAdbKeysFile(device, adb_keys_string):
    254  dir_path = posixpath.dirname(constants.ADB_KEYS_FILE)
    255  device.RunShellCommand(['mkdir', '-p', dir_path],
    256                         as_root=True, check_return=True)
    257  device.RunShellCommand(['restorecon', dir_path],
    258                         as_root=True, check_return=True)
    259  device.WriteFile(constants.ADB_KEYS_FILE, adb_keys_string, as_root=True)
    260  device.RunShellCommand(['restorecon', constants.ADB_KEYS_FILE],
    261                         as_root=True, check_return=True)
    262 
    263 
    264 def SetProperties(device, options):
    265  try:
    266    device.EnableRoot()
    267  except device_errors.CommandFailedError as e:
    268    logging.warning(str(e))
    269 
    270  if not device.IsUserBuild():
    271    _ConfigureLocalProperties(device, options.enable_java_debug)
    272  else:
    273    logging.warning('Cannot configure properties in user builds.')
    274  device_settings.ConfigureContentSettings(
    275      device, device_settings.DETERMINISTIC_DEVICE_SETTINGS)
    276  if options.disable_location:
    277    device_settings.ConfigureContentSettings(
    278        device, device_settings.DISABLE_LOCATION_SETTINGS)
    279  else:
    280    device_settings.ConfigureContentSettings(
    281        device, device_settings.ENABLE_LOCATION_SETTINGS)
    282 
    283  if options.disable_mock_location:
    284    device_settings.ConfigureContentSettings(
    285        device, device_settings.DISABLE_MOCK_LOCATION_SETTINGS)
    286  else:
    287    device_settings.ConfigureContentSettings(
    288        device, device_settings.ENABLE_MOCK_LOCATION_SETTINGS)
    289 
    290  device_settings.SetLockScreenSettings(device)
    291  if options.disable_network:
    292    device_settings.ConfigureContentSettings(
    293        device, device_settings.NETWORK_DISABLED_SETTINGS)
    294    if device.build_version_sdk >= version_codes.MARSHMALLOW:
    295      # Ensure that NFC is also switched off.
    296      device.RunShellCommand(['svc', 'nfc', 'disable'],
    297                             as_root=True, check_return=True)
    298 
    299  if options.disable_system_chrome:
    300    # The system chrome version on the device interferes with some tests.
    301    device.RunShellCommand(['pm', 'disable', 'com.android.chrome'],
    302                           check_return=True)
    303 
    304  if options.remove_system_webview:
    305    if any(device.PathExists(p) for p in _SYSTEM_WEBVIEW_PATHS):
    306      logging.info('System WebView exists and needs to be removed')
    307      if device.HasRoot():
    308        # Disabled Marshmallow's Verity security feature
    309        if device.build_version_sdk >= version_codes.MARSHMALLOW:
    310          device.adb.DisableVerity()
    311          device.Reboot()
    312          device.WaitUntilFullyBooted()
    313          device.EnableRoot()
    314 
    315        # This is required, e.g., to replace the system webview on a device.
    316        device.adb.Remount()
    317        device.RunShellCommand(['stop'], check_return=True)
    318        device.RunShellCommand(['rm', '-rf'] + _SYSTEM_WEBVIEW_PATHS,
    319                               check_return=True)
    320        device.RunShellCommand(['start'], check_return=True)
    321      else:
    322        logging.warning('Cannot remove system webview from a non-rooted device')
    323    else:
    324      logging.info('System WebView already removed')
    325 
    326  # Some device types can momentarily disappear after setting properties.
    327  device.adb.WaitForDevice()
    328 
    329 
    330 def _ConfigureLocalProperties(device, java_debug=True):
    331  """Set standard readonly testing device properties prior to reboot."""
    332  local_props = [
    333      'persist.sys.usb.config=adb',
    334      'ro.monkey=1',
    335      'ro.test_harness=1',
    336      'ro.audio.silent=1',
    337      'ro.setupwizard.mode=DISABLED',
    338      ]
    339  if java_debug:
    340    local_props.append(
    341        '%s=all' % device_utils.DeviceUtils.JAVA_ASSERT_PROPERTY)
    342    local_props.append('debug.checkjni=1')
    343  try:
    344    device.WriteFile(
    345        device.LOCAL_PROPERTIES_PATH,
    346        '\n'.join(local_props), as_root=True)
    347    # Android will not respect the local props file if it is world writable.
    348    device.RunShellCommand(
    349        ['chmod', '644', device.LOCAL_PROPERTIES_PATH],
    350        as_root=True, check_return=True)
    351  except device_errors.CommandFailedError:
    352    logging.exception('Failed to configure local properties.')
    353 
    354 
    355 def FinishProvisioning(device, options):
    356  # The lockscreen can't be disabled on user builds, so send a keyevent
    357  # to unlock it.
    358  if device.IsUserBuild():
    359    device.SendKeyEvent(keyevent.KEYCODE_MENU)
    360 
    361  if options.min_battery_level is not None:
    362    battery = battery_utils.BatteryUtils(device)
    363    try:
    364      battery.ChargeDeviceToLevel(options.min_battery_level)
    365    except device_errors.DeviceChargingError:
    366      device.Reboot()
    367      battery.ChargeDeviceToLevel(options.min_battery_level)
    368 
    369  if options.max_battery_temp is not None:
    370    try:
    371      battery = battery_utils.BatteryUtils(device)
    372      battery.LetBatteryCoolToTemperature(options.max_battery_temp)
    373    except device_errors.CommandFailedError:
    374      logging.exception('Unable to let battery cool to specified temperature.')
    375 
    376  def _set_and_verify_date():
    377    if device.build_version_sdk >= version_codes.MARSHMALLOW:
    378      date_format = '%m%d%H%M%Y.%S'
    379      set_date_command = ['date', '-u']
    380      get_date_command = ['date', '-u']
    381    else:
    382      date_format = '%Y%m%d.%H%M%S'
    383      set_date_command = ['date', '-s']
    384      get_date_command = ['date']
    385 
    386    # TODO(jbudorick): This is wrong on pre-M devices -- get/set are
    387    # dealing in local time, but we're setting based on GMT.
    388    strgmtime = time.strftime(date_format, time.gmtime())
    389    set_date_command.append(strgmtime)
    390    device.RunShellCommand(set_date_command, as_root=True, check_return=True)
    391 
    392    get_date_command.append('+"%Y%m%d.%H%M%S"')
    393    device_time = device.RunShellCommand(
    394        get_date_command, as_root=True, single_line=True).replace('"', '')
    395    device_time = datetime.datetime.strptime(device_time, "%Y%m%d.%H%M%S")
    396    correct_time = datetime.datetime.strptime(strgmtime, date_format)
    397    tdelta = abs(correct_time - device_time).seconds
    398    if tdelta <= 1:
    399      logging.info('Date/time successfully set on %s', device)
    400      return True
    401    logging.error('Date mismatch. Device: %s Correct: %s',
    402                  device_time.isoformat(), correct_time.isoformat())
    403    return False
    404 
    405  # Sometimes the date is not set correctly on the devices. Retry on failure.
    406  if device.IsUserBuild():
    407    # TODO(bpastene): Figure out how to set the date & time on user builds.
    408    pass
    409  else:
    410    if not timeout_retry.WaitFor(
    411        _set_and_verify_date, wait_period=1, max_tries=2):
    412      raise device_errors.CommandFailedError(
    413          'Failed to set date & time.', device_serial=str(device))
    414 
    415  props = device.RunShellCommand('getprop', check_return=True)
    416  for prop in props:
    417    logging.info('  %s', prop)
    418  if options.auto_reconnect:
    419    _PushAndLaunchAdbReboot(device, options.target)
    420 
    421 
    422 def _UninstallIfMatch(device, pattern, app_to_keep):
    423  installed_packages = device.RunShellCommand(['pm', 'list', 'packages'])
    424  installed_system_packages = [
    425      pkg.split(':')[1] for pkg in device.RunShellCommand(['pm', 'list',
    426                                                           'packages', '-s'])]
    427  for package_output in installed_packages:
    428    package = package_output.split(":")[1]
    429    if pattern.match(package) and not package == app_to_keep:
    430      if not device.IsUserBuild() or package not in installed_system_packages:
    431        device.Uninstall(package)
    432 
    433 
    434 def _WipeUnderDirIfMatch(device, path, pattern):
    435  for filename in device.ListDirectory(path):
    436    if pattern.match(filename):
    437      _WipeFileOrDir(device, posixpath.join(path, filename))
    438 
    439 
    440 def _WipeFileOrDir(device, path):
    441  if device.PathExists(path):
    442    device.RunShellCommand(['rm', '-rf', path], check_return=True)
    443 
    444 
    445 def _PushAndLaunchAdbReboot(device, target):
    446  """Pushes and launches the adb_reboot binary on the device.
    447 
    448  Arguments:
    449    device: The DeviceUtils instance for the device to which the adb_reboot
    450            binary should be pushed.
    451    target: The build target (example, Debug or Release) which helps in
    452            locating the adb_reboot binary.
    453  """
    454  logging.info('Will push and launch adb_reboot on %s', str(device))
    455  # Kill if adb_reboot is already running.
    456  device.KillAll('adb_reboot', blocking=True, timeout=2, quiet=True)
    457  # Push adb_reboot
    458  logging.info('  Pushing adb_reboot ...')
    459  adb_reboot = os.path.join(host_paths.DIR_SOURCE_ROOT,
    460                            'out/%s/adb_reboot' % target)
    461  device.PushChangedFiles([(adb_reboot, '/data/local/tmp/')])
    462  # Launch adb_reboot
    463  logging.info('  Launching adb_reboot ...')
    464  device.RunShellCommand(
    465      ['/data/local/tmp/adb_reboot'],
    466      check_return=True)
    467 
    468 
    469 def _LaunchHostHeartbeat():
    470  # Kill if existing host_heartbeat
    471  KillHostHeartbeat()
    472  # Launch a new host_heartbeat
    473  logging.info('Spawning host heartbeat...')
    474  subprocess.Popen([os.path.join(host_paths.DIR_SOURCE_ROOT,
    475                                 'build/android/host_heartbeat.py')])
    476 
    477 def KillHostHeartbeat():
    478  ps = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE)
    479  stdout, _ = ps.communicate()
    480  matches = re.findall('\\n.*host_heartbeat.*', stdout)
    481  for match in matches:
    482    logging.info('An instance of host heart beart running... will kill')
    483    pid = re.findall(r'(\S+)', match)[1]
    484    subprocess.call(['kill', str(pid)])
    485 
    486 def main():
    487  # Recommended options on perf bots:
    488  # --disable-network
    489  #     TODO(tonyg): We eventually want network on. However, currently radios
    490  #     can cause perfbots to drain faster than they charge.
    491  # --min-battery-level 95
    492  #     Some perf bots run benchmarks with USB charging disabled which leads
    493  #     to gradual draining of the battery. We must wait for a full charge
    494  #     before starting a run in order to keep the devices online.
    495 
    496  parser = argparse.ArgumentParser(
    497      description='Provision Android devices with settings required for bots.')
    498  parser.add_argument('-d', '--device', metavar='SERIAL',
    499                      help='the serial number of the device to be provisioned'
    500                      ' (the default is to provision all devices attached)')
    501  parser.add_argument('--adb-path',
    502                      help='Absolute path to the adb binary to use.')
    503  parser.add_argument('--denylist-file', help='Device denylist JSON file.')
    504  parser.add_argument('--phase', action='append', choices=_PHASES.ALL,
    505                      dest='phases',
    506                      help='Phases of provisioning to run. '
    507                           '(If omitted, all phases will be run.)')
    508  parser.add_argument('--skip-wipe', action='store_true', default=False,
    509                      help="don't wipe device data during provisioning")
    510  parser.add_argument('--reboot-timeout', metavar='SECS', type=int,
    511                      help='when wiping the device, max number of seconds to'
    512                      ' wait after each reboot '
    513                      '(default: %s)' % _DEFAULT_TIMEOUTS.HELP_TEXT)
    514  parser.add_argument('--min-battery-level', type=int, metavar='NUM',
    515                      help='wait for the device to reach this minimum battery'
    516                      ' level before trying to continue')
    517  parser.add_argument('--disable-location', action='store_true',
    518                      help='disable Google location services on devices')
    519  parser.add_argument('--disable-mock-location', action='store_true',
    520                      default=False, help='Set ALLOW_MOCK_LOCATION to false')
    521  parser.add_argument('--disable-network', action='store_true',
    522                      help='disable network access on devices')
    523  parser.add_argument('--disable-java-debug', action='store_false',
    524                      dest='enable_java_debug', default=True,
    525                      help='disable Java property asserts and JNI checking')
    526  parser.add_argument('--disable-system-chrome', action='store_true',
    527                      help='Disable the system chrome from devices.')
    528  parser.add_argument('--remove-system-webview', action='store_true',
    529                      help='Remove the system webview from devices.')
    530  parser.add_argument('-t', '--target', default='Debug',
    531                      help='the build target (default: %(default)s)')
    532  parser.add_argument('-r', '--auto-reconnect', action='store_true',
    533                      help='push binary which will reboot the device on adb'
    534                      ' disconnections')
    535  parser.add_argument('--adb-key-files', type=str, nargs='+',
    536                      help='list of adb keys to push to device')
    537  parser.add_argument('-v', '--verbose', action='count', default=1,
    538                      help='Log more information.')
    539  parser.add_argument('--max-battery-temp', type=int, metavar='NUM',
    540                      help='Wait for the battery to have this temp or lower.')
    541  parser.add_argument('--output-device-denylist',
    542                      help='Json file to output the device denylist.')
    543  parser.add_argument('--chrome-specific-wipe', action='store_true',
    544                      help='only wipe chrome specific data during provisioning')
    545  parser.add_argument('--emulators', action='store_true',
    546                      help='provision only emulators and ignore usb devices')
    547  args = parser.parse_args()
    548  constants.SetBuildType(args.target)
    549 
    550  run_tests_helper.SetLogLevel(args.verbose)
    551 
    552  devil_chromium.Initialize(adb_path=args.adb_path)
    553 
    554  try:
    555    return ProvisionDevices(args)
    556  except (device_errors.DeviceUnreachableError, device_errors.NoDevicesError):
    557    logging.exception('Unable to provision local devices.')
    558    return exit_codes.INFRA
    559 
    560 
    561 if __name__ == '__main__':
    562  sys.exit(main())