• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env vpython3
2# Copyright 2017 The Chromium Authors
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6# Using colorama.Fore/Back/Style members
7# pylint: disable=no-member
8
9
10import argparse
11import collections
12import json
13import logging
14import os
15import posixpath
16import random
17import re
18import shlex
19import shutil
20import subprocess
21import sys
22import tempfile
23import textwrap
24import zipfile
25
26import adb_command_line
27import devil_chromium
28from devil import devil_env
29from devil.android import apk_helper
30from devil.android import device_errors
31from devil.android import device_utils
32from devil.android import flag_changer
33from devil.android.sdk import adb_wrapper
34from devil.android.sdk import build_tools
35from devil.android.sdk import intent
36from devil.android.sdk import version_codes
37from devil.utils import run_tests_helper
38
39_DIR_SOURCE_ROOT = os.path.normpath(
40    os.path.join(os.path.dirname(__file__), '..', '..'))
41_JAVA_HOME = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'jdk', 'current')
42
43with devil_env.SysPath(
44    os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'colorama', 'src')):
45  import colorama
46
47from incremental_install import installer
48from pylib import constants
49from pylib.symbols import deobfuscator
50from pylib.utils import simpleperf
51from pylib.utils import app_bundle_utils
52
53with devil_env.SysPath(
54    os.path.join(_DIR_SOURCE_ROOT, 'build', 'android', 'gyp')):
55  import bundletool
56
57BASE_MODULE = 'base'
58
59
60def _Colorize(text, style=''):
61  return (style
62      + text
63      + colorama.Style.RESET_ALL)
64
65
66def _InstallApk(devices, apk, install_dict):
67  def install(device):
68    if install_dict:
69      installer.Install(device, install_dict, apk=apk, permissions=[])
70    else:
71      device.Install(apk, permissions=[], allow_downgrade=True, reinstall=True)
72
73  logging.info('Installing %sincremental apk.', '' if install_dict else 'non-')
74  device_utils.DeviceUtils.parallel(devices).pMap(install)
75
76
77# A named tuple containing the information needed to convert a bundle into
78# an installable .apks archive.
79# Fields:
80#   bundle_path: Path to input bundle file.
81#   bundle_apk_path: Path to output bundle .apks archive file.
82#   aapt2_path: Path to aapt2 tool.
83#   keystore_path: Path to keystore file.
84#   keystore_password: Password for the keystore file.
85#   keystore_alias: Signing key name alias within the keystore file.
86#   system_image_locales: List of Chromium locales to include in system .apks.
87BundleGenerationInfo = collections.namedtuple(
88    'BundleGenerationInfo',
89    'bundle_path,bundle_apks_path,aapt2_path,keystore_path,keystore_password,'
90    'keystore_alias,system_image_locales')
91
92
93def _GenerateBundleApks(info,
94                        output_path=None,
95                        minimal=False,
96                        minimal_sdk_version=None,
97                        mode=None,
98                        optimize_for=None):
99  """Generate an .apks archive from a bundle on demand.
100
101  Args:
102    info: A BundleGenerationInfo instance.
103    output_path: Path of output .apks archive.
104    minimal: Create the minimal set of apks possible (english-only).
105    minimal_sdk_version: When minimal=True, use this sdkVersion.
106    mode: Build mode, either None, or one of app_bundle_utils.BUILD_APKS_MODES.
107    optimize_for: Override split config, either None, or one of
108      app_bundle_utils.OPTIMIZE_FOR_OPTIONS.
109  """
110  logging.info('Generating .apks file')
111  app_bundle_utils.GenerateBundleApks(
112      info.bundle_path,
113      # Store .apks file beside the .aab file by default so that it gets cached.
114      output_path or info.bundle_apks_path,
115      info.aapt2_path,
116      info.keystore_path,
117      info.keystore_password,
118      info.keystore_alias,
119      system_image_locales=info.system_image_locales,
120      mode=mode,
121      minimal=minimal,
122      minimal_sdk_version=minimal_sdk_version,
123      optimize_for=optimize_for)
124
125
126def _InstallBundle(devices, apk_helper_instance, modules, fake_modules):
127
128  def Install(device):
129    device.Install(apk_helper_instance,
130                   permissions=[],
131                   modules=modules,
132                   fake_modules=fake_modules,
133                   allow_downgrade=True,
134                   reinstall=True)
135
136  # Basic checks for |modules| and |fake_modules|.
137  # * |fake_modules| cannot include 'base'.
138  # * If |fake_modules| is given, ensure |modules| includes 'base'.
139  # * They must be disjoint (checked by device.Install).
140  modules_set = set(modules) if modules else set()
141  fake_modules_set = set(fake_modules) if fake_modules else set()
142  if BASE_MODULE in fake_modules_set:
143    raise Exception('\'-f {}\' is disallowed.'.format(BASE_MODULE))
144  if fake_modules_set and BASE_MODULE not in modules_set:
145    raise Exception(
146        '\'-f FAKE\' must be accompanied by \'-m {}\''.format(BASE_MODULE))
147
148  logging.info('Installing bundle.')
149  device_utils.DeviceUtils.parallel(devices).pMap(Install)
150
151
152def _UninstallApk(devices, install_dict, package_name):
153  def uninstall(device):
154    if install_dict:
155      installer.Uninstall(device, package_name)
156    else:
157      device.Uninstall(package_name)
158  device_utils.DeviceUtils.parallel(devices).pMap(uninstall)
159
160
161def _IsWebViewProvider(apk_helper_instance):
162  meta_data = apk_helper_instance.GetAllMetadata()
163  meta_data_keys = [pair[0] for pair in meta_data]
164  return 'com.android.webview.WebViewLibrary' in meta_data_keys
165
166
167def _SetWebViewProvider(devices, package_name):
168
169  def switch_provider(device):
170    if device.build_version_sdk < version_codes.NOUGAT:
171      logging.error('No need to switch provider on pre-Nougat devices (%s)',
172                    device.serial)
173    else:
174      device.SetWebViewImplementation(package_name)
175
176  device_utils.DeviceUtils.parallel(devices).pMap(switch_provider)
177
178
179def _NormalizeProcessName(debug_process_name, package_name):
180  if not debug_process_name:
181    debug_process_name = package_name
182  elif debug_process_name.startswith(':'):
183    debug_process_name = package_name + debug_process_name
184  elif '.' not in debug_process_name:
185    debug_process_name = package_name + ':' + debug_process_name
186  return debug_process_name
187
188
189def _ResolveActivity(device, package_name, category, action):
190  # E.g.:
191  # Activity Resolver Table:
192  #   Schemes:
193  #     http:
194  #       67e97c0 org.chromium.pkg/.MainActivityfilter c91d43e
195  #         Action: "android.intent.action.VIEW"
196  #         Category: "android.intent.category.DEFAULT"
197  #         Category: "android.intent.category.BROWSABLE"
198  #         Scheme: "http"
199  #         Scheme: "https"
200  #
201  #   Non-Data Actions:
202  #     android.intent.action.MAIN:
203  #       67e97c0 org.chromium.pkg/.MainActivity filter 4a34cf9
204  #         Action: "android.intent.action.MAIN"
205  #         Category: "android.intent.category.LAUNCHER"
206  lines = device.RunShellCommand(['dumpsys', 'package', package_name],
207                                 check_return=True)
208
209  # Extract the Activity Resolver Table: section.
210  start_idx = next((i for i, l in enumerate(lines)
211                    if l.startswith('Activity Resolver Table:')), None)
212  if start_idx is None:
213    if not device.IsApplicationInstalled(package_name):
214      raise Exception('Package not installed: ' + package_name)
215    raise Exception('No Activity Resolver Table in:\n' + '\n'.join(lines))
216  line_count = next(i for i, l in enumerate(lines[start_idx + 1:])
217                    if l and not l[0].isspace())
218  data = '\n'.join(lines[start_idx:start_idx + line_count])
219
220  # Split on each Activity entry.
221  entries = re.split(r'^        [0-9a-f]+ ', data, flags=re.MULTILINE)
222
223  def activity_name_from_entry(entry):
224    assert entry.startswith(package_name), 'Got: ' + entry
225    activity_name = entry[len(package_name) + 1:].split(' ', 1)[0]
226    if activity_name[0] == '.':
227      activity_name = package_name + activity_name
228    return activity_name
229
230  # Find the one with the text we want.
231  category_text = f'Category: "{category}"'
232  action_text = f'Action: "{action}"'
233  matched_entries = [
234      e for e in entries[1:] if category_text in e and action_text in e
235  ]
236
237  if not matched_entries:
238    raise Exception(f'Did not find {category_text}, {action_text} in\n{data}')
239  if len(matched_entries) > 1:
240    # When there are multiple matches, look for the one marked as default.
241    # Necessary for Monochrome, which also has MonochromeLauncherActivity.
242    default_entries = [
243        e for e in matched_entries if 'android.intent.category.DEFAULT' in e
244    ]
245    matched_entries = default_entries or matched_entries
246
247  # See if all matches point to the same activity.
248  activity_names = {activity_name_from_entry(e) for e in matched_entries}
249
250  if len(activity_names) > 1:
251    raise Exception('Found multiple launcher activities:\n * ' +
252                    '\n * '.join(sorted(activity_names)))
253  return next(iter(activity_names))
254
255
256def _LaunchUrl(devices,
257               package_name,
258               argv=None,
259               command_line_flags_file=None,
260               url=None,
261               wait_for_java_debugger=False,
262               debug_process_name=None,
263               nokill=None):
264  if argv and command_line_flags_file is None:
265    raise Exception('This apk does not support any flags.')
266
267  debug_process_name = _NormalizeProcessName(debug_process_name, package_name)
268
269  if url is None:
270    category = 'android.intent.category.LAUNCHER'
271    action = 'android.intent.action.MAIN'
272  else:
273    category = 'android.intent.category.BROWSABLE'
274    action = 'android.intent.action.VIEW'
275
276  def launch(device):
277    activity = _ResolveActivity(device, package_name, category, action)
278    # --persistent is required to have Settings.Global.DEBUG_APP be set, which
279    # we currently use to allow reading of flags. https://crbug.com/784947
280    if not nokill:
281      cmd = ['am', 'set-debug-app', '--persistent', debug_process_name]
282      if wait_for_java_debugger:
283        cmd[-1:-1] = ['-w']
284      # Ignore error since it will fail if apk is not debuggable.
285      device.RunShellCommand(cmd, check_return=False)
286
287      # The flags are first updated with input args.
288      if command_line_flags_file:
289        changer = flag_changer.FlagChanger(device, command_line_flags_file)
290        flags = []
291        if argv:
292          adb_command_line.CheckBuildTypeSupportsFlags(device,
293                                                       command_line_flags_file)
294          flags = shlex.split(argv)
295        try:
296          changer.ReplaceFlags(flags)
297        except device_errors.AdbShellCommandFailedError:
298          logging.exception('Failed to set flags')
299
300    launch_intent = intent.Intent(action=action,
301                                  activity=activity,
302                                  data=url,
303                                  package=package_name)
304    logging.info('Sending launch intent for %s', activity)
305    device.StartActivity(launch_intent)
306
307  device_utils.DeviceUtils.parallel(devices).pMap(launch)
308  if wait_for_java_debugger:
309    print('Waiting for debugger to attach to process: ' +
310          _Colorize(debug_process_name, colorama.Fore.YELLOW))
311
312
313def _ChangeFlags(devices, argv, command_line_flags_file):
314  if argv is None:
315    _DisplayArgs(devices, command_line_flags_file)
316  else:
317    flags = shlex.split(argv)
318    def update(device):
319      adb_command_line.CheckBuildTypeSupportsFlags(device,
320                                                   command_line_flags_file)
321      changer = flag_changer.FlagChanger(device, command_line_flags_file)
322      changer.ReplaceFlags(flags)
323    device_utils.DeviceUtils.parallel(devices).pMap(update)
324
325
326def _TargetCpuToTargetArch(target_cpu):
327  if target_cpu == 'x64':
328    return 'x86_64'
329  if target_cpu == 'mipsel':
330    return 'mips'
331  return target_cpu
332
333
334def _RunGdb(device, package_name, debug_process_name, pid, output_directory,
335            target_cpu, port, ide, verbose):
336  if not pid:
337    debug_process_name = _NormalizeProcessName(debug_process_name, package_name)
338    pid = device.GetApplicationPids(debug_process_name, at_most_one=True)
339  if not pid:
340    # Attaching gdb makes the app run so slow that it takes *minutes* to start
341    # up (as of 2018). Better to just fail than to start & attach.
342    raise Exception('App not running.')
343
344  gdb_script_path = os.path.dirname(__file__) + '/adb_gdb'
345  cmd = [
346      gdb_script_path,
347      '--package-name=%s' % package_name,
348      '--output-directory=%s' % output_directory,
349      '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
350      '--device=%s' % device.serial,
351      '--pid=%s' % pid,
352      '--port=%d' % port,
353  ]
354  if ide:
355    cmd.append('--ide')
356  # Enable verbose output of adb_gdb if it's set for this script.
357  if verbose:
358    cmd.append('--verbose')
359  if target_cpu:
360    cmd.append('--target-arch=%s' % _TargetCpuToTargetArch(target_cpu))
361  logging.warning('Running: %s', ' '.join(shlex.quote(x) for x in cmd))
362  print(_Colorize('All subsequent output is from adb_gdb script.',
363                  colorama.Fore.YELLOW))
364  os.execv(gdb_script_path, cmd)
365
366
367def _RunLldb(device,
368             package_name,
369             debug_process_name,
370             pid,
371             output_directory,
372             port,
373             target_cpu=None,
374             ndk_dir=None,
375             lldb_server=None,
376             lldb=None,
377             verbose=None):
378  if not pid:
379    debug_process_name = _NormalizeProcessName(debug_process_name, package_name)
380    pid = device.GetApplicationPids(debug_process_name, at_most_one=True)
381  if not pid:
382    # Attaching lldb makes the app run so slow that it takes *minutes* to start
383    # up (as of 2018). Better to just fail than to start & attach.
384    raise Exception('App not running.')
385
386  lldb_script_path = os.path.dirname(__file__) + '/connect_lldb.sh'
387  cmd = [
388      lldb_script_path,
389      '--package-name=%s' % package_name,
390      '--output-directory=%s' % output_directory,
391      '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(),
392      '--device=%s' % device.serial,
393      '--pid=%s' % pid,
394      '--port=%d' % port,
395  ]
396  # Enable verbose output of connect_lldb.sh if it's set for this script.
397  if verbose:
398    cmd.append('--verbose')
399  if target_cpu:
400    cmd.append('--target-arch=%s' % _TargetCpuToTargetArch(target_cpu))
401  if ndk_dir:
402    cmd.append('--ndk-dir=%s' % ndk_dir)
403  if lldb_server:
404    cmd.append('--lldb-server=%s' % lldb_server)
405  if lldb:
406    cmd.append('--lldb=%s' % lldb)
407  logging.warning('Running: %s', ' '.join(shlex.quote(x) for x in cmd))
408  print(
409      _Colorize('All subsequent output is from connect_lldb.sh script.',
410                colorama.Fore.YELLOW))
411  os.execv(lldb_script_path, cmd)
412
413
414def _PrintPerDeviceOutput(devices, results, single_line=False):
415  for d, result in zip(devices, results):
416    if not single_line and d is not devices[0]:
417      sys.stdout.write('\n')
418    sys.stdout.write(
419          _Colorize('{} ({}):'.format(d, d.build_description),
420                    colorama.Fore.YELLOW))
421    sys.stdout.write(' ' if single_line else '\n')
422    yield result
423
424
425def _RunMemUsage(devices, package_name, query_app=False):
426  cmd_args = ['dumpsys', 'meminfo']
427  if not query_app:
428    cmd_args.append('--local')
429
430  def mem_usage_helper(d):
431    ret = []
432    for process in sorted(_GetPackageProcesses(d, package_name)):
433      meminfo = d.RunShellCommand(cmd_args + [str(process.pid)])
434      ret.append((process.name, '\n'.join(meminfo)))
435    return ret
436
437  parallel_devices = device_utils.DeviceUtils.parallel(devices)
438  all_results = parallel_devices.pMap(mem_usage_helper).pGet(None)
439  for result in _PrintPerDeviceOutput(devices, all_results):
440    if not result:
441      print('No processes found.')
442    else:
443      for name, usage in sorted(result):
444        print(_Colorize('==== Output of "dumpsys meminfo %s" ====' % name,
445                        colorama.Fore.GREEN))
446        print(usage)
447
448
449def _DuHelper(device, path_spec, run_as=None):
450  """Runs "du -s -k |path_spec|" on |device| and returns parsed result.
451
452  Args:
453    device: A DeviceUtils instance.
454    path_spec: The list of paths to run du on. May contain shell expansions
455        (will not be escaped).
456    run_as: Package name to run as, or None to run as shell user. If not None
457        and app is not android:debuggable (run-as fails), then command will be
458        run as root.
459
460  Returns:
461    A dict of path->size in KiB containing all paths in |path_spec| that exist
462    on device. Paths that do not exist are silently ignored.
463  """
464  # Example output for: du -s -k /data/data/org.chromium.chrome/{*,.*}
465  # 144     /data/data/org.chromium.chrome/cache
466  # 8       /data/data/org.chromium.chrome/files
467  # <snip>
468  # du: .*: No such file or directory
469
470  # The -d flag works differently across android version, so use -s instead.
471  # Without the explicit 2>&1, stderr and stdout get combined at random :(.
472  cmd_str = 'du -s -k ' + path_spec + ' 2>&1'
473  lines = device.RunShellCommand(cmd_str, run_as=run_as, shell=True,
474                                 check_return=False)
475  output = '\n'.join(lines)
476  # run-as: Package 'com.android.chrome' is not debuggable
477  if output.startswith('run-as:'):
478    # check_return=False needed for when some paths in path_spec do not exist.
479    lines = device.RunShellCommand(cmd_str, as_root=True, shell=True,
480                                   check_return=False)
481  ret = {}
482  try:
483    for line in lines:
484      # du: .*: No such file or directory
485      if line.startswith('du:'):
486        continue
487      size, subpath = line.split(None, 1)
488      ret[subpath] = int(size)
489    return ret
490  except ValueError:
491    logging.error('du command was: %s', cmd_str)
492    logging.error('Failed to parse du output:\n%s', output)
493    raise
494
495
496def _RunDiskUsage(devices, package_name):
497  # Measuring dex size is a bit complicated:
498  # https://source.android.com/devices/tech/dalvik/jit-compiler
499  #
500  # For KitKat and below:
501  #   dumpsys package contains:
502  #     dataDir=/data/data/org.chromium.chrome
503  #     codePath=/data/app/org.chromium.chrome-1.apk
504  #     resourcePath=/data/app/org.chromium.chrome-1.apk
505  #     nativeLibraryPath=/data/app-lib/org.chromium.chrome-1
506  #   To measure odex:
507  #     ls -l /data/dalvik-cache/data@app@org.chromium.chrome-1.apk@classes.dex
508  #
509  # For Android L and M (and maybe for N+ system apps):
510  #   dumpsys package contains:
511  #     codePath=/data/app/org.chromium.chrome-1
512  #     resourcePath=/data/app/org.chromium.chrome-1
513  #     legacyNativeLibraryDir=/data/app/org.chromium.chrome-1/lib
514  #   To measure odex:
515  #     # Option 1:
516  #  /data/dalvik-cache/arm/data@app@org.chromium.chrome-1@base.apk@classes.dex
517  #  /data/dalvik-cache/arm/data@app@org.chromium.chrome-1@base.apk@classes.vdex
518  #     ls -l /data/dalvik-cache/profiles/org.chromium.chrome
519  #         (these profiles all appear to be 0 bytes)
520  #     # Option 2:
521  #     ls -l /data/app/org.chromium.chrome-1/oat/arm/base.odex
522  #
523  # For Android N+:
524  #   dumpsys package contains:
525  #     dataDir=/data/user/0/org.chromium.chrome
526  #     codePath=/data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==
527  #     resourcePath=/data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==
528  #     legacyNativeLibraryDir=/data/app/org.chromium.chrome-GUID/lib
529  #     Instruction Set: arm
530  #       path: /data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==/base.apk
531  #       status: /data/.../oat/arm/base.odex[status=kOatUpToDate, compilation_f
532  #       ilter=quicken]
533  #     Instruction Set: arm64
534  #       path: /data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==/base.apk
535  #       status: /data/.../oat/arm64/base.odex[status=..., compilation_filter=q
536  #       uicken]
537  #   To measure odex:
538  #     ls -l /data/app/.../oat/arm/base.odex
539  #     ls -l /data/app/.../oat/arm/base.vdex (optional)
540  #   To measure the correct odex size:
541  #     cmd package compile -m speed org.chromium.chrome  # For webview
542  #     cmd package compile -m speed-profile org.chromium.chrome  # For others
543  def disk_usage_helper(d):
544    package_output = '\n'.join(d.RunShellCommand(
545        ['dumpsys', 'package', package_name], check_return=True))
546    # Does not return error when apk is not installed.
547    if not package_output or 'Unable to find package:' in package_output:
548      return None
549
550    # Ignore system apks that have updates installed.
551    package_output = re.sub(r'Hidden system packages:.*?^\b', '',
552                            package_output, flags=re.S | re.M)
553
554    try:
555      data_dir = re.search(r'dataDir=(.*)', package_output).group(1)
556      code_path = re.search(r'codePath=(.*)', package_output).group(1)
557      lib_path = re.search(r'(?:legacyN|n)ativeLibrary(?:Dir|Path)=(.*)',
558                           package_output).group(1)
559    except AttributeError as e:
560      raise Exception('Error parsing dumpsys output: ' + package_output) from e
561
562    if code_path.startswith('/system'):
563      logging.warning('Measurement of system image apks can be innacurate')
564
565    compilation_filters = set()
566    # Match "compilation_filter=value", where a line break can occur at any spot
567    # (refer to examples above).
568    awful_wrapping = r'\s*'.join('compilation_filter=')
569    for m in re.finditer(awful_wrapping + r'([\s\S]+?)[\],]', package_output):
570      compilation_filters.add(re.sub(r'\s+', '', m.group(1)))
571    # Starting Android Q, output looks like:
572    #  arm: [status=speed-profile] [reason=install]
573    for m in re.finditer(r'\[status=(.+?)\]', package_output):
574      compilation_filters.add(m.group(1))
575    compilation_filter = ','.join(sorted(compilation_filters))
576
577    data_dir_sizes = _DuHelper(d, '%s/{*,.*}' % data_dir, run_as=package_name)
578    # Measure code_cache separately since it can be large.
579    code_cache_sizes = {}
580    code_cache_dir = next(
581        (k for k in data_dir_sizes if k.endswith('/code_cache')), None)
582    if code_cache_dir:
583      data_dir_sizes.pop(code_cache_dir)
584      code_cache_sizes = _DuHelper(d, '%s/{*,.*}' % code_cache_dir,
585                                   run_as=package_name)
586
587    apk_path_spec = code_path
588    if not apk_path_spec.endswith('.apk'):
589      apk_path_spec += '/*.apk'
590    apk_sizes = _DuHelper(d, apk_path_spec)
591    if lib_path.endswith('/lib'):
592      # Shows architecture subdirectory.
593      lib_sizes = _DuHelper(d, '%s/{*,.*}' % lib_path)
594    else:
595      lib_sizes = _DuHelper(d, lib_path)
596
597    # Look at all possible locations for odex files.
598    odex_paths = []
599    for apk_path in apk_sizes:
600      mangled_apk_path = apk_path[1:].replace('/', '@')
601      apk_basename = posixpath.basename(apk_path)[:-4]
602      for ext in ('dex', 'odex', 'vdex', 'art'):
603        # Easier to check all architectures than to determine active ones.
604        for arch in ('arm', 'arm64', 'x86', 'x86_64', 'mips', 'mips64'):
605          odex_paths.append(
606              '%s/oat/%s/%s.%s' % (code_path, arch, apk_basename, ext))
607          # No app could possibly have more than 6 dex files.
608          for suffix in ('', '2', '3', '4', '5'):
609            odex_paths.append('/data/dalvik-cache/%s/%s@classes%s.%s' % (
610                arch, mangled_apk_path, suffix, ext))
611            # This path does not have |arch|, so don't repeat it for every arch.
612            if arch == 'arm':
613              odex_paths.append('/data/dalvik-cache/%s@classes%s.dex' % (
614                  mangled_apk_path, suffix))
615
616    odex_sizes = _DuHelper(d, ' '.join(shlex.quote(p) for p in odex_paths))
617
618    return (data_dir_sizes, code_cache_sizes, apk_sizes, lib_sizes, odex_sizes,
619            compilation_filter)
620
621  def print_sizes(desc, sizes):
622    print('%s: %d KiB' % (desc, sum(sizes.values())))
623    for path, size in sorted(sizes.items()):
624      print('    %s: %s KiB' % (path, size))
625
626  parallel_devices = device_utils.DeviceUtils.parallel(devices)
627  all_results = parallel_devices.pMap(disk_usage_helper).pGet(None)
628  for result in _PrintPerDeviceOutput(devices, all_results):
629    if not result:
630      print('APK is not installed.')
631      continue
632
633    (data_dir_sizes, code_cache_sizes, apk_sizes, lib_sizes, odex_sizes,
634     compilation_filter) = result
635    total = sum(sum(sizes.values()) for sizes in result[:-1])
636
637    print_sizes('Apk', apk_sizes)
638    print_sizes('App Data (non-code cache)', data_dir_sizes)
639    print_sizes('App Data (code cache)', code_cache_sizes)
640    print_sizes('Native Libs', lib_sizes)
641    show_warning = compilation_filter and 'speed' not in compilation_filter
642    compilation_filter = compilation_filter or 'n/a'
643    print_sizes('odex (compilation_filter=%s)' % compilation_filter, odex_sizes)
644    if show_warning:
645      logging.warning('For a more realistic odex size, run:')
646      logging.warning('    %s compile-dex [speed|speed-profile]', sys.argv[0])
647    print('Total: %s KiB (%.1f MiB)' % (total, total / 1024.0))
648
649
650class _LogcatProcessor:
651  ParsedLine = collections.namedtuple(
652      'ParsedLine',
653      ['date', 'invokation_time', 'pid', 'tid', 'priority', 'tag', 'message'])
654
655  class NativeStackSymbolizer:
656    """Buffers lines from native stacks and symbolizes them when done."""
657    # E.g.: #06 pc 0x0000d519 /apex/com.android.runtime/lib/libart.so
658    # E.g.: #01 pc 00180c8d  /data/data/.../lib/libbase.cr.so
659    _STACK_PATTERN = re.compile(r'\s*#\d+\s+(?:pc )?(0x)?[0-9a-f]{8,16}\s')
660
661    def __init__(self, stack_script_context, print_func):
662      # To symbolize native stacks, we need to pass all lines at once.
663      self._stack_script_context = stack_script_context
664      self._print_func = print_func
665      self._crash_lines_buffer = None
666
667    def _FlushLines(self):
668      """Prints queued lines after sending them through stack.py."""
669      if self._crash_lines_buffer is None:
670        return
671
672      crash_lines = self._crash_lines_buffer
673      self._crash_lines_buffer = None
674      with tempfile.NamedTemporaryFile(mode='w') as f:
675        f.writelines(x[0].message + '\n' for x in crash_lines)
676        f.flush()
677        proc = self._stack_script_context.Popen(
678            input_file=f.name, stdout=subprocess.PIPE)
679        lines = proc.communicate()[0].splitlines()
680
681      for i, line in enumerate(lines):
682        parsed_line, dim = crash_lines[min(i, len(crash_lines) - 1)]
683        d = parsed_line._asdict()
684        d['message'] = line
685        parsed_line = _LogcatProcessor.ParsedLine(**d)
686        self._print_func(parsed_line, dim)
687
688    def AddLine(self, parsed_line, dim):
689      # Assume all lines from DEBUG are stacks.
690      # Also look for "stack-looking" lines to catch manual stack prints.
691      # It's important to not buffer non-stack lines because stack.py does not
692      # pass them through.
693      is_crash_line = parsed_line.tag == 'DEBUG' or (self._STACK_PATTERN.match(
694          parsed_line.message))
695
696      if is_crash_line:
697        if self._crash_lines_buffer is None:
698          self._crash_lines_buffer = []
699        self._crash_lines_buffer.append((parsed_line, dim))
700        return
701
702      self._FlushLines()
703
704      self._print_func(parsed_line, dim)
705
706
707  # Logcat tags for messages that are generally relevant but are not from PIDs
708  # associated with the apk.
709  _ALLOWLISTED_TAGS = {
710      'ActivityManager',  # Shows activity lifecycle messages.
711      'ActivityTaskManager',  # More activity lifecycle messages.
712      'AndroidRuntime',  # Java crash dumps
713      'AppZygoteInit',  # Android's native application zygote support.
714      'DEBUG',  # Native crash dump.
715  }
716
717  # Matches messages only on pre-L (Dalvik) that are spammy and unimportant.
718  _DALVIK_IGNORE_PATTERN = re.compile('|'.join([
719      r'^Added shared lib',
720      r'^Could not find ',
721      r'^DexOpt:',
722      r'^GC_',
723      r'^Late-enabling CheckJNI',
724      r'^Link of class',
725      r'^No JNI_OnLoad found in',
726      r'^Trying to load lib',
727      r'^Unable to resolve superclass',
728      r'^VFY:',
729      r'^WAIT_',
730  ]))
731
732  def __init__(self,
733               device,
734               package_name,
735               stack_script_context,
736               deobfuscate=None,
737               verbose=False,
738               exit_on_match=None,
739               extra_package_names=None):
740    self._device = device
741    self._package_name = package_name
742    self._extra_package_names = extra_package_names or []
743    self._verbose = verbose
744    self._deobfuscator = deobfuscate
745    if exit_on_match is not None:
746      self._exit_on_match = re.compile(exit_on_match)
747    else:
748      self._exit_on_match = None
749    self._found_exit_match = False
750    self._native_stack_symbolizer = _LogcatProcessor.NativeStackSymbolizer(
751        stack_script_context, self._PrintParsedLine)
752    # Process ID for the app's main process (with no :name suffix).
753    self._primary_pid = None
754    # Set of all Process IDs that belong to the app.
755    self._my_pids = set()
756    # Set of all Process IDs that we've parsed at some point.
757    self._seen_pids = set()
758    # Start proc 22953:com.google.chromeremotedesktop/
759    self._pid_pattern = re.compile(r'Start proc (\d+):{}/'.format(package_name))
760    # START u0 {act=android.intent.action.MAIN \
761    # cat=[android.intent.category.LAUNCHER] \
762    # flg=0x10000000 pkg=com.google.chromeremotedesktop} from uid 2000
763    self._start_pattern = re.compile(r'START .*(?:cmp|pkg)=' + package_name)
764
765    self.nonce = 'Chromium apk_operations.py nonce={}'.format(random.random())
766    # Holds lines buffered on start-up, before we find our nonce message.
767    self._initial_buffered_lines = []
768    self._UpdateMyPids()
769    # Give preference to PID reported by "ps" over those found from
770    # _start_pattern. There can be multiple "Start proc" messages from prior
771    # runs of the app.
772    self._found_initial_pid = self._primary_pid is not None
773    # Retrieve any additional patterns that are relevant for the User.
774    self._user_defined_highlight = None
775    user_regex = os.environ.get('CHROMIUM_LOGCAT_HIGHLIGHT')
776    if user_regex:
777      self._user_defined_highlight = re.compile(user_regex)
778      if not self._user_defined_highlight:
779        print(_Colorize(
780            'Rejecting invalid regular expression: {}'.format(user_regex),
781            colorama.Fore.RED + colorama.Style.BRIGHT))
782
783  def _UpdateMyPids(self):
784    # We intentionally do not clear self._my_pids to make sure that the
785    # ProcessLine method below also includes lines from processes which may
786    # have already exited.
787    self._primary_pid = None
788    for package_name in [self._package_name] + self._extra_package_names:
789      for process in _GetPackageProcesses(self._device, package_name):
790        # We take only the first "main" process found in order to account for
791        # possibly forked() processes.
792        if ':' not in process.name and self._primary_pid is None:
793          self._primary_pid = process.pid
794        self._my_pids.add(process.pid)
795
796  def _GetPidStyle(self, pid, dim=False):
797    if pid == self._primary_pid:
798      return colorama.Fore.WHITE
799    if pid in self._my_pids:
800      # TODO(wnwen): Use one separate persistent color per process, pop LRU
801      return colorama.Fore.YELLOW
802    if dim:
803      return colorama.Style.DIM
804    return ''
805
806  def _GetPriorityStyle(self, priority, dim=False):
807    # pylint:disable=no-self-use
808    if dim:
809      return ''
810    style = colorama.Fore.BLACK
811    if priority in ('E', 'F'):
812      style += colorama.Back.RED
813    elif priority == 'W':
814      style += colorama.Back.YELLOW
815    elif priority == 'I':
816      style += colorama.Back.GREEN
817    elif priority == 'D':
818      style += colorama.Back.BLUE
819    return style
820
821  def _ParseLine(self, line):
822    tokens = line.split(None, 6)
823
824    def consume_token_or_default(default):
825      return tokens.pop(0) if len(tokens) > 0 else default
826
827    def consume_integer_token_or_default(default):
828      if len(tokens) == 0:
829        return default
830
831      try:
832        return int(tokens.pop(0))
833      except ValueError:
834        return default
835
836    date = consume_token_or_default('')
837    invokation_time = consume_token_or_default('')
838    pid = consume_integer_token_or_default(-1)
839    tid = consume_integer_token_or_default(-1)
840    priority = consume_token_or_default('')
841    tag = consume_token_or_default('')
842    original_message = consume_token_or_default('')
843
844    # Example:
845    #   09-19 06:35:51.113  9060  9154 W GCoreFlp: No location...
846    #   09-19 06:01:26.174  9060 10617 I Auth    : [ReflectiveChannelBinder]...
847    # Parsing "GCoreFlp:" vs "Auth    :", we only want tag to contain the word,
848    # and we don't want to keep the colon for the message.
849    if tag and tag[-1] == ':':
850      tag = tag[:-1]
851    elif len(original_message) > 2:
852      original_message = original_message[2:]
853    return self.ParsedLine(
854        date, invokation_time, pid, tid, priority, tag, original_message)
855
856  def _PrintParsedLine(self, parsed_line, dim=False):
857    if self._exit_on_match and self._exit_on_match.search(parsed_line.message):
858      self._found_exit_match = True
859
860    tid_style = colorama.Style.NORMAL
861    user_match = self._user_defined_highlight and (
862        re.search(self._user_defined_highlight, parsed_line.tag)
863        or re.search(self._user_defined_highlight, parsed_line.message))
864
865    # Make the main thread bright.
866    if not dim and parsed_line.pid == parsed_line.tid:
867      tid_style = colorama.Style.BRIGHT
868    pid_style = self._GetPidStyle(parsed_line.pid, dim)
869    msg_style = pid_style if not user_match else (colorama.Fore.GREEN +
870                                                  colorama.Style.BRIGHT)
871    # We have to pad before adding color as that changes the width of the tag.
872    pid_str = _Colorize('{:5}'.format(parsed_line.pid), pid_style)
873    tid_str = _Colorize('{:5}'.format(parsed_line.tid), tid_style)
874    tag = _Colorize('{:8}'.format(parsed_line.tag),
875                    pid_style + ('' if dim else colorama.Style.BRIGHT))
876    priority = _Colorize(parsed_line.priority,
877                         self._GetPriorityStyle(parsed_line.priority))
878    messages = [parsed_line.message]
879    if self._deobfuscator:
880      messages = self._deobfuscator.TransformLines(messages)
881    for message in messages:
882      message = _Colorize(message, msg_style)
883      sys.stdout.write('{} {} {} {} {} {}: {}\n'.format(
884          parsed_line.date, parsed_line.invokation_time, pid_str, tid_str,
885          priority, tag, message))
886
887  def _TriggerNonceFound(self):
888    # Once the nonce is hit, we have confidence that we know which lines
889    # belong to the current run of the app. Process all of the buffered lines.
890    if self._primary_pid:
891      for args in self._initial_buffered_lines:
892        self._native_stack_symbolizer.AddLine(*args)
893    self._initial_buffered_lines = None
894    self.nonce = None
895
896  def FoundExitMatch(self):
897    return self._found_exit_match
898
899  def ProcessLine(self, line):
900    if not line or line.startswith('------'):
901      return
902
903    if self.nonce and self.nonce in line:
904      self._TriggerNonceFound()
905
906    nonce_found = self.nonce is None
907
908    log = self._ParseLine(line)
909    if log.pid not in self._seen_pids:
910      self._seen_pids.add(log.pid)
911      if nonce_found:
912        # Update list of owned PIDs each time a new PID is encountered.
913        self._UpdateMyPids()
914
915    # Search for "Start proc $pid:$package_name/" message.
916    if not nonce_found:
917      # Capture logs before the nonce. Start with the most recent "am start".
918      if self._start_pattern.match(log.message):
919        self._initial_buffered_lines = []
920
921      # If we didn't find the PID via "ps", then extract it from log messages.
922      # This will happen if the app crashes too quickly.
923      if not self._found_initial_pid:
924        m = self._pid_pattern.match(log.message)
925        if m:
926          # Find the most recent "Start proc" line before the nonce.
927          # Track only the primary pid in this mode.
928          # The main use-case is to find app logs when no current PIDs exist.
929          # E.g.: When the app crashes on launch.
930          self._primary_pid = m.group(1)
931          self._my_pids.clear()
932          self._my_pids.add(m.group(1))
933
934    owned_pid = log.pid in self._my_pids
935    if owned_pid and not self._verbose and log.tag == 'dalvikvm':
936      if self._DALVIK_IGNORE_PATTERN.match(log.message):
937        return
938
939    if owned_pid or self._verbose or (log.priority == 'F' or  # Java crash dump
940                                      log.tag in self._ALLOWLISTED_TAGS):
941      if nonce_found:
942        self._native_stack_symbolizer.AddLine(log, not owned_pid)
943      else:
944        self._initial_buffered_lines.append((log, not owned_pid))
945
946
947def _RunLogcat(device,
948               package_name,
949               stack_script_context,
950               deobfuscate,
951               verbose,
952               exit_on_match=None,
953               extra_package_names=None):
954  logcat_processor = _LogcatProcessor(device,
955                                      package_name,
956                                      stack_script_context,
957                                      deobfuscate,
958                                      verbose,
959                                      exit_on_match=exit_on_match,
960                                      extra_package_names=extra_package_names)
961  device.RunShellCommand(['log', logcat_processor.nonce])
962  for line in device.adb.Logcat(logcat_format='threadtime'):
963    try:
964      logcat_processor.ProcessLine(line)
965      if logcat_processor.FoundExitMatch():
966        return
967    except:
968      sys.stderr.write('Failed to process line: ' + line + '\n')
969      # Skip stack trace for the common case of the adb server being
970      # restarted.
971      if 'unexpected EOF' in line:
972        sys.exit(1)
973      raise
974
975
976def _GetPackageProcesses(device, package_name):
977  my_names = (package_name, package_name + '_zygote')
978  return [
979      p for p in device.ListProcesses(package_name)
980      if p.name in my_names or p.name.startswith(package_name + ':')
981  ]
982
983
984def _RunPs(devices, package_name):
985  parallel_devices = device_utils.DeviceUtils.parallel(devices)
986  all_processes = parallel_devices.pMap(
987      lambda d: _GetPackageProcesses(d, package_name)).pGet(None)
988  for processes in _PrintPerDeviceOutput(devices, all_processes):
989    if not processes:
990      print('No processes found.')
991    else:
992      proc_map = collections.defaultdict(list)
993      for p in processes:
994        proc_map[p.name].append(str(p.pid))
995      for name, pids in sorted(proc_map.items()):
996        print(name, ','.join(pids))
997
998
999def _RunShell(devices, package_name, cmd):
1000  if cmd:
1001    parallel_devices = device_utils.DeviceUtils.parallel(devices)
1002    outputs = parallel_devices.RunShellCommand(
1003        cmd, run_as=package_name).pGet(None)
1004    for output in _PrintPerDeviceOutput(devices, outputs):
1005      for line in output:
1006        print(line)
1007  else:
1008    adb_path = adb_wrapper.AdbWrapper.GetAdbPath()
1009    cmd = [adb_path, '-s', devices[0].serial, 'shell']
1010    # Pre-N devices do not support -t flag.
1011    if devices[0].build_version_sdk >= version_codes.NOUGAT:
1012      cmd += ['-t', 'run-as', package_name]
1013    else:
1014      print('Upon entering the shell, run:')
1015      print('run-as', package_name)
1016      print()
1017    os.execv(adb_path, cmd)
1018
1019
1020def _RunCompileDex(devices, package_name, compilation_filter):
1021  cmd = ['cmd', 'package', 'compile', '-f', '-m', compilation_filter,
1022         package_name]
1023  parallel_devices = device_utils.DeviceUtils.parallel(devices)
1024  outputs = parallel_devices.RunShellCommand(cmd, timeout=120).pGet(None)
1025  for output in _PrintPerDeviceOutput(devices, outputs):
1026    for line in output:
1027      print(line)
1028
1029
1030def _RunProfile(device, package_name, host_build_directory, pprof_out_path,
1031                process_specifier, thread_specifier, events, extra_args):
1032  simpleperf.PrepareDevice(device)
1033  device_simpleperf_path = simpleperf.InstallSimpleperf(device, package_name)
1034  with tempfile.NamedTemporaryFile() as fh:
1035    host_simpleperf_out_path = fh.name
1036
1037    with simpleperf.RunSimpleperf(device, device_simpleperf_path, package_name,
1038                                  process_specifier, thread_specifier,
1039                                  events, extra_args, host_simpleperf_out_path):
1040      sys.stdout.write('Profiler is running; press Enter to stop...\n')
1041      sys.stdin.read(1)
1042      sys.stdout.write('Post-processing data...\n')
1043
1044    simpleperf.ConvertSimpleperfToPprof(host_simpleperf_out_path,
1045                                        host_build_directory, pprof_out_path)
1046    print(textwrap.dedent("""
1047        Profile data written to %(s)s.
1048
1049        To view profile as a call graph in browser:
1050          pprof -web %(s)s
1051
1052        To print the hottest methods:
1053          pprof -top %(s)s
1054
1055        pprof has many useful customization options; `pprof --help` for details.
1056        """ % {'s': pprof_out_path}))
1057
1058
1059class _StackScriptContext:
1060  """Maintains temporary files needed by stack.py."""
1061
1062  def __init__(self,
1063               output_directory,
1064               apk_path,
1065               bundle_generation_info,
1066               quiet=False):
1067    self._output_directory = output_directory
1068    self._apk_path = apk_path
1069    self._bundle_generation_info = bundle_generation_info
1070    self._staging_dir = None
1071    self._quiet = quiet
1072
1073  def _CreateStaging(self):
1074    # In many cases, stack decoding requires APKs to map trace lines to native
1075    # libraries. Create a temporary directory, and either unpack a bundle's
1076    # APKS into it, or simply symlink the standalone APK into it. This
1077    # provides an unambiguous set of APK files for the stack decoding process
1078    # to inspect.
1079    logging.debug('Creating stack staging directory')
1080    self._staging_dir = tempfile.mkdtemp()
1081    bundle_generation_info = self._bundle_generation_info
1082
1083    if bundle_generation_info:
1084      # TODO(wnwen): Use apk_helper instead.
1085      _GenerateBundleApks(bundle_generation_info)
1086      logging.debug('Extracting .apks file')
1087      with zipfile.ZipFile(bundle_generation_info.bundle_apks_path, 'r') as z:
1088        files_to_extract = [
1089            f for f in z.namelist() if f.endswith('-master.apk')
1090        ]
1091        z.extractall(self._staging_dir, files_to_extract)
1092    elif self._apk_path:
1093      # Otherwise an incremental APK and an empty apks directory is correct.
1094      output = os.path.join(self._staging_dir, os.path.basename(self._apk_path))
1095      os.symlink(self._apk_path, output)
1096
1097  def Close(self):
1098    if self._staging_dir:
1099      logging.debug('Clearing stack staging directory')
1100      shutil.rmtree(self._staging_dir)
1101      self._staging_dir = None
1102
1103  def Popen(self, input_file=None, **kwargs):
1104    if self._staging_dir is None:
1105      self._CreateStaging()
1106    stack_script = os.path.join(
1107        constants.host_paths.ANDROID_PLATFORM_DEVELOPMENT_SCRIPTS_PATH,
1108        'stack.py')
1109    cmd = [
1110        stack_script, '--output-directory', self._output_directory,
1111        '--apks-directory', self._staging_dir
1112    ]
1113    if self._quiet:
1114      cmd.append('--quiet')
1115    if input_file:
1116      cmd.append(input_file)
1117    logging.info('Running: %s', shlex.join(cmd))
1118    return subprocess.Popen(cmd, universal_newlines=True, **kwargs)
1119
1120
1121def _GenerateAvailableDevicesMessage(devices):
1122  devices_obj = device_utils.DeviceUtils.parallel(devices)
1123  descriptions = devices_obj.pMap(lambda d: d.build_description).pGet(None)
1124  msg = 'Available devices:\n'
1125  for d, desc in zip(devices, descriptions):
1126    msg += '  %s (%s)\n' % (d, desc)
1127  return msg
1128
1129
1130# TODO(agrieve):add "--all" in the MultipleDevicesError message and use it here.
1131def _GenerateMissingAllFlagMessage(devices):
1132  return ('More than one device available. Use --all to select all devices, ' +
1133          'or use --device to select a device by serial.\n\n' +
1134          _GenerateAvailableDevicesMessage(devices))
1135
1136
1137def _DisplayArgs(devices, command_line_flags_file):
1138  def flags_helper(d):
1139    changer = flag_changer.FlagChanger(d, command_line_flags_file)
1140    return changer.GetCurrentFlags()
1141
1142  parallel_devices = device_utils.DeviceUtils.parallel(devices)
1143  outputs = parallel_devices.pMap(flags_helper).pGet(None)
1144  print('Existing flags per-device (via /data/local/tmp/{}):'.format(
1145      command_line_flags_file))
1146  for flags in _PrintPerDeviceOutput(devices, outputs, single_line=True):
1147    quoted_flags = ' '.join(shlex.quote(f) for f in flags)
1148    print(quoted_flags or 'No flags set.')
1149
1150
1151def _DeviceCachePath(device, output_directory):
1152  file_name = 'device_cache_%s.json' % device.serial
1153  return os.path.join(output_directory, file_name)
1154
1155
1156def _LoadDeviceCaches(devices, output_directory):
1157  if not output_directory:
1158    return
1159  for d in devices:
1160    cache_path = _DeviceCachePath(d, output_directory)
1161    if os.path.exists(cache_path):
1162      logging.debug('Using device cache: %s', cache_path)
1163      with open(cache_path) as f:
1164        d.LoadCacheData(f.read())
1165      # Delete the cached file so that any exceptions cause it to be cleared.
1166      os.unlink(cache_path)
1167    else:
1168      logging.debug('No cache present for device: %s', d)
1169
1170
1171def _SaveDeviceCaches(devices, output_directory):
1172  if not output_directory:
1173    return
1174  for d in devices:
1175    cache_path = _DeviceCachePath(d, output_directory)
1176    with open(cache_path, 'w') as f:
1177      f.write(d.DumpCacheData())
1178      logging.info('Wrote device cache: %s', cache_path)
1179
1180
1181class _Command:
1182  name = None
1183  description = None
1184  long_description = None
1185  needs_package_name = False
1186  needs_output_directory = False
1187  needs_apk_helper = False
1188  supports_incremental = False
1189  accepts_command_line_flags = False
1190  accepts_args = False
1191  need_device_args = True
1192  all_devices_by_default = False
1193  calls_exec = False
1194  supports_multiple_devices = True
1195
1196  def __init__(self, from_wrapper_script, is_bundle, is_test_apk):
1197    self._parser = None
1198    self._from_wrapper_script = from_wrapper_script
1199    self.args = None
1200    self.apk_helper = None
1201    self.additional_apk_helpers = None
1202    self.install_dict = None
1203    self.devices = None
1204    self.is_bundle = is_bundle
1205    self.is_test_apk = is_test_apk
1206    self.bundle_generation_info = None
1207    # Only support  incremental install from APK wrapper scripts.
1208    if is_bundle or not from_wrapper_script:
1209      self.supports_incremental = False
1210
1211  def RegisterBundleGenerationInfo(self, bundle_generation_info):
1212    self.bundle_generation_info = bundle_generation_info
1213
1214  def _RegisterExtraArgs(self, group):
1215    pass
1216
1217  def RegisterArgs(self, parser):
1218    subp = parser.add_parser(
1219        self.name, help=self.description,
1220        description=self.long_description or self.description,
1221        formatter_class=argparse.RawDescriptionHelpFormatter)
1222    self._parser = subp
1223    subp.set_defaults(command=self)
1224    if self.need_device_args:
1225      subp.add_argument('--all',
1226                        action='store_true',
1227                        default=self.all_devices_by_default,
1228                        help='Operate on all connected devices.',)
1229      subp.add_argument('-d',
1230                        '--device',
1231                        action='append',
1232                        default=[],
1233                        dest='devices',
1234                        help='Target device for script to work on. Enter '
1235                            'multiple times for multiple devices.')
1236    subp.add_argument('-v',
1237                      '--verbose',
1238                      action='count',
1239                      default=0,
1240                      dest='verbose_count',
1241                      help='Verbose level (multiple times for more)')
1242    group = subp.add_argument_group('%s arguments' % self.name)
1243
1244    if self.needs_package_name:
1245      # Three cases to consider here, since later code assumes
1246      #  self.args.package_name always exists, even if None:
1247      #
1248      # - Called from a bundle wrapper script, the package_name is already
1249      #   set through parser.set_defaults(), so don't call add_argument()
1250      #   to avoid overriding its value.
1251      #
1252      # - Called from an apk wrapper script. The --package-name argument
1253      #   should not appear, but self.args.package_name will be gleaned from
1254      #   the --apk-path file later.
1255      #
1256      # - Called directly, then --package-name is required on the command-line.
1257      #
1258      if not self.is_bundle:
1259        group.add_argument(
1260            '--package-name',
1261            help=argparse.SUPPRESS if self._from_wrapper_script else (
1262                "App's package name."))
1263
1264    if self.needs_apk_helper or self.needs_package_name:
1265      # Adding this argument to the subparser would override the set_defaults()
1266      # value set by on the parent parser (even if None).
1267      if not self._from_wrapper_script and not self.is_bundle:
1268        group.add_argument(
1269            '--apk-path', required=self.needs_apk_helper, help='Path to .apk')
1270
1271    if self.supports_incremental:
1272      group.add_argument('--incremental',
1273                          action='store_true',
1274                          default=False,
1275                          help='Always install an incremental apk.')
1276      group.add_argument('--non-incremental',
1277                          action='store_true',
1278                          default=False,
1279                          help='Always install a non-incremental apk.')
1280
1281    # accepts_command_line_flags and accepts_args are mutually exclusive.
1282    # argparse will throw if they are both set.
1283    if self.accepts_command_line_flags:
1284      group.add_argument(
1285          '--args', help='Command-line flags. Use = to assign args.')
1286
1287    if self.accepts_args:
1288      group.add_argument(
1289          '--args', help='Extra arguments. Use = to assign args')
1290
1291    if not self._from_wrapper_script and self.accepts_command_line_flags:
1292      # Provided by wrapper scripts.
1293      group.add_argument(
1294          '--command-line-flags-file',
1295          help='Name of the command-line flags file')
1296
1297    self._RegisterExtraArgs(group)
1298
1299  def _CreateApkHelpers(self, args, incremental_apk_path, install_dict):
1300    """Returns true iff self.apk_helper was created and assigned."""
1301    if self.apk_helper is None:
1302      if args.apk_path:
1303        self.apk_helper = apk_helper.ToHelper(args.apk_path)
1304      elif incremental_apk_path:
1305        self.install_dict = install_dict
1306        self.apk_helper = apk_helper.ToHelper(incremental_apk_path)
1307      elif self.is_bundle:
1308        _GenerateBundleApks(self.bundle_generation_info)
1309        self.apk_helper = apk_helper.ToHelper(
1310            self.bundle_generation_info.bundle_apks_path)
1311    if args.additional_apk_paths and self.additional_apk_helpers is None:
1312      self.additional_apk_helpers = [
1313          apk_helper.ToHelper(apk_path)
1314          for apk_path in args.additional_apk_paths
1315      ]
1316    return self.apk_helper is not None
1317
1318  def _FindSupportedDevices(self, devices):
1319    """Returns supported devices and reasons for each not supported one."""
1320    app_abis = self.apk_helper.GetAbis()
1321    calling_script_name = os.path.basename(sys.argv[0])
1322    is_webview = 'webview' in calling_script_name
1323    requires_32_bit = self.apk_helper.Get32BitAbiOverride() == '0xffffffff'
1324    logging.debug('App supports (requires 32bit: %r, is webview: %r): %r',
1325                  requires_32_bit, is_webview, app_abis)
1326    # Webview 32_64 targets can work even on 64-bit only devices since only the
1327    # webview library in the target needs the correct bitness.
1328    if requires_32_bit and not is_webview:
1329      app_abis = [abi for abi in app_abis if '64' not in abi]
1330      logging.debug('App supports (filtered): %r', app_abis)
1331    if not app_abis:
1332      # The app does not have any native libs, so all devices can support it.
1333      return devices, None
1334    fully_supported = []
1335    not_supported_reasons = {}
1336    for device in devices:
1337      device_abis = device.GetSupportedABIs()
1338      device_primary_abi = device_abis[0]
1339      logging.debug('Device primary: %s', device_primary_abi)
1340      logging.debug('Device supports: %r', device_abis)
1341
1342      # x86/x86_64 emulators sometimes advertises arm support but arm builds do
1343      # not work on them. Thus these non-functional ABIs need to be filtered out
1344      # here to avoid resulting in hard to understand runtime failures.
1345      if device_primary_abi in ('x86', 'x86_64'):
1346        device_abis = [abi for abi in device_abis if not abi.startswith('arm')]
1347        logging.debug('Device supports (filtered): %r', device_abis)
1348
1349      if any(abi in app_abis for abi in device_abis):
1350        fully_supported.append(device)
1351      else:  # No common supported ABIs between the device and app.
1352        if device_primary_abi == 'x86':
1353          target_cpu = 'x86'
1354        elif device_primary_abi == 'x86_64':
1355          target_cpu = 'x64'
1356        elif device_primary_abi.startswith('arm64'):
1357          target_cpu = 'arm64'
1358        elif device_primary_abi.startswith('armeabi'):
1359          target_cpu = 'arm'
1360        else:
1361          target_cpu = '<something else>'
1362        # pylint: disable=line-too-long
1363        native_lib_link = 'https://chromium.googlesource.com/chromium/src/+/main/docs/android_native_libraries.md'
1364        not_supported_reasons[device.serial] = (
1365            f"none of the app's ABIs ({','.join(app_abis)}) match this "
1366            f"device's ABIs ({','.join(device_abis)}), you may need to set "
1367            f'target_cpu="{target_cpu}" in your args.gn. If you already set '
1368            'the target_cpu arg, you may need to use one of the _64 or _64_32 '
1369            f'targets, see {native_lib_link} for more details.')
1370    return fully_supported, not_supported_reasons
1371
1372  def ProcessArgs(self, args):
1373    self.args = args
1374    # Ensure these keys always exist. They are set by wrapper scripts, but not
1375    # always added when not using wrapper scripts.
1376    args.__dict__.setdefault('apk_path', None)
1377    args.__dict__.setdefault('incremental_json', None)
1378
1379    incremental_apk_path = None
1380    install_dict = None
1381    if args.incremental_json and not (self.supports_incremental and
1382                                      args.non_incremental):
1383      with open(args.incremental_json) as f:
1384        install_dict = json.load(f)
1385        incremental_apk_path = os.path.join(args.output_directory,
1386                                            install_dict['apk_path'])
1387        if not os.path.exists(incremental_apk_path):
1388          incremental_apk_path = None
1389
1390    if self.supports_incremental:
1391      if args.incremental and args.non_incremental:
1392        self._parser.error('Must use only one of --incremental and '
1393                           '--non-incremental')
1394      elif args.non_incremental:
1395        if not args.apk_path:
1396          self._parser.error('Apk has not been built.')
1397      elif args.incremental:
1398        if not incremental_apk_path:
1399          self._parser.error('Incremental apk has not been built.')
1400        args.apk_path = None
1401
1402      if args.apk_path and incremental_apk_path:
1403        self._parser.error('Both incremental and non-incremental apks exist. '
1404                           'Select using --incremental or --non-incremental')
1405
1406
1407    # Gate apk_helper creation with _CreateApkHelpers since for bundles it takes
1408    # a while to unpack the apks file from the aab file, so avoid this slowdown
1409    # for simple commands that don't need apk_helper.
1410    if self.needs_apk_helper:
1411      if not self._CreateApkHelpers(args, incremental_apk_path, install_dict):
1412        self._parser.error('App is not built.')
1413
1414    if self.needs_package_name and not args.package_name:
1415      if self._CreateApkHelpers(args, incremental_apk_path, install_dict):
1416        args.package_name = self.apk_helper.GetPackageName()
1417      elif self._from_wrapper_script:
1418        self._parser.error('App is not built.')
1419      else:
1420        self._parser.error('One of --package-name or --apk-path is required.')
1421
1422    self.devices = []
1423    if self.need_device_args:
1424      # Avoid filtering by ABIs with catapult since some x86 or x86_64 emulators
1425      # can still work with the right target_cpu GN arg and the right targets.
1426      # Doing this manually allows us to output more informative warnings to
1427      # help devs towards the right course, see: https://crbug.com/1335139
1428      available_devices = device_utils.DeviceUtils.HealthyDevices(
1429          device_arg=args.devices,
1430          enable_device_files_cache=bool(args.output_directory),
1431          default_retries=0)
1432      if not available_devices:
1433        raise Exception('Cannot find any available devices.')
1434
1435      if not self._CreateApkHelpers(args, incremental_apk_path, install_dict):
1436        self.devices = available_devices
1437      else:
1438        fully_supported, not_supported_reasons = self._FindSupportedDevices(
1439            available_devices)
1440        if fully_supported:
1441          self.devices = fully_supported
1442        else:
1443          reason_string = '\n'.join(
1444              'The device (serial={}) is not supported because {}'.format(
1445                  serial, reason)
1446              for serial, reason in not_supported_reasons.items())
1447          raise Exception('Cannot find any supported devices for this app.\n\n'
1448                          f'{reason_string}')
1449
1450      # TODO(agrieve): Device cache should not depend on output directory.
1451      #     Maybe put into /tmp?
1452      _LoadDeviceCaches(self.devices, args.output_directory)
1453
1454      try:
1455        if len(self.devices) > 1:
1456          if not self.supports_multiple_devices:
1457            self._parser.error(device_errors.MultipleDevicesError(self.devices))
1458          if not args.all and not args.devices:
1459            self._parser.error(_GenerateMissingAllFlagMessage(self.devices))
1460        # Save cache now if command will not get a chance to afterwards.
1461        if self.calls_exec:
1462          _SaveDeviceCaches(self.devices, args.output_directory)
1463      except:
1464        _SaveDeviceCaches(self.devices, args.output_directory)
1465        raise
1466
1467
1468class _DevicesCommand(_Command):
1469  name = 'devices'
1470  description = 'Describe attached devices.'
1471  all_devices_by_default = True
1472
1473  def Run(self):
1474    print(_GenerateAvailableDevicesMessage(self.devices))
1475
1476
1477class _PackageInfoCommand(_Command):
1478  name = 'package-info'
1479  description = 'Show various attributes of this app.'
1480  need_device_args = False
1481  needs_package_name = True
1482  needs_apk_helper = True
1483
1484  def Run(self):
1485    # Format all (even ints) as strings, to handle cases where APIs return None
1486    print('Package name: "%s"' % self.args.package_name)
1487    print('versionCode: %s' % self.apk_helper.GetVersionCode())
1488    print('versionName: "%s"' % self.apk_helper.GetVersionName())
1489    print('minSdkVersion: %s' % self.apk_helper.GetMinSdkVersion())
1490    print('targetSdkVersion: %s' % self.apk_helper.GetTargetSdkVersion())
1491    print('Supported ABIs: %r' % self.apk_helper.GetAbis())
1492
1493
1494class _InstallCommand(_Command):
1495  name = 'install'
1496  description = 'Installs the APK or bundle to one or more devices.'
1497  needs_apk_helper = True
1498  supports_incremental = True
1499  default_modules = []
1500
1501  def _RegisterExtraArgs(self, group):
1502    if self.is_bundle:
1503      group.add_argument(
1504          '-m',
1505          '--module',
1506          action='append',
1507          default=self.default_modules,
1508          help='Module to install. Can be specified multiple times.')
1509      group.add_argument(
1510          '-f',
1511          '--fake',
1512          action='append',
1513          default=[],
1514          help='Fake bundle module install. Can be specified multiple times. '
1515          'Requires \'-m {0}\' to be given, and \'-f {0}\' is illegal.'.format(
1516              BASE_MODULE))
1517      # Add even if |self.default_modules| is empty, for consistency.
1518      group.add_argument('--no-module',
1519                         action='append',
1520                         choices=self.default_modules,
1521                         default=[],
1522                         help='Module to exclude from default install.')
1523
1524  def Run(self):
1525    if self.additional_apk_helpers:
1526      for additional_apk_helper in self.additional_apk_helpers:
1527        _InstallApk(self.devices, additional_apk_helper, None)
1528    if self.is_bundle:
1529      modules = list(
1530          set(self.args.module) - set(self.args.no_module) -
1531          set(self.args.fake))
1532      _InstallBundle(self.devices, self.apk_helper, modules, self.args.fake)
1533    else:
1534      _InstallApk(self.devices, self.apk_helper, self.install_dict)
1535
1536
1537class _UninstallCommand(_Command):
1538  name = 'uninstall'
1539  description = 'Removes the APK or bundle from one or more devices.'
1540  needs_package_name = True
1541
1542  def Run(self):
1543    _UninstallApk(self.devices, self.install_dict, self.args.package_name)
1544
1545
1546class _SetWebViewProviderCommand(_Command):
1547  name = 'set-webview-provider'
1548  description = ("Sets the device's WebView provider to this APK's "
1549                 "package name.")
1550  needs_package_name = True
1551  needs_apk_helper = True
1552
1553  def Run(self):
1554    if not _IsWebViewProvider(self.apk_helper):
1555      raise Exception('This package does not have a WebViewLibrary meta-data '
1556                      'tag. Are you sure it contains a WebView implementation?')
1557    _SetWebViewProvider(self.devices, self.args.package_name)
1558
1559
1560class _LaunchCommand(_Command):
1561  name = 'launch'
1562  description = ('Sends a launch intent for the APK or bundle after first '
1563                 'writing the command-line flags file.')
1564  needs_package_name = True
1565  accepts_command_line_flags = True
1566  all_devices_by_default = True
1567
1568  def _RegisterExtraArgs(self, group):
1569    group.add_argument('-w', '--wait-for-java-debugger', action='store_true',
1570                       help='Pause execution until debugger attaches. Applies '
1571                            'only to the main process. To have renderers wait, '
1572                            'use --args="--renderer-wait-for-java-debugger"')
1573    group.add_argument('--debug-process-name',
1574                       help='Name of the process to debug. '
1575                            'E.g. "privileged_process0", or "foo.bar:baz"')
1576    group.add_argument('--nokill', action='store_true',
1577                       help='Do not set the debug-app, nor set command-line '
1578                            'flags. Useful to load a URL without having the '
1579                             'app restart.')
1580    group.add_argument('url', nargs='?', help='A URL to launch with.')
1581
1582  def Run(self):
1583    if self.is_test_apk:
1584      raise Exception('Use the bin/run_* scripts to run test apks.')
1585    _LaunchUrl(self.devices,
1586               self.args.package_name,
1587               argv=self.args.args,
1588               command_line_flags_file=self.args.command_line_flags_file,
1589               url=self.args.url,
1590               wait_for_java_debugger=self.args.wait_for_java_debugger,
1591               debug_process_name=self.args.debug_process_name,
1592               nokill=self.args.nokill)
1593
1594
1595class _StopCommand(_Command):
1596  name = 'stop'
1597  description = 'Force-stops the app.'
1598  needs_package_name = True
1599  all_devices_by_default = True
1600
1601  def Run(self):
1602    device_utils.DeviceUtils.parallel(self.devices).ForceStop(
1603        self.args.package_name)
1604
1605
1606class _ClearDataCommand(_Command):
1607  name = 'clear-data'
1608  descriptions = 'Clears all app data.'
1609  needs_package_name = True
1610  all_devices_by_default = True
1611
1612  def Run(self):
1613    device_utils.DeviceUtils.parallel(self.devices).ClearApplicationState(
1614        self.args.package_name)
1615
1616
1617class _ArgvCommand(_Command):
1618  name = 'argv'
1619  description = 'Display and optionally update command-line flags file.'
1620  needs_package_name = True
1621  accepts_command_line_flags = True
1622  all_devices_by_default = True
1623
1624  def Run(self):
1625    _ChangeFlags(self.devices, self.args.args,
1626                 self.args.command_line_flags_file)
1627
1628
1629class _GdbCommand(_Command):
1630  name = 'gdb'
1631  description = 'Runs //build/android/adb_gdb with apk-specific args.'
1632  long_description = description + """
1633
1634To attach to a process other than the APK's main process, use --pid=1234.
1635To list all PIDs, use the "ps" command.
1636
1637If no apk process is currently running, sends a launch intent.
1638"""
1639  needs_package_name = True
1640  needs_output_directory = True
1641  calls_exec = True
1642  supports_multiple_devices = False
1643
1644  def Run(self):
1645    _RunGdb(self.devices[0], self.args.package_name,
1646            self.args.debug_process_name, self.args.pid,
1647            self.args.output_directory, self.args.target_cpu, self.args.port,
1648            self.args.ide, bool(self.args.verbose_count))
1649
1650  def _RegisterExtraArgs(self, group):
1651    pid_group = group.add_mutually_exclusive_group()
1652    pid_group.add_argument('--debug-process-name',
1653                           help='Name of the process to attach to. '
1654                                'E.g. "privileged_process0", or "foo.bar:baz"')
1655    pid_group.add_argument('--pid',
1656                           help='The process ID to attach to. Defaults to '
1657                                'the main process for the package.')
1658    group.add_argument('--ide', action='store_true',
1659                       help='Rather than enter a gdb prompt, set up the '
1660                            'gdb connection and wait for an IDE to '
1661                            'connect.')
1662    # Same default port that ndk-gdb.py uses.
1663    group.add_argument('--port', type=int, default=5039,
1664                       help='Use the given port for the GDB connection')
1665
1666
1667class _LldbCommand(_Command):
1668  name = 'lldb'
1669  description = 'Runs //build/android/connect_lldb.sh with apk-specific args.'
1670  long_description = description + """
1671
1672To attach to a process other than the APK's main process, use --pid=1234.
1673To list all PIDs, use the "ps" command.
1674
1675If no apk process is currently running, sends a launch intent.
1676"""
1677  needs_package_name = True
1678  needs_output_directory = True
1679  calls_exec = True
1680  supports_multiple_devices = False
1681
1682  def Run(self):
1683    _RunLldb(device=self.devices[0],
1684             package_name=self.args.package_name,
1685             debug_process_name=self.args.debug_process_name,
1686             pid=self.args.pid,
1687             output_directory=self.args.output_directory,
1688             port=self.args.port,
1689             target_cpu=self.args.target_cpu,
1690             ndk_dir=self.args.ndk_dir,
1691             lldb_server=self.args.lldb_server,
1692             lldb=self.args.lldb,
1693             verbose=bool(self.args.verbose_count))
1694
1695  def _RegisterExtraArgs(self, group):
1696    pid_group = group.add_mutually_exclusive_group()
1697    pid_group.add_argument('--debug-process-name',
1698                           help='Name of the process to attach to. '
1699                           'E.g. "privileged_process0", or "foo.bar:baz"')
1700    pid_group.add_argument('--pid',
1701                           help='The process ID to attach to. Defaults to '
1702                           'the main process for the package.')
1703    group.add_argument('--ndk-dir',
1704                       help='Select alternative NDK root directory.')
1705    group.add_argument('--lldb-server',
1706                       help='Select alternative on-device lldb-server.')
1707    group.add_argument('--lldb', help='Select alternative client lldb.sh.')
1708    # Same default port that ndk-gdb.py uses.
1709    group.add_argument('--port',
1710                       type=int,
1711                       default=5039,
1712                       help='Use the given port for the LLDB connection')
1713
1714
1715class _LogcatCommand(_Command):
1716  name = 'logcat'
1717  description = 'Runs "adb logcat" with filters relevant the current APK.'
1718  long_description = description + """
1719
1720"Relevant filters" means:
1721  * Log messages from processes belonging to the apk,
1722  * Plus log messages from log tags: ActivityManager|DEBUG,
1723  * Plus fatal logs from any process,
1724  * Minus spamy dalvikvm logs (for pre-L devices).
1725
1726Colors:
1727  * Primary process is white
1728  * Other processes (gpu, renderer) are yellow
1729  * Non-apk processes are grey
1730  * UI thread has a bolded Thread-ID
1731
1732Java stack traces are detected and deobfuscated (for release builds).
1733
1734To disable filtering, (but keep coloring), use --verbose.
1735"""
1736  needs_package_name = True
1737  supports_multiple_devices = False
1738
1739  def Run(self):
1740    deobfuscate = None
1741    if self.args.proguard_mapping_path and not self.args.no_deobfuscate:
1742      deobfuscate = deobfuscator.Deobfuscator(self.args.proguard_mapping_path)
1743
1744    stack_script_context = _StackScriptContext(
1745        self.args.output_directory,
1746        self.args.apk_path,
1747        self.bundle_generation_info,
1748        quiet=True)
1749
1750    extra_package_names = []
1751    if self.is_test_apk and self.additional_apk_helpers:
1752      for additional_apk_helper in self.additional_apk_helpers:
1753        extra_package_names.append(additional_apk_helper.GetPackageName())
1754
1755    try:
1756      _RunLogcat(self.devices[0],
1757                 self.args.package_name,
1758                 stack_script_context,
1759                 deobfuscate,
1760                 bool(self.args.verbose_count),
1761                 self.args.exit_on_match,
1762                 extra_package_names=extra_package_names)
1763    except KeyboardInterrupt:
1764      pass  # Don't show stack trace upon Ctrl-C
1765    finally:
1766      stack_script_context.Close()
1767      if deobfuscate:
1768        deobfuscate.Close()
1769
1770  def _RegisterExtraArgs(self, group):
1771    if self._from_wrapper_script:
1772      group.add_argument('--no-deobfuscate', action='store_true',
1773          help='Disables ProGuard deobfuscation of logcat.')
1774    else:
1775      group.set_defaults(no_deobfuscate=False)
1776      group.add_argument('--proguard-mapping-path',
1777          help='Path to ProGuard map (enables deobfuscation)')
1778    group.add_argument('--exit-on-match',
1779                       help='Exits logcat when a message matches this regex.')
1780
1781
1782class _PsCommand(_Command):
1783  name = 'ps'
1784  description = 'Show PIDs of any APK processes currently running.'
1785  needs_package_name = True
1786  all_devices_by_default = True
1787
1788  def Run(self):
1789    _RunPs(self.devices, self.args.package_name)
1790
1791
1792class _DiskUsageCommand(_Command):
1793  name = 'disk-usage'
1794  description = 'Show how much device storage is being consumed by the app.'
1795  needs_package_name = True
1796  all_devices_by_default = True
1797
1798  def Run(self):
1799    _RunDiskUsage(self.devices, self.args.package_name)
1800
1801
1802class _MemUsageCommand(_Command):
1803  name = 'mem-usage'
1804  description = 'Show memory usage of currently running APK processes.'
1805  needs_package_name = True
1806  all_devices_by_default = True
1807
1808  def _RegisterExtraArgs(self, group):
1809    group.add_argument('--query-app', action='store_true',
1810        help='Do not add --local to "dumpsys meminfo". This will output '
1811             'additional metrics (e.g. Context count), but also cause memory '
1812             'to be used in order to gather the metrics.')
1813
1814  def Run(self):
1815    _RunMemUsage(self.devices, self.args.package_name,
1816                 query_app=self.args.query_app)
1817
1818
1819class _ShellCommand(_Command):
1820  name = 'shell'
1821  description = ('Same as "adb shell <command>", but runs as the apk\'s uid '
1822                 '(via run-as). Useful for inspecting the app\'s data '
1823                 'directory.')
1824  needs_package_name = True
1825
1826  @property
1827  def calls_exec(self):
1828    return not self.args.cmd
1829
1830  @property
1831  def supports_multiple_devices(self):
1832    return not self.args.cmd
1833
1834  def _RegisterExtraArgs(self, group):
1835    group.add_argument(
1836        'cmd', nargs=argparse.REMAINDER, help='Command to run.')
1837
1838  def Run(self):
1839    _RunShell(self.devices, self.args.package_name, self.args.cmd)
1840
1841
1842class _CompileDexCommand(_Command):
1843  name = 'compile-dex'
1844  description = ('Applicable only for Android N+. Forces .odex files to be '
1845                 'compiled with the given compilation filter. To see existing '
1846                 'filter, use "disk-usage" command.')
1847  needs_package_name = True
1848  all_devices_by_default = True
1849
1850  def _RegisterExtraArgs(self, group):
1851    group.add_argument(
1852        'compilation_filter',
1853        choices=['verify', 'quicken', 'space-profile', 'space',
1854                 'speed-profile', 'speed'],
1855        help='For WebView/Monochrome, use "speed". For other apks, use '
1856             '"speed-profile".')
1857
1858  def Run(self):
1859    _RunCompileDex(self.devices, self.args.package_name,
1860                   self.args.compilation_filter)
1861
1862
1863class _PrintCertsCommand(_Command):
1864  name = 'print-certs'
1865  description = 'Print info about certificates used to sign this APK.'
1866  need_device_args = False
1867  needs_apk_helper = True
1868
1869  def _RegisterExtraArgs(self, group):
1870    group.add_argument(
1871        '--full-cert',
1872        action='store_true',
1873        help=("Print the certificate's full signature, Base64-encoded. "
1874              "Useful when configuring an Android image's "
1875              "config_webview_packages.xml."))
1876
1877  def Run(self):
1878    keytool = os.path.join(_JAVA_HOME, 'bin', 'keytool')
1879    pem_certificate_pattern = re.compile(
1880        r'-+BEGIN CERTIFICATE-+([\r\n0-9A-Za-z+/=]+)-+END CERTIFICATE-+[\r\n]*')
1881    if self.is_bundle:
1882      # Bundles are not signed until converted to .apks. The wrapper scripts
1883      # record which key will be used to sign though.
1884      with tempfile.NamedTemporaryFile() as f:
1885        logging.warning('Bundles are not signed until turned into .apk files.')
1886        logging.warning('Showing signing info based on associated keystore.')
1887        cmd = [
1888            keytool, '-exportcert', '-keystore',
1889            self.bundle_generation_info.keystore_path, '-storepass',
1890            self.bundle_generation_info.keystore_password, '-alias',
1891            self.bundle_generation_info.keystore_alias, '-file', f.name
1892        ]
1893        logging.warning('Running: %s', shlex.join(cmd))
1894        subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1895        cmd = [keytool, '-printcert', '-file', f.name]
1896        logging.warning('Running: %s', shlex.join(cmd))
1897        subprocess.check_call(cmd)
1898        if self.args.full_cert:
1899          # Redirect stderr to hide a keytool warning about using non-standard
1900          # keystore format.
1901          cmd += ['-rfc']
1902          logging.warning('Running: %s', shlex.join(cmd))
1903          pem_encoded_certificate = subprocess.check_output(
1904              cmd, stderr=subprocess.STDOUT).decode()
1905    else:
1906
1907      def run_apksigner(min_sdk_version):
1908        cmd = [
1909            build_tools.GetPath('apksigner'), 'verify', '--min-sdk-version',
1910            str(min_sdk_version), '--print-certs-pem', '--verbose',
1911            self.apk_helper.path
1912        ]
1913        logging.warning('Running: %s', shlex.join(cmd))
1914        env = os.environ.copy()
1915        env['PATH'] = os.path.pathsep.join(
1916            [os.path.join(_JAVA_HOME, 'bin'),
1917             env.get('PATH')])
1918        # Redirect stderr to hide verification failures (see explanation below).
1919        return subprocess.check_output(cmd,
1920                                       env=env,
1921                                       universal_newlines=True,
1922                                       stderr=subprocess.STDOUT)
1923
1924      # apksigner's default behavior is nonintuitive: it will print "Verified
1925      # using <scheme number>...: false" for any scheme which is obsolete for
1926      # the APK's minSdkVersion even if it actually was signed with that scheme
1927      # (ex. it prints "Verified using v1 scheme: false" for Monochrome because
1928      # v1 was obsolete by N). To workaround this, we force apksigner to use the
1929      # lowest possible minSdkVersion. We need to fallback to higher
1930      # minSdkVersions in case the APK fails to verify for that minSdkVersion
1931      # (which means the APK is genuinely not signed with that scheme). These
1932      # SDK values are the highest SDK version before the next scheme is
1933      # available:
1934      versions = [
1935          version_codes.MARSHMALLOW,  # before v2 launched in N
1936          version_codes.OREO_MR1,  # before v3 launched in P
1937          version_codes.Q,  # before v4 launched in R
1938          version_codes.R,
1939      ]
1940      stdout = None
1941      for min_sdk_version in versions:
1942        try:
1943          stdout = run_apksigner(min_sdk_version)
1944          break
1945        except subprocess.CalledProcessError:
1946          # Doesn't verify with this min-sdk-version, so try again with a higher
1947          # one
1948          continue
1949      if not stdout:
1950        raise RuntimeError('apksigner was not able to verify APK')
1951
1952      # Separate what the '--print-certs' flag would output vs. the additional
1953      # signature output included by '--print-certs-pem'. The additional PEM
1954      # output is only printed when self.args.full_cert is specified.
1955      verification_hash_info = pem_certificate_pattern.sub('', stdout)
1956      print(verification_hash_info)
1957      if self.args.full_cert:
1958        m = pem_certificate_pattern.search(stdout)
1959        if not m:
1960          raise Exception('apksigner did not print a certificate')
1961        pem_encoded_certificate = m.group(0)
1962
1963
1964    if self.args.full_cert:
1965      m = pem_certificate_pattern.search(pem_encoded_certificate)
1966      if not m:
1967        raise Exception(
1968            'Unable to parse certificate:\n{}'.format(pem_encoded_certificate))
1969      signature = re.sub(r'[\r\n]+', '', m.group(1))
1970      print()
1971      print('Full Signature:')
1972      print(signature)
1973
1974
1975class _ProfileCommand(_Command):
1976  name = 'profile'
1977  description = ('Run the simpleperf sampling CPU profiler on the currently-'
1978                 'running APK. If --args is used, the extra arguments will be '
1979                 'passed on to simpleperf; otherwise, the following default '
1980                 'arguments are used: -g -f 1000 -o /data/local/tmp/perf.data')
1981  needs_package_name = True
1982  needs_output_directory = True
1983  supports_multiple_devices = False
1984  accepts_args = True
1985
1986  def _RegisterExtraArgs(self, group):
1987    group.add_argument(
1988        '--profile-process', default='browser',
1989        help=('Which process to profile. This may be a process name or pid '
1990              'such as you would get from running `%s ps`; or '
1991              'it can be one of (browser, renderer, gpu).' % sys.argv[0]))
1992    group.add_argument(
1993        '--profile-thread', default=None,
1994        help=('(Optional) Profile only a single thread. This may be either a '
1995              'thread ID such as you would get by running `adb shell ps -t` '
1996              '(pre-Oreo) or `adb shell ps -e -T` (Oreo and later); or it may '
1997              'be one of (io, compositor, main, render), in which case '
1998              '--profile-process is also required. (Note that "render" thread '
1999              'refers to a thread in the browser process that manages a '
2000              'renderer; to profile the main thread of the renderer process, '
2001              'use --profile-thread=main).'))
2002    group.add_argument('--profile-output', default='profile.pb',
2003                       help='Output file for profiling data')
2004    group.add_argument('--profile-events', default='cpu-cycles',
2005                      help=('A comma separated list of perf events to capture '
2006                      '(e.g. \'cpu-cycles,branch-misses\'). Run '
2007                      '`simpleperf list` on your device to see available '
2008                      'events.'))
2009
2010  def Run(self):
2011    extra_args = shlex.split(self.args.args or '')
2012    _RunProfile(self.devices[0], self.args.package_name,
2013                self.args.output_directory, self.args.profile_output,
2014                self.args.profile_process, self.args.profile_thread,
2015                self.args.profile_events, extra_args)
2016
2017
2018class _RunCommand(_InstallCommand, _LaunchCommand, _LogcatCommand):
2019  name = 'run'
2020  description = 'Install, launch, and show logcat (when targeting one device).'
2021  all_devices_by_default = False
2022  supports_multiple_devices = True
2023
2024  def _RegisterExtraArgs(self, group):
2025    _InstallCommand._RegisterExtraArgs(self, group)
2026    _LaunchCommand._RegisterExtraArgs(self, group)
2027    _LogcatCommand._RegisterExtraArgs(self, group)
2028    group.add_argument('--no-logcat', action='store_true',
2029                       help='Install and launch, but do not enter logcat.')
2030
2031  def Run(self):
2032    if self.is_test_apk:
2033      raise Exception('Use the bin/run_* scripts to run test apks.')
2034    logging.warning('Installing...')
2035    _InstallCommand.Run(self)
2036    logging.warning('Sending launch intent...')
2037    _LaunchCommand.Run(self)
2038    if len(self.devices) == 1 and not self.args.no_logcat:
2039      logging.warning('Entering logcat...')
2040      _LogcatCommand.Run(self)
2041
2042
2043class _BuildBundleApks(_Command):
2044  name = 'build-bundle-apks'
2045  description = ('Build the .apks archive from an Android app bundle, and '
2046                 'optionally copy it to a specific destination.')
2047  need_device_args = False
2048
2049  def _RegisterExtraArgs(self, group):
2050    group.add_argument(
2051        '--output-apks', required=True, help='Destination path for .apks file.')
2052    group.add_argument(
2053        '--minimal',
2054        action='store_true',
2055        help='Build .apks archive that targets the bundle\'s minSdkVersion and '
2056        'contains only english splits. It still contains optional splits.')
2057    group.add_argument(
2058        '--sdk-version', help='The sdkVersion to build the .apks for.')
2059    group.add_argument(
2060        '--build-mode',
2061        choices=app_bundle_utils.BUILD_APKS_MODES,
2062        help='Specify which type of APKs archive to build. "default" '
2063        'generates regular splits, "universal" generates an archive with a '
2064        'single universal APK, "system" generates an archive with a system '
2065        'image APK, while "system_compressed" generates a compressed system '
2066        'APK, with an additional stub APK for the system image.')
2067    group.add_argument(
2068        '--optimize-for',
2069        choices=app_bundle_utils.OPTIMIZE_FOR_OPTIONS,
2070        help='Override split configuration.')
2071
2072  def Run(self):
2073    _GenerateBundleApks(
2074        self.bundle_generation_info,
2075        output_path=self.args.output_apks,
2076        minimal=self.args.minimal,
2077        minimal_sdk_version=self.args.sdk_version,
2078        mode=self.args.build_mode,
2079        optimize_for=self.args.optimize_for)
2080
2081
2082class _ManifestCommand(_Command):
2083  name = 'dump-manifest'
2084  description = 'Dump the android manifest as XML, to stdout.'
2085  need_device_args = False
2086  needs_apk_helper = True
2087
2088  def Run(self):
2089    if self.is_bundle:
2090      sys.stdout.write(
2091          bundletool.RunBundleTool([
2092              'dump', 'manifest', '--bundle',
2093              self.bundle_generation_info.bundle_path
2094          ]))
2095    else:
2096      apkanalyzer = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'android_sdk',
2097                                 'public', 'cmdline-tools', 'latest', 'bin',
2098                                 'apkanalyzer')
2099      cmd = [apkanalyzer, 'manifest', 'print', self.apk_helper.path]
2100      logging.info('Running: %s', shlex.join(cmd))
2101      subprocess.check_call(cmd)
2102
2103
2104class _StackCommand(_Command):
2105  name = 'stack'
2106  description = 'Decodes an Android stack.'
2107  need_device_args = False
2108
2109  def _RegisterExtraArgs(self, group):
2110    group.add_argument(
2111        'file',
2112        nargs='?',
2113        help='File to decode. If not specified, stdin is processed.')
2114
2115  def Run(self):
2116    context = _StackScriptContext(self.args.output_directory,
2117                                  self.args.apk_path,
2118                                  self.bundle_generation_info)
2119    try:
2120      proc = context.Popen(input_file=self.args.file)
2121      if proc.wait():
2122        raise Exception('stack script returned {}'.format(proc.returncode))
2123    finally:
2124      context.Close()
2125
2126
2127# Shared commands for regular APKs and app bundles.
2128_COMMANDS = [
2129    _DevicesCommand,
2130    _PackageInfoCommand,
2131    _InstallCommand,
2132    _UninstallCommand,
2133    _SetWebViewProviderCommand,
2134    _LaunchCommand,
2135    _StopCommand,
2136    _ClearDataCommand,
2137    _ArgvCommand,
2138    _GdbCommand,
2139    _LldbCommand,
2140    _LogcatCommand,
2141    _PsCommand,
2142    _DiskUsageCommand,
2143    _MemUsageCommand,
2144    _ShellCommand,
2145    _CompileDexCommand,
2146    _PrintCertsCommand,
2147    _ProfileCommand,
2148    _RunCommand,
2149    _StackCommand,
2150    _ManifestCommand,
2151]
2152
2153# Commands specific to app bundles.
2154_BUNDLE_COMMANDS = [
2155    _BuildBundleApks,
2156]
2157
2158
2159def _ParseArgs(parser, from_wrapper_script, is_bundle, is_test_apk):
2160  subparsers = parser.add_subparsers()
2161  command_list = _COMMANDS + (_BUNDLE_COMMANDS if is_bundle else [])
2162  commands = [
2163      clazz(from_wrapper_script, is_bundle, is_test_apk)
2164      for clazz in command_list
2165  ]
2166
2167  for command in commands:
2168    if from_wrapper_script or not command.needs_output_directory:
2169      command.RegisterArgs(subparsers)
2170
2171  # Show extended help when no command is passed.
2172  argv = sys.argv[1:]
2173  if not argv:
2174    argv = ['--help']
2175
2176  return parser.parse_args(argv)
2177
2178
2179def _RunInternal(parser,
2180                 output_directory=None,
2181                 additional_apk_paths=None,
2182                 bundle_generation_info=None,
2183                 is_test_apk=False):
2184  colorama.init()
2185  parser.set_defaults(
2186      additional_apk_paths=additional_apk_paths,
2187      output_directory=output_directory)
2188  from_wrapper_script = bool(output_directory)
2189  args = _ParseArgs(parser,
2190                    from_wrapper_script,
2191                    is_bundle=bool(bundle_generation_info),
2192                    is_test_apk=is_test_apk)
2193  run_tests_helper.SetLogLevel(args.verbose_count)
2194  if bundle_generation_info:
2195    args.command.RegisterBundleGenerationInfo(bundle_generation_info)
2196  if args.additional_apk_paths:
2197    for path in additional_apk_paths:
2198      if not path or not os.path.exists(path):
2199        raise Exception('Invalid additional APK path "{}"'.format(path))
2200  args.command.ProcessArgs(args)
2201  args.command.Run()
2202  # Incremental install depends on the cache being cleared when uninstalling.
2203  if args.command.name != 'uninstall':
2204    _SaveDeviceCaches(args.command.devices, output_directory)
2205
2206
2207def Run(output_directory, apk_path, additional_apk_paths, incremental_json,
2208        command_line_flags_file, target_cpu, proguard_mapping_path):
2209  """Entry point for generated wrapper scripts."""
2210  constants.SetOutputDirectory(output_directory)
2211  devil_chromium.Initialize(output_directory=output_directory)
2212  parser = argparse.ArgumentParser()
2213  exists_or_none = lambda p: p if p and os.path.exists(p) else None
2214
2215  parser.set_defaults(
2216      command_line_flags_file=command_line_flags_file,
2217      target_cpu=target_cpu,
2218      apk_path=exists_or_none(apk_path),
2219      incremental_json=exists_or_none(incremental_json),
2220      proguard_mapping_path=proguard_mapping_path)
2221  _RunInternal(
2222      parser,
2223      output_directory=output_directory,
2224      additional_apk_paths=additional_apk_paths)
2225
2226
2227def RunForBundle(output_directory, bundle_path, bundle_apks_path,
2228                 additional_apk_paths, aapt2_path, keystore_path,
2229                 keystore_password, keystore_alias, package_name,
2230                 command_line_flags_file, proguard_mapping_path, target_cpu,
2231                 system_image_locales, default_modules):
2232  """Entry point for generated app bundle wrapper scripts.
2233
2234  Args:
2235    output_dir: Chromium output directory path.
2236    bundle_path: Input bundle path.
2237    bundle_apks_path: Output bundle .apks archive path.
2238    additional_apk_paths: Additional APKs to install prior to bundle install.
2239    aapt2_path: Aapt2 tool path.
2240    keystore_path: Keystore file path.
2241    keystore_password: Keystore password.
2242    keystore_alias: Signing key name alias in keystore file.
2243    package_name: Application's package name.
2244    command_line_flags_file: Optional. Name of an on-device file that will be
2245      used to store command-line flags for this bundle.
2246    proguard_mapping_path: Input path to the Proguard mapping file, used to
2247      deobfuscate Java stack traces.
2248    target_cpu: Chromium target CPU name, used by the 'gdb' command.
2249    system_image_locales: List of Chromium locales that should be included in
2250      system image APKs.
2251    default_modules: List of modules that are installed in addition to those
2252      given by the '-m' switch.
2253  """
2254  constants.SetOutputDirectory(output_directory)
2255  devil_chromium.Initialize(output_directory=output_directory)
2256  bundle_generation_info = BundleGenerationInfo(
2257      bundle_path=bundle_path,
2258      bundle_apks_path=bundle_apks_path,
2259      aapt2_path=aapt2_path,
2260      keystore_path=keystore_path,
2261      keystore_password=keystore_password,
2262      keystore_alias=keystore_alias,
2263      system_image_locales=system_image_locales)
2264  _InstallCommand.default_modules = default_modules
2265
2266  parser = argparse.ArgumentParser()
2267  parser.set_defaults(
2268      package_name=package_name,
2269      command_line_flags_file=command_line_flags_file,
2270      proguard_mapping_path=proguard_mapping_path,
2271      target_cpu=target_cpu)
2272  _RunInternal(
2273      parser,
2274      output_directory=output_directory,
2275      additional_apk_paths=additional_apk_paths,
2276      bundle_generation_info=bundle_generation_info)
2277
2278
2279def RunForTestApk(*, output_directory, package_name, test_apk_path,
2280                  test_apk_json, proguard_mapping_path, additional_apk_paths):
2281  """Entry point for generated test apk wrapper scripts.
2282
2283  This is intended to make commands like logcat (with proguard deobfuscation)
2284  available. The run_* scripts should be used to actually run tests.
2285
2286  Args:
2287    output_dir: Chromium output directory path.
2288    package_name: The package name for the test apk.
2289    test_apk_path: The test apk to install.
2290    test_apk_json: The incremental json dict for the test apk.
2291    proguard_mapping_path: Input path to the Proguard mapping file, used to
2292      deobfuscate Java stack traces.
2293    additional_apk_paths: Additional APKs to install.
2294  """
2295  constants.SetOutputDirectory(output_directory)
2296  devil_chromium.Initialize(output_directory=output_directory)
2297
2298  parser = argparse.ArgumentParser()
2299  exists_or_none = lambda p: p if p and os.path.exists(p) else None
2300
2301  parser.set_defaults(apk_path=exists_or_none(test_apk_path),
2302                      incremental_json=exists_or_none(test_apk_json),
2303                      package_name=package_name,
2304                      proguard_mapping_path=proguard_mapping_path)
2305
2306  _RunInternal(parser,
2307               output_directory=output_directory,
2308               additional_apk_paths=additional_apk_paths,
2309               is_test_apk=True)
2310
2311
2312def main():
2313  devil_chromium.Initialize()
2314  _RunInternal(argparse.ArgumentParser())
2315
2316
2317if __name__ == '__main__':
2318  main()
2319