• 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    if stack_script_context:
751      self._print_func = _LogcatProcessor.NativeStackSymbolizer(
752          stack_script_context, self._PrintParsedLine).AddLine
753    else:
754      self._print_func = self._PrintParsedLine
755    # Process ID for the app's main process (with no :name suffix).
756    self._primary_pid = None
757    # Set of all Process IDs that belong to the app.
758    self._my_pids = set()
759    # Set of all Process IDs that we've parsed at some point.
760    self._seen_pids = set()
761    # Start proc 22953:com.google.chromeremotedesktop/
762    self._pid_pattern = re.compile(r'Start proc (\d+):{}/'.format(package_name))
763    # START u0 {act=android.intent.action.MAIN \
764    # cat=[android.intent.category.LAUNCHER] \
765    # flg=0x10000000 pkg=com.google.chromeremotedesktop} from uid 2000
766    self._start_pattern = re.compile(r'START .*(?:cmp|pkg)=' + package_name)
767
768    self.nonce = 'Chromium apk_operations.py nonce={}'.format(random.random())
769    # Holds lines buffered on start-up, before we find our nonce message.
770    self._initial_buffered_lines = []
771    self._UpdateMyPids()
772    # Give preference to PID reported by "ps" over those found from
773    # _start_pattern. There can be multiple "Start proc" messages from prior
774    # runs of the app.
775    self._found_initial_pid = self._primary_pid is not None
776    # Retrieve any additional patterns that are relevant for the User.
777    self._user_defined_highlight = None
778    user_regex = os.environ.get('CHROMIUM_LOGCAT_HIGHLIGHT')
779    if user_regex:
780      self._user_defined_highlight = re.compile(user_regex)
781      if not self._user_defined_highlight:
782        print(_Colorize(
783            'Rejecting invalid regular expression: {}'.format(user_regex),
784            colorama.Fore.RED + colorama.Style.BRIGHT))
785
786  def _UpdateMyPids(self):
787    # We intentionally do not clear self._my_pids to make sure that the
788    # ProcessLine method below also includes lines from processes which may
789    # have already exited.
790    self._primary_pid = None
791    for package_name in [self._package_name] + self._extra_package_names:
792      for process in _GetPackageProcesses(self._device, package_name):
793        # We take only the first "main" process found in order to account for
794        # possibly forked() processes.
795        if ':' not in process.name and self._primary_pid is None:
796          self._primary_pid = process.pid
797        self._my_pids.add(process.pid)
798
799  def _GetPidStyle(self, pid, dim=False):
800    if pid == self._primary_pid:
801      return colorama.Fore.WHITE
802    if pid in self._my_pids:
803      # TODO(wnwen): Use one separate persistent color per process, pop LRU
804      return colorama.Fore.YELLOW
805    if dim:
806      return colorama.Style.DIM
807    return ''
808
809  def _GetPriorityStyle(self, priority, dim=False):
810    # pylint:disable=no-self-use
811    if dim:
812      return ''
813    style = colorama.Fore.BLACK
814    if priority in ('E', 'F'):
815      style += colorama.Back.RED
816    elif priority == 'W':
817      style += colorama.Back.YELLOW
818    elif priority == 'I':
819      style += colorama.Back.GREEN
820    elif priority == 'D':
821      style += colorama.Back.BLUE
822    return style
823
824  def _ParseLine(self, line):
825    tokens = line.split(None, 6)
826
827    def consume_token_or_default(default):
828      return tokens.pop(0) if len(tokens) > 0 else default
829
830    def consume_integer_token_or_default(default):
831      if len(tokens) == 0:
832        return default
833
834      try:
835        return int(tokens.pop(0))
836      except ValueError:
837        return default
838
839    date = consume_token_or_default('')
840    invokation_time = consume_token_or_default('')
841    pid = consume_integer_token_or_default(-1)
842    tid = consume_integer_token_or_default(-1)
843    priority = consume_token_or_default('')
844    tag = consume_token_or_default('')
845    original_message = consume_token_or_default('')
846
847    # Example:
848    #   09-19 06:35:51.113  9060  9154 W GCoreFlp: No location...
849    #   09-19 06:01:26.174  9060 10617 I Auth    : [ReflectiveChannelBinder]...
850    # Parsing "GCoreFlp:" vs "Auth    :", we only want tag to contain the word,
851    # and we don't want to keep the colon for the message.
852    if tag and tag[-1] == ':':
853      tag = tag[:-1]
854    elif len(original_message) > 2:
855      original_message = original_message[2:]
856    return self.ParsedLine(
857        date, invokation_time, pid, tid, priority, tag, original_message)
858
859  def _PrintParsedLine(self, parsed_line, dim=False):
860    if self._exit_on_match and self._exit_on_match.search(parsed_line.message):
861      self._found_exit_match = True
862
863    tid_style = colorama.Style.NORMAL
864    user_match = self._user_defined_highlight and (
865        re.search(self._user_defined_highlight, parsed_line.tag)
866        or re.search(self._user_defined_highlight, parsed_line.message))
867
868    # Make the main thread bright.
869    if not dim and parsed_line.pid == parsed_line.tid:
870      tid_style = colorama.Style.BRIGHT
871    pid_style = self._GetPidStyle(parsed_line.pid, dim)
872    msg_style = pid_style if not user_match else (colorama.Fore.GREEN +
873                                                  colorama.Style.BRIGHT)
874    # We have to pad before adding color as that changes the width of the tag.
875    pid_str = _Colorize('{:5}'.format(parsed_line.pid), pid_style)
876    tid_str = _Colorize('{:5}'.format(parsed_line.tid), tid_style)
877    tag = _Colorize('{:8}'.format(parsed_line.tag),
878                    pid_style + ('' if dim else colorama.Style.BRIGHT))
879    priority = _Colorize(parsed_line.priority,
880                         self._GetPriorityStyle(parsed_line.priority))
881    messages = [parsed_line.message]
882    if self._deobfuscator:
883      messages = self._deobfuscator.TransformLines(messages)
884    for message in messages:
885      message = _Colorize(message, msg_style)
886      sys.stdout.write('{} {} {} {} {} {}: {}\n'.format(
887          parsed_line.date, parsed_line.invokation_time, pid_str, tid_str,
888          priority, tag, message))
889
890  def _TriggerNonceFound(self):
891    # Once the nonce is hit, we have confidence that we know which lines
892    # belong to the current run of the app. Process all of the buffered lines.
893    if self._primary_pid:
894      for args in self._initial_buffered_lines:
895        self._print_func(*args)
896    self._initial_buffered_lines = None
897    self.nonce = None
898
899  def FoundExitMatch(self):
900    return self._found_exit_match
901
902  def ProcessLine(self, line):
903    if not line or line.startswith('------'):
904      return
905
906    if self.nonce and self.nonce in line:
907      self._TriggerNonceFound()
908
909    nonce_found = self.nonce is None
910
911    log = self._ParseLine(line)
912    if log.pid not in self._seen_pids:
913      self._seen_pids.add(log.pid)
914      if nonce_found:
915        # Update list of owned PIDs each time a new PID is encountered.
916        self._UpdateMyPids()
917
918    # Search for "Start proc $pid:$package_name/" message.
919    if not nonce_found:
920      # Capture logs before the nonce. Start with the most recent "am start".
921      if self._start_pattern.match(log.message):
922        self._initial_buffered_lines = []
923
924      # If we didn't find the PID via "ps", then extract it from log messages.
925      # This will happen if the app crashes too quickly.
926      if not self._found_initial_pid:
927        m = self._pid_pattern.match(log.message)
928        if m:
929          # Find the most recent "Start proc" line before the nonce.
930          # Track only the primary pid in this mode.
931          # The main use-case is to find app logs when no current PIDs exist.
932          # E.g.: When the app crashes on launch.
933          self._primary_pid = m.group(1)
934          self._my_pids.clear()
935          self._my_pids.add(m.group(1))
936
937    owned_pid = log.pid in self._my_pids
938    if owned_pid and not self._verbose and log.tag == 'dalvikvm':
939      if self._DALVIK_IGNORE_PATTERN.match(log.message):
940        return
941
942    if owned_pid or self._verbose or (log.priority == 'F' or  # Java crash dump
943                                      log.tag in self._ALLOWLISTED_TAGS):
944      if nonce_found:
945        self._print_func(log, not owned_pid)
946      else:
947        self._initial_buffered_lines.append((log, not owned_pid))
948
949
950def _RunLogcat(device,
951               package_name,
952               stack_script_context,
953               deobfuscate,
954               verbose,
955               exit_on_match=None,
956               extra_package_names=None):
957  logcat_processor = _LogcatProcessor(device,
958                                      package_name,
959                                      stack_script_context,
960                                      deobfuscate,
961                                      verbose,
962                                      exit_on_match=exit_on_match,
963                                      extra_package_names=extra_package_names)
964  device.RunShellCommand(['log', logcat_processor.nonce])
965  for line in device.adb.Logcat(logcat_format='threadtime'):
966    try:
967      logcat_processor.ProcessLine(line)
968      if logcat_processor.FoundExitMatch():
969        return
970    except:
971      sys.stderr.write('Failed to process line: ' + line + '\n')
972      # Skip stack trace for the common case of the adb server being
973      # restarted.
974      if 'unexpected EOF' in line:
975        sys.exit(1)
976      raise
977
978
979def _GetPackageProcesses(device, package_name):
980  my_names = (package_name, package_name + '_zygote')
981  return [
982      p for p in device.ListProcesses(package_name)
983      if p.name in my_names or p.name.startswith(package_name + ':')
984  ]
985
986
987def _RunPs(devices, package_name):
988  parallel_devices = device_utils.DeviceUtils.parallel(devices)
989  all_processes = parallel_devices.pMap(
990      lambda d: _GetPackageProcesses(d, package_name)).pGet(None)
991  for processes in _PrintPerDeviceOutput(devices, all_processes):
992    if not processes:
993      print('No processes found.')
994    else:
995      proc_map = collections.defaultdict(list)
996      for p in processes:
997        proc_map[p.name].append(str(p.pid))
998      for name, pids in sorted(proc_map.items()):
999        print(name, ','.join(pids))
1000
1001
1002def _RunShell(devices, package_name, cmd):
1003  if cmd:
1004    parallel_devices = device_utils.DeviceUtils.parallel(devices)
1005    outputs = parallel_devices.RunShellCommand(
1006        cmd, run_as=package_name).pGet(None)
1007    for output in _PrintPerDeviceOutput(devices, outputs):
1008      for line in output:
1009        print(line)
1010  else:
1011    adb_path = adb_wrapper.AdbWrapper.GetAdbPath()
1012    cmd = [adb_path, '-s', devices[0].serial, 'shell']
1013    # Pre-N devices do not support -t flag.
1014    if devices[0].build_version_sdk >= version_codes.NOUGAT:
1015      cmd += ['-t', 'run-as', package_name]
1016    else:
1017      print('Upon entering the shell, run:')
1018      print('run-as', package_name)
1019      print()
1020    os.execv(adb_path, cmd)
1021
1022
1023def _RunCompileDex(devices, package_name, compilation_filter):
1024  cmd = ['cmd', 'package', 'compile', '-f', '-m', compilation_filter,
1025         package_name]
1026  parallel_devices = device_utils.DeviceUtils.parallel(devices)
1027  outputs = parallel_devices.RunShellCommand(cmd, timeout=120).pGet(None)
1028  for output in _PrintPerDeviceOutput(devices, outputs):
1029    for line in output:
1030      print(line)
1031
1032
1033def _RunProfile(device, package_name, host_build_directory, pprof_out_path,
1034                process_specifier, thread_specifier, events, extra_args):
1035  simpleperf.PrepareDevice(device)
1036  device_simpleperf_path = simpleperf.InstallSimpleperf(device, package_name)
1037  with tempfile.NamedTemporaryFile() as fh:
1038    host_simpleperf_out_path = fh.name
1039
1040    with simpleperf.RunSimpleperf(device, device_simpleperf_path, package_name,
1041                                  process_specifier, thread_specifier,
1042                                  events, extra_args, host_simpleperf_out_path):
1043      sys.stdout.write('Profiler is running; press Enter to stop...\n')
1044      sys.stdin.read(1)
1045      sys.stdout.write('Post-processing data...\n')
1046
1047    simpleperf.ConvertSimpleperfToPprof(host_simpleperf_out_path,
1048                                        host_build_directory, pprof_out_path)
1049    print(textwrap.dedent("""
1050        Profile data written to %(s)s.
1051
1052        To view profile as a call graph in browser:
1053          pprof -web %(s)s
1054
1055        To print the hottest methods:
1056          pprof -top %(s)s
1057
1058        pprof has many useful customization options; `pprof --help` for details.
1059        """ % {'s': pprof_out_path}))
1060
1061
1062class _StackScriptContext:
1063  """Maintains temporary files needed by stack.py."""
1064
1065  def __init__(self,
1066               output_directory,
1067               apk_path,
1068               bundle_generation_info,
1069               quiet=False):
1070    self._output_directory = output_directory
1071    self._apk_path = apk_path
1072    self._bundle_generation_info = bundle_generation_info
1073    self._staging_dir = None
1074    self._quiet = quiet
1075
1076  def _CreateStaging(self):
1077    # In many cases, stack decoding requires APKs to map trace lines to native
1078    # libraries. Create a temporary directory, and either unpack a bundle's
1079    # APKS into it, or simply symlink the standalone APK into it. This
1080    # provides an unambiguous set of APK files for the stack decoding process
1081    # to inspect.
1082    logging.debug('Creating stack staging directory')
1083    self._staging_dir = tempfile.mkdtemp()
1084    bundle_generation_info = self._bundle_generation_info
1085
1086    if bundle_generation_info:
1087      # TODO(wnwen): Use apk_helper instead.
1088      _GenerateBundleApks(bundle_generation_info)
1089      logging.debug('Extracting .apks file')
1090      with zipfile.ZipFile(bundle_generation_info.bundle_apks_path, 'r') as z:
1091        files_to_extract = [
1092            f for f in z.namelist() if f.endswith('-master.apk')
1093        ]
1094        z.extractall(self._staging_dir, files_to_extract)
1095    elif self._apk_path:
1096      # Otherwise an incremental APK and an empty apks directory is correct.
1097      output = os.path.join(self._staging_dir, os.path.basename(self._apk_path))
1098      os.symlink(self._apk_path, output)
1099
1100  def Close(self):
1101    if self._staging_dir:
1102      logging.debug('Clearing stack staging directory')
1103      shutil.rmtree(self._staging_dir)
1104      self._staging_dir = None
1105
1106  def Popen(self, input_file=None, **kwargs):
1107    if self._staging_dir is None:
1108      self._CreateStaging()
1109    stack_script = os.path.join(
1110        constants.host_paths.ANDROID_PLATFORM_DEVELOPMENT_SCRIPTS_PATH,
1111        'stack.py')
1112    cmd = [
1113        stack_script, '--output-directory', self._output_directory,
1114        '--apks-directory', self._staging_dir
1115    ]
1116    if self._quiet:
1117      cmd.append('--quiet')
1118    if input_file:
1119      cmd.append(input_file)
1120    logging.info('Running: %s', shlex.join(cmd))
1121    return subprocess.Popen(cmd, universal_newlines=True, **kwargs)
1122
1123
1124def _GenerateAvailableDevicesMessage(devices):
1125  devices_obj = device_utils.DeviceUtils.parallel(devices)
1126  descriptions = devices_obj.pMap(lambda d: d.build_description).pGet(None)
1127  msg = 'Available devices:\n'
1128  for d, desc in zip(devices, descriptions):
1129    msg += '  %s (%s)\n' % (d, desc)
1130  return msg
1131
1132
1133# TODO(agrieve):add "--all" in the MultipleDevicesError message and use it here.
1134def _GenerateMissingAllFlagMessage(devices):
1135  return ('More than one device available. Use --all to select all devices, ' +
1136          'or use --device to select a device by serial.\n\n' +
1137          _GenerateAvailableDevicesMessage(devices))
1138
1139
1140def _DisplayArgs(devices, command_line_flags_file):
1141  def flags_helper(d):
1142    changer = flag_changer.FlagChanger(d, command_line_flags_file)
1143    return changer.GetCurrentFlags()
1144
1145  parallel_devices = device_utils.DeviceUtils.parallel(devices)
1146  outputs = parallel_devices.pMap(flags_helper).pGet(None)
1147  print('Existing flags per-device (via /data/local/tmp/{}):'.format(
1148      command_line_flags_file))
1149  for flags in _PrintPerDeviceOutput(devices, outputs, single_line=True):
1150    quoted_flags = ' '.join(shlex.quote(f) for f in flags)
1151    print(quoted_flags or 'No flags set.')
1152
1153
1154def _DeviceCachePath(device, output_directory):
1155  file_name = 'device_cache_%s.json' % device.serial
1156  return os.path.join(output_directory, file_name)
1157
1158
1159def _LoadDeviceCaches(devices, output_directory):
1160  if not output_directory:
1161    return
1162  for d in devices:
1163    cache_path = _DeviceCachePath(d, output_directory)
1164    if os.path.exists(cache_path):
1165      logging.debug('Using device cache: %s', cache_path)
1166      with open(cache_path) as f:
1167        d.LoadCacheData(f.read())
1168      # Delete the cached file so that any exceptions cause it to be cleared.
1169      os.unlink(cache_path)
1170    else:
1171      logging.debug('No cache present for device: %s', d)
1172
1173
1174def _SaveDeviceCaches(devices, output_directory):
1175  if not output_directory:
1176    return
1177  for d in devices:
1178    cache_path = _DeviceCachePath(d, output_directory)
1179    with open(cache_path, 'w') as f:
1180      f.write(d.DumpCacheData())
1181      logging.info('Wrote device cache: %s', cache_path)
1182
1183
1184class _Command:
1185  name = None
1186  description = None
1187  long_description = None
1188  needs_package_name = False
1189  needs_output_directory = False
1190  needs_apk_helper = False
1191  supports_incremental = False
1192  accepts_command_line_flags = False
1193  accepts_args = False
1194  need_device_args = True
1195  all_devices_by_default = False
1196  calls_exec = False
1197  supports_multiple_devices = True
1198
1199  def __init__(self, from_wrapper_script, is_bundle, is_test_apk):
1200    self._parser = None
1201    self._from_wrapper_script = from_wrapper_script
1202    self.args = None
1203    self.apk_helper = None
1204    self.additional_apk_helpers = None
1205    self.install_dict = None
1206    self.devices = None
1207    self.is_bundle = is_bundle
1208    self.is_test_apk = is_test_apk
1209    self.bundle_generation_info = None
1210    # Only support  incremental install from APK wrapper scripts.
1211    if is_bundle or not from_wrapper_script:
1212      self.supports_incremental = False
1213
1214  def RegisterBundleGenerationInfo(self, bundle_generation_info):
1215    self.bundle_generation_info = bundle_generation_info
1216
1217  def _RegisterExtraArgs(self, group):
1218    pass
1219
1220  def RegisterArgs(self, parser):
1221    subp = parser.add_parser(
1222        self.name, help=self.description,
1223        description=self.long_description or self.description,
1224        formatter_class=argparse.RawDescriptionHelpFormatter)
1225    self._parser = subp
1226    subp.set_defaults(command=self)
1227    if self.need_device_args:
1228      subp.add_argument('--all',
1229                        action='store_true',
1230                        default=self.all_devices_by_default,
1231                        help='Operate on all connected devices.',)
1232      subp.add_argument('-d',
1233                        '--device',
1234                        action='append',
1235                        default=[],
1236                        dest='devices',
1237                        help='Target device for script to work on. Enter '
1238                            'multiple times for multiple devices.')
1239    subp.add_argument('-v',
1240                      '--verbose',
1241                      action='count',
1242                      default=0,
1243                      dest='verbose_count',
1244                      help='Verbose level (multiple times for more)')
1245    group = subp.add_argument_group('%s arguments' % self.name)
1246
1247    if self.needs_package_name:
1248      # Three cases to consider here, since later code assumes
1249      #  self.args.package_name always exists, even if None:
1250      #
1251      # - Called from a bundle wrapper script, the package_name is already
1252      #   set through parser.set_defaults(), so don't call add_argument()
1253      #   to avoid overriding its value.
1254      #
1255      # - Called from an apk wrapper script. The --package-name argument
1256      #   should not appear, but self.args.package_name will be gleaned from
1257      #   the --apk-path file later.
1258      #
1259      # - Called directly, then --package-name is required on the command-line.
1260      #
1261      if not self.is_bundle:
1262        group.add_argument(
1263            '--package-name',
1264            help=argparse.SUPPRESS if self._from_wrapper_script else (
1265                "App's package name."))
1266
1267    if self.needs_apk_helper or self.needs_package_name:
1268      # Adding this argument to the subparser would override the set_defaults()
1269      # value set by on the parent parser (even if None).
1270      if not self._from_wrapper_script and not self.is_bundle:
1271        group.add_argument(
1272            '--apk-path', required=self.needs_apk_helper, help='Path to .apk')
1273
1274    if self.supports_incremental:
1275      group.add_argument('--incremental',
1276                          action='store_true',
1277                          default=False,
1278                          help='Always install an incremental apk.')
1279      group.add_argument('--non-incremental',
1280                          action='store_true',
1281                          default=False,
1282                          help='Always install a non-incremental apk.')
1283
1284    # accepts_command_line_flags and accepts_args are mutually exclusive.
1285    # argparse will throw if they are both set.
1286    if self.accepts_command_line_flags:
1287      group.add_argument(
1288          '--args', help='Command-line flags. Use = to assign args.')
1289
1290    if self.accepts_args:
1291      group.add_argument(
1292          '--args', help='Extra arguments. Use = to assign args')
1293
1294    if not self._from_wrapper_script and self.accepts_command_line_flags:
1295      # Provided by wrapper scripts.
1296      group.add_argument(
1297          '--command-line-flags-file',
1298          help='Name of the command-line flags file')
1299
1300    self._RegisterExtraArgs(group)
1301
1302  def _CreateApkHelpers(self, args, incremental_apk_path, install_dict):
1303    """Returns true iff self.apk_helper was created and assigned."""
1304    if self.apk_helper is None:
1305      if args.apk_path:
1306        self.apk_helper = apk_helper.ToHelper(args.apk_path)
1307      elif incremental_apk_path:
1308        self.install_dict = install_dict
1309        self.apk_helper = apk_helper.ToHelper(incremental_apk_path)
1310      elif self.is_bundle:
1311        _GenerateBundleApks(self.bundle_generation_info)
1312        self.apk_helper = apk_helper.ToHelper(
1313            self.bundle_generation_info.bundle_apks_path)
1314    if args.additional_apk_paths and self.additional_apk_helpers is None:
1315      self.additional_apk_helpers = [
1316          apk_helper.ToHelper(apk_path)
1317          for apk_path in args.additional_apk_paths
1318      ]
1319    return self.apk_helper is not None
1320
1321  def _FindSupportedDevices(self, devices):
1322    """Returns supported devices and reasons for each not supported one."""
1323    app_abis = self.apk_helper.GetAbis()
1324    calling_script_name = os.path.basename(sys.argv[0])
1325    is_webview = 'webview' in calling_script_name
1326    requires_32_bit = self.apk_helper.Get32BitAbiOverride() == '0xffffffff'
1327    logging.debug('App supports (requires 32bit: %r, is webview: %r): %r',
1328                  requires_32_bit, is_webview, app_abis)
1329    # Webview 32_64 targets can work even on 64-bit only devices since only the
1330    # webview library in the target needs the correct bitness.
1331    if requires_32_bit and not is_webview:
1332      app_abis = [abi for abi in app_abis if '64' not in abi]
1333      logging.debug('App supports (filtered): %r', app_abis)
1334    if not app_abis:
1335      # The app does not have any native libs, so all devices can support it.
1336      return devices, {}
1337    fully_supported = []
1338    not_supported_reasons = {}
1339    for device in devices:
1340      device_abis = device.GetSupportedABIs()
1341      device_primary_abi = device_abis[0]
1342      logging.debug('Device primary: %s', device_primary_abi)
1343      logging.debug('Device supports: %r', device_abis)
1344
1345      # x86/x86_64 emulators sometimes advertises arm support but arm builds do
1346      # not work on them. Thus these non-functional ABIs need to be filtered out
1347      # here to avoid resulting in hard to understand runtime failures.
1348      if device_primary_abi in ('x86', 'x86_64'):
1349        device_abis = [abi for abi in device_abis if not abi.startswith('arm')]
1350        logging.debug('Device supports (filtered): %r', device_abis)
1351
1352      if any(abi in app_abis for abi in device_abis):
1353        fully_supported.append(device)
1354      else:  # No common supported ABIs between the device and app.
1355        if device_primary_abi == 'x86':
1356          target_cpu = 'x86'
1357        elif device_primary_abi == 'x86_64':
1358          target_cpu = 'x64'
1359        elif device_primary_abi.startswith('arm64'):
1360          target_cpu = 'arm64'
1361        elif device_primary_abi.startswith('armeabi'):
1362          target_cpu = 'arm'
1363        else:
1364          target_cpu = '<something else>'
1365        # pylint: disable=line-too-long
1366        native_lib_link = 'https://chromium.googlesource.com/chromium/src/+/main/docs/android_native_libraries.md'
1367        not_supported_reasons[device.serial] = (
1368            f"none of the app's ABIs ({','.join(app_abis)}) match this "
1369            f"device's ABIs ({','.join(device_abis)}), you may need to set "
1370            f'target_cpu="{target_cpu}" in your args.gn. If you already set '
1371            'the target_cpu arg, you may need to use one of the _64 or _64_32 '
1372            f'targets, see {native_lib_link} for more details.')
1373    return fully_supported, not_supported_reasons
1374
1375  def ProcessArgs(self, args):
1376    self.args = args
1377    # Ensure these keys always exist. They are set by wrapper scripts, but not
1378    # always added when not using wrapper scripts.
1379    args.__dict__.setdefault('apk_path', None)
1380    args.__dict__.setdefault('incremental_json', None)
1381
1382    incremental_apk_path = None
1383    install_dict = None
1384    if args.incremental_json and not (self.supports_incremental and
1385                                      args.non_incremental):
1386      with open(args.incremental_json) as f:
1387        install_dict = json.load(f)
1388        incremental_apk_path = os.path.join(args.output_directory,
1389                                            install_dict['apk_path'])
1390        if not os.path.exists(incremental_apk_path):
1391          incremental_apk_path = None
1392
1393    if self.supports_incremental:
1394      if args.incremental and args.non_incremental:
1395        self._parser.error('Must use only one of --incremental and '
1396                           '--non-incremental')
1397      elif args.non_incremental:
1398        if not args.apk_path:
1399          self._parser.error('Apk has not been built.')
1400      elif args.incremental:
1401        if not incremental_apk_path:
1402          self._parser.error('Incremental apk has not been built.')
1403        args.apk_path = None
1404
1405      if args.apk_path and incremental_apk_path:
1406        self._parser.error('Both incremental and non-incremental apks exist. '
1407                           'Select using --incremental or --non-incremental')
1408
1409
1410    # Gate apk_helper creation with _CreateApkHelpers since for bundles it takes
1411    # a while to unpack the apks file from the aab file, so avoid this slowdown
1412    # for simple commands that don't need apk_helper.
1413    if self.needs_apk_helper:
1414      if not self._CreateApkHelpers(args, incremental_apk_path, install_dict):
1415        self._parser.error('App is not built.')
1416
1417    if self.needs_package_name and not args.package_name:
1418      if self._CreateApkHelpers(args, incremental_apk_path, install_dict):
1419        args.package_name = self.apk_helper.GetPackageName()
1420      elif self._from_wrapper_script:
1421        self._parser.error('App is not built.')
1422      else:
1423        self._parser.error('One of --package-name or --apk-path is required.')
1424
1425    self.devices = []
1426    if self.need_device_args:
1427      # Avoid filtering by ABIs with catapult since some x86 or x86_64 emulators
1428      # can still work with the right target_cpu GN arg and the right targets.
1429      # Doing this manually allows us to output more informative warnings to
1430      # help devs towards the right course, see: https://crbug.com/1335139
1431      available_devices = device_utils.DeviceUtils.HealthyDevices(
1432          device_arg=args.devices,
1433          enable_device_files_cache=bool(args.output_directory),
1434          default_retries=0)
1435      if not available_devices:
1436        raise Exception('Cannot find any available devices.')
1437
1438      if not self._CreateApkHelpers(args, incremental_apk_path, install_dict):
1439        self.devices = available_devices
1440      else:
1441        fully_supported, not_supported_reasons = self._FindSupportedDevices(
1442            available_devices)
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        if args.devices:
1448          if reason_string:
1449            logging.warning('Devices not supported: %s', reason_string)
1450          self.devices = available_devices
1451        elif fully_supported:
1452          self.devices = fully_supported
1453        else:
1454          raise Exception('Cannot find any supported devices for this app.\n\n'
1455                          f'{reason_string}')
1456
1457      # TODO(agrieve): Device cache should not depend on output directory.
1458      #     Maybe put into /tmp?
1459      _LoadDeviceCaches(self.devices, args.output_directory)
1460
1461      try:
1462        if len(self.devices) > 1:
1463          if not self.supports_multiple_devices:
1464            self._parser.error(device_errors.MultipleDevicesError(self.devices))
1465          if not args.all and not args.devices:
1466            self._parser.error(_GenerateMissingAllFlagMessage(self.devices))
1467        # Save cache now if command will not get a chance to afterwards.
1468        if self.calls_exec:
1469          _SaveDeviceCaches(self.devices, args.output_directory)
1470      except:
1471        _SaveDeviceCaches(self.devices, args.output_directory)
1472        raise
1473
1474
1475class _DevicesCommand(_Command):
1476  name = 'devices'
1477  description = 'Describe attached devices.'
1478  all_devices_by_default = True
1479
1480  def Run(self):
1481    print(_GenerateAvailableDevicesMessage(self.devices))
1482
1483
1484class _PackageInfoCommand(_Command):
1485  name = 'package-info'
1486  description = 'Show various attributes of this app.'
1487  need_device_args = False
1488  needs_package_name = True
1489  needs_apk_helper = True
1490
1491  def Run(self):
1492    # Format all (even ints) as strings, to handle cases where APIs return None
1493    print('Package name: "%s"' % self.args.package_name)
1494    print('versionCode: %s' % self.apk_helper.GetVersionCode())
1495    print('versionName: "%s"' % self.apk_helper.GetVersionName())
1496    print('minSdkVersion: %s' % self.apk_helper.GetMinSdkVersion())
1497    print('targetSdkVersion: %s' % self.apk_helper.GetTargetSdkVersion())
1498    print('Supported ABIs: %r' % self.apk_helper.GetAbis())
1499
1500
1501class _InstallCommand(_Command):
1502  name = 'install'
1503  description = 'Installs the APK or bundle to one or more devices.'
1504  needs_apk_helper = True
1505  supports_incremental = True
1506  default_modules = []
1507
1508  def _RegisterExtraArgs(self, group):
1509    if self.is_bundle:
1510      group.add_argument(
1511          '-m',
1512          '--module',
1513          action='append',
1514          default=self.default_modules,
1515          help='Module to install. Can be specified multiple times.')
1516      group.add_argument(
1517          '-f',
1518          '--fake',
1519          action='append',
1520          default=[],
1521          help='Fake bundle module install. Can be specified multiple times. '
1522          'Requires \'-m {0}\' to be given, and \'-f {0}\' is illegal.'.format(
1523              BASE_MODULE))
1524      # Add even if |self.default_modules| is empty, for consistency.
1525      group.add_argument('--no-module',
1526                         action='append',
1527                         choices=self.default_modules,
1528                         default=[],
1529                         help='Module to exclude from default install.')
1530
1531  def Run(self):
1532    if self.additional_apk_helpers:
1533      for additional_apk_helper in self.additional_apk_helpers:
1534        _InstallApk(self.devices, additional_apk_helper, None)
1535    if self.is_bundle:
1536      modules = list(
1537          set(self.args.module) - set(self.args.no_module) -
1538          set(self.args.fake))
1539      _InstallBundle(self.devices, self.apk_helper, modules, self.args.fake)
1540    else:
1541      _InstallApk(self.devices, self.apk_helper, self.install_dict)
1542
1543
1544class _UninstallCommand(_Command):
1545  name = 'uninstall'
1546  description = 'Removes the APK or bundle from one or more devices.'
1547  needs_package_name = True
1548
1549  def Run(self):
1550    _UninstallApk(self.devices, self.install_dict, self.args.package_name)
1551
1552
1553class _SetWebViewProviderCommand(_Command):
1554  name = 'set-webview-provider'
1555  description = ("Sets the device's WebView provider to this APK's "
1556                 "package name.")
1557  needs_package_name = True
1558  needs_apk_helper = True
1559
1560  def Run(self):
1561    if not _IsWebViewProvider(self.apk_helper):
1562      raise Exception('This package does not have a WebViewLibrary meta-data '
1563                      'tag. Are you sure it contains a WebView implementation?')
1564    _SetWebViewProvider(self.devices, self.args.package_name)
1565
1566
1567class _LaunchCommand(_Command):
1568  name = 'launch'
1569  description = ('Sends a launch intent for the APK or bundle after first '
1570                 'writing the command-line flags file.')
1571  needs_package_name = True
1572  accepts_command_line_flags = True
1573  all_devices_by_default = True
1574
1575  def _RegisterExtraArgs(self, group):
1576    group.add_argument('-w', '--wait-for-java-debugger', action='store_true',
1577                       help='Pause execution until debugger attaches. Applies '
1578                            'only to the main process. To have renderers wait, '
1579                            'use --args="--renderer-wait-for-java-debugger"')
1580    group.add_argument('--debug-process-name',
1581                       help='Name of the process to debug. '
1582                            'E.g. "privileged_process0", or "foo.bar:baz"')
1583    group.add_argument('--nokill', action='store_true',
1584                       help='Do not set the debug-app, nor set command-line '
1585                            'flags. Useful to load a URL without having the '
1586                             'app restart.')
1587    group.add_argument('url', nargs='?', help='A URL to launch with.')
1588
1589  def Run(self):
1590    if self.is_test_apk:
1591      raise Exception('Use the bin/run_* scripts to run test apks.')
1592    _LaunchUrl(self.devices,
1593               self.args.package_name,
1594               argv=self.args.args,
1595               command_line_flags_file=self.args.command_line_flags_file,
1596               url=self.args.url,
1597               wait_for_java_debugger=self.args.wait_for_java_debugger,
1598               debug_process_name=self.args.debug_process_name,
1599               nokill=self.args.nokill)
1600
1601
1602class _StopCommand(_Command):
1603  name = 'stop'
1604  description = 'Force-stops the app.'
1605  needs_package_name = True
1606  all_devices_by_default = True
1607
1608  def Run(self):
1609    device_utils.DeviceUtils.parallel(self.devices).ForceStop(
1610        self.args.package_name)
1611
1612
1613class _ClearDataCommand(_Command):
1614  name = 'clear-data'
1615  descriptions = 'Clears all app data.'
1616  needs_package_name = True
1617  all_devices_by_default = True
1618
1619  def Run(self):
1620    device_utils.DeviceUtils.parallel(self.devices).ClearApplicationState(
1621        self.args.package_name)
1622
1623
1624class _ArgvCommand(_Command):
1625  name = 'argv'
1626  description = 'Display and optionally update command-line flags file.'
1627  needs_package_name = True
1628  accepts_command_line_flags = True
1629  all_devices_by_default = True
1630
1631  def Run(self):
1632    _ChangeFlags(self.devices, self.args.args,
1633                 self.args.command_line_flags_file)
1634
1635
1636class _GdbCommand(_Command):
1637  name = 'gdb'
1638  description = 'Runs //build/android/adb_gdb with apk-specific args.'
1639  long_description = description + """
1640
1641To attach to a process other than the APK's main process, use --pid=1234.
1642To list all PIDs, use the "ps" command.
1643
1644If no apk process is currently running, sends a launch intent.
1645"""
1646  needs_package_name = True
1647  needs_output_directory = True
1648  calls_exec = True
1649  supports_multiple_devices = False
1650
1651  def Run(self):
1652    _RunGdb(self.devices[0], self.args.package_name,
1653            self.args.debug_process_name, self.args.pid,
1654            self.args.output_directory, self.args.target_cpu, self.args.port,
1655            self.args.ide, bool(self.args.verbose_count))
1656
1657  def _RegisterExtraArgs(self, group):
1658    pid_group = group.add_mutually_exclusive_group()
1659    pid_group.add_argument('--debug-process-name',
1660                           help='Name of the process to attach to. '
1661                                'E.g. "privileged_process0", or "foo.bar:baz"')
1662    pid_group.add_argument('--pid',
1663                           help='The process ID to attach to. Defaults to '
1664                                'the main process for the package.')
1665    group.add_argument('--ide', action='store_true',
1666                       help='Rather than enter a gdb prompt, set up the '
1667                            'gdb connection and wait for an IDE to '
1668                            'connect.')
1669    # Same default port that ndk-gdb.py uses.
1670    group.add_argument('--port', type=int, default=5039,
1671                       help='Use the given port for the GDB connection')
1672
1673
1674class _LldbCommand(_Command):
1675  name = 'lldb'
1676  description = 'Runs //build/android/connect_lldb.sh with apk-specific args.'
1677  long_description = description + """
1678
1679To attach to a process other than the APK's main process, use --pid=1234.
1680To list all PIDs, use the "ps" command.
1681
1682If no apk process is currently running, sends a launch intent.
1683"""
1684  needs_package_name = True
1685  needs_output_directory = True
1686  calls_exec = True
1687  supports_multiple_devices = False
1688
1689  def Run(self):
1690    _RunLldb(device=self.devices[0],
1691             package_name=self.args.package_name,
1692             debug_process_name=self.args.debug_process_name,
1693             pid=self.args.pid,
1694             output_directory=self.args.output_directory,
1695             port=self.args.port,
1696             target_cpu=self.args.target_cpu,
1697             ndk_dir=self.args.ndk_dir,
1698             lldb_server=self.args.lldb_server,
1699             lldb=self.args.lldb,
1700             verbose=bool(self.args.verbose_count))
1701
1702  def _RegisterExtraArgs(self, group):
1703    pid_group = group.add_mutually_exclusive_group()
1704    pid_group.add_argument('--debug-process-name',
1705                           help='Name of the process to attach to. '
1706                           'E.g. "privileged_process0", or "foo.bar:baz"')
1707    pid_group.add_argument('--pid',
1708                           help='The process ID to attach to. Defaults to '
1709                           'the main process for the package.')
1710    group.add_argument('--ndk-dir',
1711                       help='Select alternative NDK root directory.')
1712    group.add_argument('--lldb-server',
1713                       help='Select alternative on-device lldb-server.')
1714    group.add_argument('--lldb', help='Select alternative client lldb.sh.')
1715    # Same default port that ndk-gdb.py uses.
1716    group.add_argument('--port',
1717                       type=int,
1718                       default=5039,
1719                       help='Use the given port for the LLDB connection')
1720
1721
1722class _LogcatCommand(_Command):
1723  name = 'logcat'
1724  description = 'Runs "adb logcat" with filters relevant the current APK.'
1725  long_description = description + """
1726
1727"Relevant filters" means:
1728  * Log messages from processes belonging to the apk,
1729  * Plus log messages from log tags: ActivityManager|DEBUG,
1730  * Plus fatal logs from any process,
1731  * Minus spamy dalvikvm logs (for pre-L devices).
1732
1733Colors:
1734  * Primary process is white
1735  * Other processes (gpu, renderer) are yellow
1736  * Non-apk processes are grey
1737  * UI thread has a bolded Thread-ID
1738
1739Java stack traces are detected and deobfuscated (for release builds).
1740
1741To disable filtering, (but keep coloring), use --verbose.
1742"""
1743  needs_package_name = True
1744  supports_multiple_devices = False
1745
1746  def Run(self):
1747    deobfuscate = None
1748    if self.args.proguard_mapping_path and not self.args.no_deobfuscate:
1749      deobfuscate = deobfuscator.Deobfuscator(self.args.proguard_mapping_path)
1750
1751    if self.args.apk_path or self.bundle_generation_info:
1752      stack_script_context = _StackScriptContext(self.args.output_directory,
1753                                                 self.args.apk_path,
1754                                                 self.bundle_generation_info,
1755                                                 quiet=True)
1756    else:
1757      stack_script_context = None
1758
1759    extra_package_names = []
1760    if self.is_test_apk and self.additional_apk_helpers:
1761      for additional_apk_helper in self.additional_apk_helpers:
1762        extra_package_names.append(additional_apk_helper.GetPackageName())
1763
1764    try:
1765      _RunLogcat(self.devices[0],
1766                 self.args.package_name,
1767                 stack_script_context,
1768                 deobfuscate,
1769                 bool(self.args.verbose_count),
1770                 self.args.exit_on_match,
1771                 extra_package_names=extra_package_names)
1772    except KeyboardInterrupt:
1773      pass  # Don't show stack trace upon Ctrl-C
1774    finally:
1775      if stack_script_context:
1776        stack_script_context.Close()
1777      if deobfuscate:
1778        deobfuscate.Close()
1779
1780  def _RegisterExtraArgs(self, group):
1781    if self._from_wrapper_script:
1782      group.add_argument('--no-deobfuscate', action='store_true',
1783          help='Disables ProGuard deobfuscation of logcat.')
1784    else:
1785      group.set_defaults(no_deobfuscate=False)
1786      group.add_argument('--proguard-mapping-path',
1787          help='Path to ProGuard map (enables deobfuscation)')
1788    group.add_argument('--exit-on-match',
1789                       help='Exits logcat when a message matches this regex.')
1790
1791
1792class _PsCommand(_Command):
1793  name = 'ps'
1794  description = 'Show PIDs of any APK processes currently running.'
1795  needs_package_name = True
1796  all_devices_by_default = True
1797
1798  def Run(self):
1799    _RunPs(self.devices, self.args.package_name)
1800
1801
1802class _DiskUsageCommand(_Command):
1803  name = 'disk-usage'
1804  description = 'Show how much device storage is being consumed by the app.'
1805  needs_package_name = True
1806  all_devices_by_default = True
1807
1808  def Run(self):
1809    _RunDiskUsage(self.devices, self.args.package_name)
1810
1811
1812class _MemUsageCommand(_Command):
1813  name = 'mem-usage'
1814  description = 'Show memory usage of currently running APK processes.'
1815  needs_package_name = True
1816  all_devices_by_default = True
1817
1818  def _RegisterExtraArgs(self, group):
1819    group.add_argument('--query-app', action='store_true',
1820        help='Do not add --local to "dumpsys meminfo". This will output '
1821             'additional metrics (e.g. Context count), but also cause memory '
1822             'to be used in order to gather the metrics.')
1823
1824  def Run(self):
1825    _RunMemUsage(self.devices, self.args.package_name,
1826                 query_app=self.args.query_app)
1827
1828
1829class _ShellCommand(_Command):
1830  name = 'shell'
1831  description = ('Same as "adb shell <command>", but runs as the apk\'s uid '
1832                 '(via run-as). Useful for inspecting the app\'s data '
1833                 'directory.')
1834  needs_package_name = True
1835
1836  @property
1837  def calls_exec(self):
1838    return not self.args.cmd
1839
1840  @property
1841  def supports_multiple_devices(self):
1842    return not self.args.cmd
1843
1844  def _RegisterExtraArgs(self, group):
1845    group.add_argument(
1846        'cmd', nargs=argparse.REMAINDER, help='Command to run.')
1847
1848  def Run(self):
1849    _RunShell(self.devices, self.args.package_name, self.args.cmd)
1850
1851
1852class _CompileDexCommand(_Command):
1853  name = 'compile-dex'
1854  description = ('Applicable only for Android N+. Forces .odex files to be '
1855                 'compiled with the given compilation filter. To see existing '
1856                 'filter, use "disk-usage" command.')
1857  needs_package_name = True
1858  all_devices_by_default = True
1859
1860  def _RegisterExtraArgs(self, group):
1861    group.add_argument(
1862        'compilation_filter',
1863        choices=['verify', 'quicken', 'space-profile', 'space',
1864                 'speed-profile', 'speed'],
1865        help='For WebView/Monochrome, use "speed". For other apks, use '
1866             '"speed-profile".')
1867
1868  def Run(self):
1869    _RunCompileDex(self.devices, self.args.package_name,
1870                   self.args.compilation_filter)
1871
1872
1873class _PrintCertsCommand(_Command):
1874  name = 'print-certs'
1875  description = 'Print info about certificates used to sign this APK.'
1876  need_device_args = False
1877  needs_apk_helper = True
1878
1879  def _RegisterExtraArgs(self, group):
1880    group.add_argument(
1881        '--full-cert',
1882        action='store_true',
1883        help=("Print the certificate's full signature, Base64-encoded. "
1884              "Useful when configuring an Android image's "
1885              "config_webview_packages.xml."))
1886
1887  def Run(self):
1888    keytool = os.path.join(_JAVA_HOME, 'bin', 'keytool')
1889    pem_certificate_pattern = re.compile(
1890        r'-+BEGIN CERTIFICATE-+([\r\n0-9A-Za-z+/=]+)-+END CERTIFICATE-+[\r\n]*')
1891    if self.is_bundle:
1892      # Bundles are not signed until converted to .apks. The wrapper scripts
1893      # record which key will be used to sign though.
1894      with tempfile.NamedTemporaryFile() as f:
1895        logging.warning('Bundles are not signed until turned into .apk files.')
1896        logging.warning('Showing signing info based on associated keystore.')
1897        cmd = [
1898            keytool, '-exportcert', '-keystore',
1899            self.bundle_generation_info.keystore_path, '-storepass',
1900            self.bundle_generation_info.keystore_password, '-alias',
1901            self.bundle_generation_info.keystore_alias, '-file', f.name
1902        ]
1903        logging.warning('Running: %s', shlex.join(cmd))
1904        subprocess.check_output(cmd, stderr=subprocess.STDOUT)
1905        cmd = [keytool, '-printcert', '-file', f.name]
1906        logging.warning('Running: %s', shlex.join(cmd))
1907        subprocess.check_call(cmd)
1908        if self.args.full_cert:
1909          # Redirect stderr to hide a keytool warning about using non-standard
1910          # keystore format.
1911          cmd += ['-rfc']
1912          logging.warning('Running: %s', shlex.join(cmd))
1913          pem_encoded_certificate = subprocess.check_output(
1914              cmd, stderr=subprocess.STDOUT).decode()
1915    else:
1916
1917      def run_apksigner(min_sdk_version):
1918        cmd = [
1919            build_tools.GetPath('apksigner'), 'verify', '--min-sdk-version',
1920            str(min_sdk_version), '--print-certs-pem', '--verbose',
1921            self.apk_helper.path
1922        ]
1923        logging.warning('Running: %s', shlex.join(cmd))
1924        env = os.environ.copy()
1925        env['PATH'] = os.path.pathsep.join(
1926            [os.path.join(_JAVA_HOME, 'bin'),
1927             env.get('PATH')])
1928        # Redirect stderr to hide verification failures (see explanation below).
1929        return subprocess.check_output(cmd,
1930                                       env=env,
1931                                       universal_newlines=True,
1932                                       stderr=subprocess.STDOUT)
1933
1934      # apksigner's default behavior is nonintuitive: it will print "Verified
1935      # using <scheme number>...: false" for any scheme which is obsolete for
1936      # the APK's minSdkVersion even if it actually was signed with that scheme
1937      # (ex. it prints "Verified using v1 scheme: false" for Monochrome because
1938      # v1 was obsolete by N). To workaround this, we force apksigner to use the
1939      # lowest possible minSdkVersion. We need to fallback to higher
1940      # minSdkVersions in case the APK fails to verify for that minSdkVersion
1941      # (which means the APK is genuinely not signed with that scheme). These
1942      # SDK values are the highest SDK version before the next scheme is
1943      # available:
1944      versions = [
1945          version_codes.MARSHMALLOW,  # before v2 launched in N
1946          version_codes.OREO_MR1,  # before v3 launched in P
1947          version_codes.Q,  # before v4 launched in R
1948          version_codes.R,
1949      ]
1950      stdout = None
1951      for min_sdk_version in versions:
1952        try:
1953          stdout = run_apksigner(min_sdk_version)
1954          break
1955        except subprocess.CalledProcessError:
1956          # Doesn't verify with this min-sdk-version, so try again with a higher
1957          # one
1958          continue
1959      if not stdout:
1960        raise RuntimeError('apksigner was not able to verify APK')
1961
1962      # Separate what the '--print-certs' flag would output vs. the additional
1963      # signature output included by '--print-certs-pem'. The additional PEM
1964      # output is only printed when self.args.full_cert is specified.
1965      verification_hash_info = pem_certificate_pattern.sub('', stdout)
1966      print(verification_hash_info)
1967      if self.args.full_cert:
1968        m = pem_certificate_pattern.search(stdout)
1969        if not m:
1970          raise Exception('apksigner did not print a certificate')
1971        pem_encoded_certificate = m.group(0)
1972
1973
1974    if self.args.full_cert:
1975      m = pem_certificate_pattern.search(pem_encoded_certificate)
1976      if not m:
1977        raise Exception(
1978            'Unable to parse certificate:\n{}'.format(pem_encoded_certificate))
1979      signature = re.sub(r'[\r\n]+', '', m.group(1))
1980      print()
1981      print('Full Signature:')
1982      print(signature)
1983
1984
1985class _ProfileCommand(_Command):
1986  name = 'profile'
1987  description = ('Run the simpleperf sampling CPU profiler on the currently-'
1988                 'running APK. If --args is used, the extra arguments will be '
1989                 'passed on to simpleperf; otherwise, the following default '
1990                 'arguments are used: -g -f 1000 -o /data/local/tmp/perf.data')
1991  needs_package_name = True
1992  needs_output_directory = True
1993  supports_multiple_devices = False
1994  accepts_args = True
1995
1996  def _RegisterExtraArgs(self, group):
1997    group.add_argument(
1998        '--profile-process', default='browser',
1999        help=('Which process to profile. This may be a process name or pid '
2000              'such as you would get from running `%s ps`; or '
2001              'it can be one of (browser, renderer, gpu).' % sys.argv[0]))
2002    group.add_argument(
2003        '--profile-thread', default=None,
2004        help=('(Optional) Profile only a single thread. This may be either a '
2005              'thread ID such as you would get by running `adb shell ps -t` '
2006              '(pre-Oreo) or `adb shell ps -e -T` (Oreo and later); or it may '
2007              'be one of (io, compositor, main, render), in which case '
2008              '--profile-process is also required. (Note that "render" thread '
2009              'refers to a thread in the browser process that manages a '
2010              'renderer; to profile the main thread of the renderer process, '
2011              'use --profile-thread=main).'))
2012    group.add_argument('--profile-output', default='profile.pb',
2013                       help='Output file for profiling data')
2014    group.add_argument('--profile-events', default='cpu-cycles',
2015                      help=('A comma separated list of perf events to capture '
2016                      '(e.g. \'cpu-cycles,branch-misses\'). Run '
2017                      '`simpleperf list` on your device to see available '
2018                      'events.'))
2019
2020  def Run(self):
2021    extra_args = shlex.split(self.args.args or '')
2022    _RunProfile(self.devices[0], self.args.package_name,
2023                self.args.output_directory, self.args.profile_output,
2024                self.args.profile_process, self.args.profile_thread,
2025                self.args.profile_events, extra_args)
2026
2027
2028class _RunCommand(_InstallCommand, _LaunchCommand, _LogcatCommand):
2029  name = 'run'
2030  description = 'Install, launch, and show logcat (when targeting one device).'
2031  all_devices_by_default = False
2032  supports_multiple_devices = True
2033
2034  def _RegisterExtraArgs(self, group):
2035    _InstallCommand._RegisterExtraArgs(self, group)
2036    _LaunchCommand._RegisterExtraArgs(self, group)
2037    _LogcatCommand._RegisterExtraArgs(self, group)
2038    group.add_argument('--no-logcat', action='store_true',
2039                       help='Install and launch, but do not enter logcat.')
2040
2041  def Run(self):
2042    if self.is_test_apk:
2043      raise Exception('Use the bin/run_* scripts to run test apks.')
2044    logging.warning('Installing...')
2045    _InstallCommand.Run(self)
2046    logging.warning('Sending launch intent...')
2047    _LaunchCommand.Run(self)
2048    if len(self.devices) == 1 and not self.args.no_logcat:
2049      logging.warning('Entering logcat...')
2050      _LogcatCommand.Run(self)
2051
2052
2053class _BuildBundleApks(_Command):
2054  name = 'build-bundle-apks'
2055  description = ('Build the .apks archive from an Android app bundle, and '
2056                 'optionally copy it to a specific destination.')
2057  need_device_args = False
2058
2059  def _RegisterExtraArgs(self, group):
2060    group.add_argument(
2061        '--output-apks', required=True, help='Destination path for .apks file.')
2062    group.add_argument(
2063        '--minimal',
2064        action='store_true',
2065        help='Build .apks archive that targets the bundle\'s minSdkVersion and '
2066        'contains only english splits. It still contains optional splits.')
2067    group.add_argument(
2068        '--sdk-version', help='The sdkVersion to build the .apks for.')
2069    group.add_argument(
2070        '--build-mode',
2071        choices=app_bundle_utils.BUILD_APKS_MODES,
2072        help='Specify which type of APKs archive to build. "default" '
2073        'generates regular splits, "universal" generates an archive with a '
2074        'single universal APK, "system" generates an archive with a system '
2075        'image APK, while "system_compressed" generates a compressed system '
2076        'APK, with an additional stub APK for the system image.')
2077    group.add_argument(
2078        '--optimize-for',
2079        choices=app_bundle_utils.OPTIMIZE_FOR_OPTIONS,
2080        help='Override split configuration.')
2081
2082  def Run(self):
2083    _GenerateBundleApks(
2084        self.bundle_generation_info,
2085        output_path=self.args.output_apks,
2086        minimal=self.args.minimal,
2087        minimal_sdk_version=self.args.sdk_version,
2088        mode=self.args.build_mode,
2089        optimize_for=self.args.optimize_for)
2090
2091
2092class _ManifestCommand(_Command):
2093  name = 'dump-manifest'
2094  description = 'Dump the android manifest as XML, to stdout.'
2095  need_device_args = False
2096  needs_apk_helper = True
2097
2098  def Run(self):
2099    if self.is_bundle:
2100      sys.stdout.write(
2101          bundletool.RunBundleTool([
2102              'dump', 'manifest', '--bundle',
2103              self.bundle_generation_info.bundle_path
2104          ]))
2105    else:
2106      apkanalyzer = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'android_sdk',
2107                                 'public', 'cmdline-tools', 'latest', 'bin',
2108                                 'apkanalyzer')
2109      cmd = [apkanalyzer, 'manifest', 'print', self.apk_helper.path]
2110      logging.info('Running: %s', shlex.join(cmd))
2111      subprocess.check_call(cmd)
2112
2113
2114class _StackCommand(_Command):
2115  name = 'stack'
2116  description = 'Decodes an Android stack.'
2117  need_device_args = False
2118
2119  def _RegisterExtraArgs(self, group):
2120    group.add_argument(
2121        'file',
2122        nargs='?',
2123        help='File to decode. If not specified, stdin is processed.')
2124
2125  def Run(self):
2126    context = _StackScriptContext(self.args.output_directory,
2127                                  self.args.apk_path,
2128                                  self.bundle_generation_info)
2129    try:
2130      proc = context.Popen(input_file=self.args.file)
2131      if proc.wait():
2132        raise Exception('stack script returned {}'.format(proc.returncode))
2133    finally:
2134      context.Close()
2135
2136
2137# Shared commands for regular APKs and app bundles.
2138_COMMANDS = [
2139    _DevicesCommand,
2140    _PackageInfoCommand,
2141    _InstallCommand,
2142    _UninstallCommand,
2143    _SetWebViewProviderCommand,
2144    _LaunchCommand,
2145    _StopCommand,
2146    _ClearDataCommand,
2147    _ArgvCommand,
2148    _GdbCommand,
2149    _LldbCommand,
2150    _LogcatCommand,
2151    _PsCommand,
2152    _DiskUsageCommand,
2153    _MemUsageCommand,
2154    _ShellCommand,
2155    _CompileDexCommand,
2156    _PrintCertsCommand,
2157    _ProfileCommand,
2158    _RunCommand,
2159    _StackCommand,
2160    _ManifestCommand,
2161]
2162
2163# Commands specific to app bundles.
2164_BUNDLE_COMMANDS = [
2165    _BuildBundleApks,
2166]
2167
2168
2169def _ParseArgs(parser, from_wrapper_script, is_bundle, is_test_apk):
2170  subparsers = parser.add_subparsers()
2171  command_list = _COMMANDS + (_BUNDLE_COMMANDS if is_bundle else [])
2172  commands = [
2173      clazz(from_wrapper_script, is_bundle, is_test_apk)
2174      for clazz in command_list
2175  ]
2176
2177  for command in commands:
2178    if from_wrapper_script or not command.needs_output_directory:
2179      command.RegisterArgs(subparsers)
2180
2181  # Show extended help when no command is passed.
2182  argv = sys.argv[1:]
2183  if not argv:
2184    argv = ['--help']
2185
2186  return parser.parse_args(argv)
2187
2188
2189def _RunInternal(parser,
2190                 output_directory=None,
2191                 additional_apk_paths=None,
2192                 bundle_generation_info=None,
2193                 is_test_apk=False):
2194  colorama.init()
2195  parser.set_defaults(
2196      additional_apk_paths=additional_apk_paths,
2197      output_directory=output_directory)
2198  from_wrapper_script = bool(output_directory)
2199  args = _ParseArgs(parser,
2200                    from_wrapper_script,
2201                    is_bundle=bool(bundle_generation_info),
2202                    is_test_apk=is_test_apk)
2203  run_tests_helper.SetLogLevel(args.verbose_count)
2204  if bundle_generation_info:
2205    args.command.RegisterBundleGenerationInfo(bundle_generation_info)
2206  if args.additional_apk_paths:
2207    for i, path in enumerate(args.additional_apk_paths):
2208      if path and not os.path.exists(path):
2209        inc_path = path.replace('.apk', '_incremental.apk')
2210        if os.path.exists(inc_path):
2211          args.additional_apk_paths[i] = inc_path
2212          path = inc_path
2213      if not path or not os.path.exists(path):
2214        raise Exception('Invalid additional APK path "{}"'.format(path))
2215  args.command.ProcessArgs(args)
2216  args.command.Run()
2217  # Incremental install depends on the cache being cleared when uninstalling.
2218  if args.command.name != 'uninstall':
2219    _SaveDeviceCaches(args.command.devices, output_directory)
2220
2221
2222def Run(output_directory, apk_path, additional_apk_paths, incremental_json,
2223        command_line_flags_file, target_cpu, proguard_mapping_path):
2224  """Entry point for generated wrapper scripts."""
2225  constants.SetOutputDirectory(output_directory)
2226  devil_chromium.Initialize(output_directory=output_directory)
2227  parser = argparse.ArgumentParser()
2228  exists_or_none = lambda p: p if p and os.path.exists(p) else None
2229
2230  parser.set_defaults(
2231      command_line_flags_file=command_line_flags_file,
2232      target_cpu=target_cpu,
2233      apk_path=exists_or_none(apk_path),
2234      incremental_json=exists_or_none(incremental_json),
2235      proguard_mapping_path=proguard_mapping_path)
2236  _RunInternal(
2237      parser,
2238      output_directory=output_directory,
2239      additional_apk_paths=additional_apk_paths)
2240
2241
2242def RunForBundle(output_directory, bundle_path, bundle_apks_path,
2243                 additional_apk_paths, aapt2_path, keystore_path,
2244                 keystore_password, keystore_alias, package_name,
2245                 command_line_flags_file, proguard_mapping_path, target_cpu,
2246                 system_image_locales, default_modules):
2247  """Entry point for generated app bundle wrapper scripts.
2248
2249  Args:
2250    output_dir: Chromium output directory path.
2251    bundle_path: Input bundle path.
2252    bundle_apks_path: Output bundle .apks archive path.
2253    additional_apk_paths: Additional APKs to install prior to bundle install.
2254    aapt2_path: Aapt2 tool path.
2255    keystore_path: Keystore file path.
2256    keystore_password: Keystore password.
2257    keystore_alias: Signing key name alias in keystore file.
2258    package_name: Application's package name.
2259    command_line_flags_file: Optional. Name of an on-device file that will be
2260      used to store command-line flags for this bundle.
2261    proguard_mapping_path: Input path to the Proguard mapping file, used to
2262      deobfuscate Java stack traces.
2263    target_cpu: Chromium target CPU name, used by the 'gdb' command.
2264    system_image_locales: List of Chromium locales that should be included in
2265      system image APKs.
2266    default_modules: List of modules that are installed in addition to those
2267      given by the '-m' switch.
2268  """
2269  constants.SetOutputDirectory(output_directory)
2270  devil_chromium.Initialize(output_directory=output_directory)
2271  bundle_generation_info = BundleGenerationInfo(
2272      bundle_path=bundle_path,
2273      bundle_apks_path=bundle_apks_path,
2274      aapt2_path=aapt2_path,
2275      keystore_path=keystore_path,
2276      keystore_password=keystore_password,
2277      keystore_alias=keystore_alias,
2278      system_image_locales=system_image_locales)
2279  _InstallCommand.default_modules = default_modules
2280
2281  parser = argparse.ArgumentParser()
2282  parser.set_defaults(
2283      package_name=package_name,
2284      command_line_flags_file=command_line_flags_file,
2285      proguard_mapping_path=proguard_mapping_path,
2286      target_cpu=target_cpu)
2287  _RunInternal(
2288      parser,
2289      output_directory=output_directory,
2290      additional_apk_paths=additional_apk_paths,
2291      bundle_generation_info=bundle_generation_info)
2292
2293
2294def RunForTestApk(*, output_directory, package_name, test_apk_path,
2295                  test_apk_json, proguard_mapping_path, additional_apk_paths):
2296  """Entry point for generated test apk wrapper scripts.
2297
2298  This is intended to make commands like logcat (with proguard deobfuscation)
2299  available. The run_* scripts should be used to actually run tests.
2300
2301  Args:
2302    output_dir: Chromium output directory path.
2303    package_name: The package name for the test apk.
2304    test_apk_path: The test apk to install.
2305    test_apk_json: The incremental json dict for the test apk.
2306    proguard_mapping_path: Input path to the Proguard mapping file, used to
2307      deobfuscate Java stack traces.
2308    additional_apk_paths: Additional APKs to install.
2309  """
2310  constants.SetOutputDirectory(output_directory)
2311  devil_chromium.Initialize(output_directory=output_directory)
2312
2313  parser = argparse.ArgumentParser()
2314  exists_or_none = lambda p: p if p and os.path.exists(p) else None
2315
2316  parser.set_defaults(apk_path=exists_or_none(test_apk_path),
2317                      incremental_json=exists_or_none(test_apk_json),
2318                      package_name=package_name,
2319                      proguard_mapping_path=proguard_mapping_path)
2320
2321  _RunInternal(parser,
2322               output_directory=output_directory,
2323               additional_apk_paths=additional_apk_paths,
2324               is_test_apk=True)
2325
2326
2327def main():
2328  devil_chromium.Initialize()
2329  _RunInternal(argparse.ArgumentParser())
2330
2331
2332if __name__ == '__main__':
2333  main()
2334