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@[email protected]@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@[email protected]@[email protected] 517 # /data/dalvik-cache/arm/data@[email protected]@[email protected] 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, None 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 if fully_supported: 1444 self.devices = fully_supported 1445 else: 1446 reason_string = '\n'.join( 1447 'The device (serial={}) is not supported because {}'.format( 1448 serial, reason) 1449 for serial, reason in not_supported_reasons.items()) 1450 raise Exception('Cannot find any supported devices for this app.\n\n' 1451 f'{reason_string}') 1452 1453 # TODO(agrieve): Device cache should not depend on output directory. 1454 # Maybe put into /tmp? 1455 _LoadDeviceCaches(self.devices, args.output_directory) 1456 1457 try: 1458 if len(self.devices) > 1: 1459 if not self.supports_multiple_devices: 1460 self._parser.error(device_errors.MultipleDevicesError(self.devices)) 1461 if not args.all and not args.devices: 1462 self._parser.error(_GenerateMissingAllFlagMessage(self.devices)) 1463 # Save cache now if command will not get a chance to afterwards. 1464 if self.calls_exec: 1465 _SaveDeviceCaches(self.devices, args.output_directory) 1466 except: 1467 _SaveDeviceCaches(self.devices, args.output_directory) 1468 raise 1469 1470 1471class _DevicesCommand(_Command): 1472 name = 'devices' 1473 description = 'Describe attached devices.' 1474 all_devices_by_default = True 1475 1476 def Run(self): 1477 print(_GenerateAvailableDevicesMessage(self.devices)) 1478 1479 1480class _PackageInfoCommand(_Command): 1481 name = 'package-info' 1482 description = 'Show various attributes of this app.' 1483 need_device_args = False 1484 needs_package_name = True 1485 needs_apk_helper = True 1486 1487 def Run(self): 1488 # Format all (even ints) as strings, to handle cases where APIs return None 1489 print('Package name: "%s"' % self.args.package_name) 1490 print('versionCode: %s' % self.apk_helper.GetVersionCode()) 1491 print('versionName: "%s"' % self.apk_helper.GetVersionName()) 1492 print('minSdkVersion: %s' % self.apk_helper.GetMinSdkVersion()) 1493 print('targetSdkVersion: %s' % self.apk_helper.GetTargetSdkVersion()) 1494 print('Supported ABIs: %r' % self.apk_helper.GetAbis()) 1495 1496 1497class _InstallCommand(_Command): 1498 name = 'install' 1499 description = 'Installs the APK or bundle to one or more devices.' 1500 needs_apk_helper = True 1501 supports_incremental = True 1502 default_modules = [] 1503 1504 def _RegisterExtraArgs(self, group): 1505 if self.is_bundle: 1506 group.add_argument( 1507 '-m', 1508 '--module', 1509 action='append', 1510 default=self.default_modules, 1511 help='Module to install. Can be specified multiple times.') 1512 group.add_argument( 1513 '-f', 1514 '--fake', 1515 action='append', 1516 default=[], 1517 help='Fake bundle module install. Can be specified multiple times. ' 1518 'Requires \'-m {0}\' to be given, and \'-f {0}\' is illegal.'.format( 1519 BASE_MODULE)) 1520 # Add even if |self.default_modules| is empty, for consistency. 1521 group.add_argument('--no-module', 1522 action='append', 1523 choices=self.default_modules, 1524 default=[], 1525 help='Module to exclude from default install.') 1526 1527 def Run(self): 1528 if self.additional_apk_helpers: 1529 for additional_apk_helper in self.additional_apk_helpers: 1530 _InstallApk(self.devices, additional_apk_helper, None) 1531 if self.is_bundle: 1532 modules = list( 1533 set(self.args.module) - set(self.args.no_module) - 1534 set(self.args.fake)) 1535 _InstallBundle(self.devices, self.apk_helper, modules, self.args.fake) 1536 else: 1537 _InstallApk(self.devices, self.apk_helper, self.install_dict) 1538 1539 1540class _UninstallCommand(_Command): 1541 name = 'uninstall' 1542 description = 'Removes the APK or bundle from one or more devices.' 1543 needs_package_name = True 1544 1545 def Run(self): 1546 _UninstallApk(self.devices, self.install_dict, self.args.package_name) 1547 1548 1549class _SetWebViewProviderCommand(_Command): 1550 name = 'set-webview-provider' 1551 description = ("Sets the device's WebView provider to this APK's " 1552 "package name.") 1553 needs_package_name = True 1554 needs_apk_helper = True 1555 1556 def Run(self): 1557 if not _IsWebViewProvider(self.apk_helper): 1558 raise Exception('This package does not have a WebViewLibrary meta-data ' 1559 'tag. Are you sure it contains a WebView implementation?') 1560 _SetWebViewProvider(self.devices, self.args.package_name) 1561 1562 1563class _LaunchCommand(_Command): 1564 name = 'launch' 1565 description = ('Sends a launch intent for the APK or bundle after first ' 1566 'writing the command-line flags file.') 1567 needs_package_name = True 1568 accepts_command_line_flags = True 1569 all_devices_by_default = True 1570 1571 def _RegisterExtraArgs(self, group): 1572 group.add_argument('-w', '--wait-for-java-debugger', action='store_true', 1573 help='Pause execution until debugger attaches. Applies ' 1574 'only to the main process. To have renderers wait, ' 1575 'use --args="--renderer-wait-for-java-debugger"') 1576 group.add_argument('--debug-process-name', 1577 help='Name of the process to debug. ' 1578 'E.g. "privileged_process0", or "foo.bar:baz"') 1579 group.add_argument('--nokill', action='store_true', 1580 help='Do not set the debug-app, nor set command-line ' 1581 'flags. Useful to load a URL without having the ' 1582 'app restart.') 1583 group.add_argument('url', nargs='?', help='A URL to launch with.') 1584 1585 def Run(self): 1586 if self.is_test_apk: 1587 raise Exception('Use the bin/run_* scripts to run test apks.') 1588 _LaunchUrl(self.devices, 1589 self.args.package_name, 1590 argv=self.args.args, 1591 command_line_flags_file=self.args.command_line_flags_file, 1592 url=self.args.url, 1593 wait_for_java_debugger=self.args.wait_for_java_debugger, 1594 debug_process_name=self.args.debug_process_name, 1595 nokill=self.args.nokill) 1596 1597 1598class _StopCommand(_Command): 1599 name = 'stop' 1600 description = 'Force-stops the app.' 1601 needs_package_name = True 1602 all_devices_by_default = True 1603 1604 def Run(self): 1605 device_utils.DeviceUtils.parallel(self.devices).ForceStop( 1606 self.args.package_name) 1607 1608 1609class _ClearDataCommand(_Command): 1610 name = 'clear-data' 1611 descriptions = 'Clears all app data.' 1612 needs_package_name = True 1613 all_devices_by_default = True 1614 1615 def Run(self): 1616 device_utils.DeviceUtils.parallel(self.devices).ClearApplicationState( 1617 self.args.package_name) 1618 1619 1620class _ArgvCommand(_Command): 1621 name = 'argv' 1622 description = 'Display and optionally update command-line flags file.' 1623 needs_package_name = True 1624 accepts_command_line_flags = True 1625 all_devices_by_default = True 1626 1627 def Run(self): 1628 _ChangeFlags(self.devices, self.args.args, 1629 self.args.command_line_flags_file) 1630 1631 1632class _GdbCommand(_Command): 1633 name = 'gdb' 1634 description = 'Runs //build/android/adb_gdb with apk-specific args.' 1635 long_description = description + """ 1636 1637To attach to a process other than the APK's main process, use --pid=1234. 1638To list all PIDs, use the "ps" command. 1639 1640If no apk process is currently running, sends a launch intent. 1641""" 1642 needs_package_name = True 1643 needs_output_directory = True 1644 calls_exec = True 1645 supports_multiple_devices = False 1646 1647 def Run(self): 1648 _RunGdb(self.devices[0], self.args.package_name, 1649 self.args.debug_process_name, self.args.pid, 1650 self.args.output_directory, self.args.target_cpu, self.args.port, 1651 self.args.ide, bool(self.args.verbose_count)) 1652 1653 def _RegisterExtraArgs(self, group): 1654 pid_group = group.add_mutually_exclusive_group() 1655 pid_group.add_argument('--debug-process-name', 1656 help='Name of the process to attach to. ' 1657 'E.g. "privileged_process0", or "foo.bar:baz"') 1658 pid_group.add_argument('--pid', 1659 help='The process ID to attach to. Defaults to ' 1660 'the main process for the package.') 1661 group.add_argument('--ide', action='store_true', 1662 help='Rather than enter a gdb prompt, set up the ' 1663 'gdb connection and wait for an IDE to ' 1664 'connect.') 1665 # Same default port that ndk-gdb.py uses. 1666 group.add_argument('--port', type=int, default=5039, 1667 help='Use the given port for the GDB connection') 1668 1669 1670class _LldbCommand(_Command): 1671 name = 'lldb' 1672 description = 'Runs //build/android/connect_lldb.sh with apk-specific args.' 1673 long_description = description + """ 1674 1675To attach to a process other than the APK's main process, use --pid=1234. 1676To list all PIDs, use the "ps" command. 1677 1678If no apk process is currently running, sends a launch intent. 1679""" 1680 needs_package_name = True 1681 needs_output_directory = True 1682 calls_exec = True 1683 supports_multiple_devices = False 1684 1685 def Run(self): 1686 _RunLldb(device=self.devices[0], 1687 package_name=self.args.package_name, 1688 debug_process_name=self.args.debug_process_name, 1689 pid=self.args.pid, 1690 output_directory=self.args.output_directory, 1691 port=self.args.port, 1692 target_cpu=self.args.target_cpu, 1693 ndk_dir=self.args.ndk_dir, 1694 lldb_server=self.args.lldb_server, 1695 lldb=self.args.lldb, 1696 verbose=bool(self.args.verbose_count)) 1697 1698 def _RegisterExtraArgs(self, group): 1699 pid_group = group.add_mutually_exclusive_group() 1700 pid_group.add_argument('--debug-process-name', 1701 help='Name of the process to attach to. ' 1702 'E.g. "privileged_process0", or "foo.bar:baz"') 1703 pid_group.add_argument('--pid', 1704 help='The process ID to attach to. Defaults to ' 1705 'the main process for the package.') 1706 group.add_argument('--ndk-dir', 1707 help='Select alternative NDK root directory.') 1708 group.add_argument('--lldb-server', 1709 help='Select alternative on-device lldb-server.') 1710 group.add_argument('--lldb', help='Select alternative client lldb.sh.') 1711 # Same default port that ndk-gdb.py uses. 1712 group.add_argument('--port', 1713 type=int, 1714 default=5039, 1715 help='Use the given port for the LLDB connection') 1716 1717 1718class _LogcatCommand(_Command): 1719 name = 'logcat' 1720 description = 'Runs "adb logcat" with filters relevant the current APK.' 1721 long_description = description + """ 1722 1723"Relevant filters" means: 1724 * Log messages from processes belonging to the apk, 1725 * Plus log messages from log tags: ActivityManager|DEBUG, 1726 * Plus fatal logs from any process, 1727 * Minus spamy dalvikvm logs (for pre-L devices). 1728 1729Colors: 1730 * Primary process is white 1731 * Other processes (gpu, renderer) are yellow 1732 * Non-apk processes are grey 1733 * UI thread has a bolded Thread-ID 1734 1735Java stack traces are detected and deobfuscated (for release builds). 1736 1737To disable filtering, (but keep coloring), use --verbose. 1738""" 1739 needs_package_name = True 1740 supports_multiple_devices = False 1741 1742 def Run(self): 1743 deobfuscate = None 1744 if self.args.proguard_mapping_path and not self.args.no_deobfuscate: 1745 deobfuscate = deobfuscator.Deobfuscator(self.args.proguard_mapping_path) 1746 1747 if self.args.apk_path or self.bundle_generation_info: 1748 stack_script_context = _StackScriptContext(self.args.output_directory, 1749 self.args.apk_path, 1750 self.bundle_generation_info, 1751 quiet=True) 1752 else: 1753 stack_script_context = None 1754 1755 extra_package_names = [] 1756 if self.is_test_apk and self.additional_apk_helpers: 1757 for additional_apk_helper in self.additional_apk_helpers: 1758 extra_package_names.append(additional_apk_helper.GetPackageName()) 1759 1760 try: 1761 _RunLogcat(self.devices[0], 1762 self.args.package_name, 1763 stack_script_context, 1764 deobfuscate, 1765 bool(self.args.verbose_count), 1766 self.args.exit_on_match, 1767 extra_package_names=extra_package_names) 1768 except KeyboardInterrupt: 1769 pass # Don't show stack trace upon Ctrl-C 1770 finally: 1771 if stack_script_context: 1772 stack_script_context.Close() 1773 if deobfuscate: 1774 deobfuscate.Close() 1775 1776 def _RegisterExtraArgs(self, group): 1777 if self._from_wrapper_script: 1778 group.add_argument('--no-deobfuscate', action='store_true', 1779 help='Disables ProGuard deobfuscation of logcat.') 1780 else: 1781 group.set_defaults(no_deobfuscate=False) 1782 group.add_argument('--proguard-mapping-path', 1783 help='Path to ProGuard map (enables deobfuscation)') 1784 group.add_argument('--exit-on-match', 1785 help='Exits logcat when a message matches this regex.') 1786 1787 1788class _PsCommand(_Command): 1789 name = 'ps' 1790 description = 'Show PIDs of any APK processes currently running.' 1791 needs_package_name = True 1792 all_devices_by_default = True 1793 1794 def Run(self): 1795 _RunPs(self.devices, self.args.package_name) 1796 1797 1798class _DiskUsageCommand(_Command): 1799 name = 'disk-usage' 1800 description = 'Show how much device storage is being consumed by the app.' 1801 needs_package_name = True 1802 all_devices_by_default = True 1803 1804 def Run(self): 1805 _RunDiskUsage(self.devices, self.args.package_name) 1806 1807 1808class _MemUsageCommand(_Command): 1809 name = 'mem-usage' 1810 description = 'Show memory usage of currently running APK processes.' 1811 needs_package_name = True 1812 all_devices_by_default = True 1813 1814 def _RegisterExtraArgs(self, group): 1815 group.add_argument('--query-app', action='store_true', 1816 help='Do not add --local to "dumpsys meminfo". This will output ' 1817 'additional metrics (e.g. Context count), but also cause memory ' 1818 'to be used in order to gather the metrics.') 1819 1820 def Run(self): 1821 _RunMemUsage(self.devices, self.args.package_name, 1822 query_app=self.args.query_app) 1823 1824 1825class _ShellCommand(_Command): 1826 name = 'shell' 1827 description = ('Same as "adb shell <command>", but runs as the apk\'s uid ' 1828 '(via run-as). Useful for inspecting the app\'s data ' 1829 'directory.') 1830 needs_package_name = True 1831 1832 @property 1833 def calls_exec(self): 1834 return not self.args.cmd 1835 1836 @property 1837 def supports_multiple_devices(self): 1838 return not self.args.cmd 1839 1840 def _RegisterExtraArgs(self, group): 1841 group.add_argument( 1842 'cmd', nargs=argparse.REMAINDER, help='Command to run.') 1843 1844 def Run(self): 1845 _RunShell(self.devices, self.args.package_name, self.args.cmd) 1846 1847 1848class _CompileDexCommand(_Command): 1849 name = 'compile-dex' 1850 description = ('Applicable only for Android N+. Forces .odex files to be ' 1851 'compiled with the given compilation filter. To see existing ' 1852 'filter, use "disk-usage" command.') 1853 needs_package_name = True 1854 all_devices_by_default = True 1855 1856 def _RegisterExtraArgs(self, group): 1857 group.add_argument( 1858 'compilation_filter', 1859 choices=['verify', 'quicken', 'space-profile', 'space', 1860 'speed-profile', 'speed'], 1861 help='For WebView/Monochrome, use "speed". For other apks, use ' 1862 '"speed-profile".') 1863 1864 def Run(self): 1865 _RunCompileDex(self.devices, self.args.package_name, 1866 self.args.compilation_filter) 1867 1868 1869class _PrintCertsCommand(_Command): 1870 name = 'print-certs' 1871 description = 'Print info about certificates used to sign this APK.' 1872 need_device_args = False 1873 needs_apk_helper = True 1874 1875 def _RegisterExtraArgs(self, group): 1876 group.add_argument( 1877 '--full-cert', 1878 action='store_true', 1879 help=("Print the certificate's full signature, Base64-encoded. " 1880 "Useful when configuring an Android image's " 1881 "config_webview_packages.xml.")) 1882 1883 def Run(self): 1884 keytool = os.path.join(_JAVA_HOME, 'bin', 'keytool') 1885 pem_certificate_pattern = re.compile( 1886 r'-+BEGIN CERTIFICATE-+([\r\n0-9A-Za-z+/=]+)-+END CERTIFICATE-+[\r\n]*') 1887 if self.is_bundle: 1888 # Bundles are not signed until converted to .apks. The wrapper scripts 1889 # record which key will be used to sign though. 1890 with tempfile.NamedTemporaryFile() as f: 1891 logging.warning('Bundles are not signed until turned into .apk files.') 1892 logging.warning('Showing signing info based on associated keystore.') 1893 cmd = [ 1894 keytool, '-exportcert', '-keystore', 1895 self.bundle_generation_info.keystore_path, '-storepass', 1896 self.bundle_generation_info.keystore_password, '-alias', 1897 self.bundle_generation_info.keystore_alias, '-file', f.name 1898 ] 1899 logging.warning('Running: %s', shlex.join(cmd)) 1900 subprocess.check_output(cmd, stderr=subprocess.STDOUT) 1901 cmd = [keytool, '-printcert', '-file', f.name] 1902 logging.warning('Running: %s', shlex.join(cmd)) 1903 subprocess.check_call(cmd) 1904 if self.args.full_cert: 1905 # Redirect stderr to hide a keytool warning about using non-standard 1906 # keystore format. 1907 cmd += ['-rfc'] 1908 logging.warning('Running: %s', shlex.join(cmd)) 1909 pem_encoded_certificate = subprocess.check_output( 1910 cmd, stderr=subprocess.STDOUT).decode() 1911 else: 1912 1913 def run_apksigner(min_sdk_version): 1914 cmd = [ 1915 build_tools.GetPath('apksigner'), 'verify', '--min-sdk-version', 1916 str(min_sdk_version), '--print-certs-pem', '--verbose', 1917 self.apk_helper.path 1918 ] 1919 logging.warning('Running: %s', shlex.join(cmd)) 1920 env = os.environ.copy() 1921 env['PATH'] = os.path.pathsep.join( 1922 [os.path.join(_JAVA_HOME, 'bin'), 1923 env.get('PATH')]) 1924 # Redirect stderr to hide verification failures (see explanation below). 1925 return subprocess.check_output(cmd, 1926 env=env, 1927 universal_newlines=True, 1928 stderr=subprocess.STDOUT) 1929 1930 # apksigner's default behavior is nonintuitive: it will print "Verified 1931 # using <scheme number>...: false" for any scheme which is obsolete for 1932 # the APK's minSdkVersion even if it actually was signed with that scheme 1933 # (ex. it prints "Verified using v1 scheme: false" for Monochrome because 1934 # v1 was obsolete by N). To workaround this, we force apksigner to use the 1935 # lowest possible minSdkVersion. We need to fallback to higher 1936 # minSdkVersions in case the APK fails to verify for that minSdkVersion 1937 # (which means the APK is genuinely not signed with that scheme). These 1938 # SDK values are the highest SDK version before the next scheme is 1939 # available: 1940 versions = [ 1941 version_codes.MARSHMALLOW, # before v2 launched in N 1942 version_codes.OREO_MR1, # before v3 launched in P 1943 version_codes.Q, # before v4 launched in R 1944 version_codes.R, 1945 ] 1946 stdout = None 1947 for min_sdk_version in versions: 1948 try: 1949 stdout = run_apksigner(min_sdk_version) 1950 break 1951 except subprocess.CalledProcessError: 1952 # Doesn't verify with this min-sdk-version, so try again with a higher 1953 # one 1954 continue 1955 if not stdout: 1956 raise RuntimeError('apksigner was not able to verify APK') 1957 1958 # Separate what the '--print-certs' flag would output vs. the additional 1959 # signature output included by '--print-certs-pem'. The additional PEM 1960 # output is only printed when self.args.full_cert is specified. 1961 verification_hash_info = pem_certificate_pattern.sub('', stdout) 1962 print(verification_hash_info) 1963 if self.args.full_cert: 1964 m = pem_certificate_pattern.search(stdout) 1965 if not m: 1966 raise Exception('apksigner did not print a certificate') 1967 pem_encoded_certificate = m.group(0) 1968 1969 1970 if self.args.full_cert: 1971 m = pem_certificate_pattern.search(pem_encoded_certificate) 1972 if not m: 1973 raise Exception( 1974 'Unable to parse certificate:\n{}'.format(pem_encoded_certificate)) 1975 signature = re.sub(r'[\r\n]+', '', m.group(1)) 1976 print() 1977 print('Full Signature:') 1978 print(signature) 1979 1980 1981class _ProfileCommand(_Command): 1982 name = 'profile' 1983 description = ('Run the simpleperf sampling CPU profiler on the currently-' 1984 'running APK. If --args is used, the extra arguments will be ' 1985 'passed on to simpleperf; otherwise, the following default ' 1986 'arguments are used: -g -f 1000 -o /data/local/tmp/perf.data') 1987 needs_package_name = True 1988 needs_output_directory = True 1989 supports_multiple_devices = False 1990 accepts_args = True 1991 1992 def _RegisterExtraArgs(self, group): 1993 group.add_argument( 1994 '--profile-process', default='browser', 1995 help=('Which process to profile. This may be a process name or pid ' 1996 'such as you would get from running `%s ps`; or ' 1997 'it can be one of (browser, renderer, gpu).' % sys.argv[0])) 1998 group.add_argument( 1999 '--profile-thread', default=None, 2000 help=('(Optional) Profile only a single thread. This may be either a ' 2001 'thread ID such as you would get by running `adb shell ps -t` ' 2002 '(pre-Oreo) or `adb shell ps -e -T` (Oreo and later); or it may ' 2003 'be one of (io, compositor, main, render), in which case ' 2004 '--profile-process is also required. (Note that "render" thread ' 2005 'refers to a thread in the browser process that manages a ' 2006 'renderer; to profile the main thread of the renderer process, ' 2007 'use --profile-thread=main).')) 2008 group.add_argument('--profile-output', default='profile.pb', 2009 help='Output file for profiling data') 2010 group.add_argument('--profile-events', default='cpu-cycles', 2011 help=('A comma separated list of perf events to capture ' 2012 '(e.g. \'cpu-cycles,branch-misses\'). Run ' 2013 '`simpleperf list` on your device to see available ' 2014 'events.')) 2015 2016 def Run(self): 2017 extra_args = shlex.split(self.args.args or '') 2018 _RunProfile(self.devices[0], self.args.package_name, 2019 self.args.output_directory, self.args.profile_output, 2020 self.args.profile_process, self.args.profile_thread, 2021 self.args.profile_events, extra_args) 2022 2023 2024class _RunCommand(_InstallCommand, _LaunchCommand, _LogcatCommand): 2025 name = 'run' 2026 description = 'Install, launch, and show logcat (when targeting one device).' 2027 all_devices_by_default = False 2028 supports_multiple_devices = True 2029 2030 def _RegisterExtraArgs(self, group): 2031 _InstallCommand._RegisterExtraArgs(self, group) 2032 _LaunchCommand._RegisterExtraArgs(self, group) 2033 _LogcatCommand._RegisterExtraArgs(self, group) 2034 group.add_argument('--no-logcat', action='store_true', 2035 help='Install and launch, but do not enter logcat.') 2036 2037 def Run(self): 2038 if self.is_test_apk: 2039 raise Exception('Use the bin/run_* scripts to run test apks.') 2040 logging.warning('Installing...') 2041 _InstallCommand.Run(self) 2042 logging.warning('Sending launch intent...') 2043 _LaunchCommand.Run(self) 2044 if len(self.devices) == 1 and not self.args.no_logcat: 2045 logging.warning('Entering logcat...') 2046 _LogcatCommand.Run(self) 2047 2048 2049class _BuildBundleApks(_Command): 2050 name = 'build-bundle-apks' 2051 description = ('Build the .apks archive from an Android app bundle, and ' 2052 'optionally copy it to a specific destination.') 2053 need_device_args = False 2054 2055 def _RegisterExtraArgs(self, group): 2056 group.add_argument( 2057 '--output-apks', required=True, help='Destination path for .apks file.') 2058 group.add_argument( 2059 '--minimal', 2060 action='store_true', 2061 help='Build .apks archive that targets the bundle\'s minSdkVersion and ' 2062 'contains only english splits. It still contains optional splits.') 2063 group.add_argument( 2064 '--sdk-version', help='The sdkVersion to build the .apks for.') 2065 group.add_argument( 2066 '--build-mode', 2067 choices=app_bundle_utils.BUILD_APKS_MODES, 2068 help='Specify which type of APKs archive to build. "default" ' 2069 'generates regular splits, "universal" generates an archive with a ' 2070 'single universal APK, "system" generates an archive with a system ' 2071 'image APK, while "system_compressed" generates a compressed system ' 2072 'APK, with an additional stub APK for the system image.') 2073 group.add_argument( 2074 '--optimize-for', 2075 choices=app_bundle_utils.OPTIMIZE_FOR_OPTIONS, 2076 help='Override split configuration.') 2077 2078 def Run(self): 2079 _GenerateBundleApks( 2080 self.bundle_generation_info, 2081 output_path=self.args.output_apks, 2082 minimal=self.args.minimal, 2083 minimal_sdk_version=self.args.sdk_version, 2084 mode=self.args.build_mode, 2085 optimize_for=self.args.optimize_for) 2086 2087 2088class _ManifestCommand(_Command): 2089 name = 'dump-manifest' 2090 description = 'Dump the android manifest as XML, to stdout.' 2091 need_device_args = False 2092 needs_apk_helper = True 2093 2094 def Run(self): 2095 if self.is_bundle: 2096 sys.stdout.write( 2097 bundletool.RunBundleTool([ 2098 'dump', 'manifest', '--bundle', 2099 self.bundle_generation_info.bundle_path 2100 ])) 2101 else: 2102 apkanalyzer = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'android_sdk', 2103 'public', 'cmdline-tools', 'latest', 'bin', 2104 'apkanalyzer') 2105 cmd = [apkanalyzer, 'manifest', 'print', self.apk_helper.path] 2106 logging.info('Running: %s', shlex.join(cmd)) 2107 subprocess.check_call(cmd) 2108 2109 2110class _StackCommand(_Command): 2111 name = 'stack' 2112 description = 'Decodes an Android stack.' 2113 need_device_args = False 2114 2115 def _RegisterExtraArgs(self, group): 2116 group.add_argument( 2117 'file', 2118 nargs='?', 2119 help='File to decode. If not specified, stdin is processed.') 2120 2121 def Run(self): 2122 context = _StackScriptContext(self.args.output_directory, 2123 self.args.apk_path, 2124 self.bundle_generation_info) 2125 try: 2126 proc = context.Popen(input_file=self.args.file) 2127 if proc.wait(): 2128 raise Exception('stack script returned {}'.format(proc.returncode)) 2129 finally: 2130 context.Close() 2131 2132 2133# Shared commands for regular APKs and app bundles. 2134_COMMANDS = [ 2135 _DevicesCommand, 2136 _PackageInfoCommand, 2137 _InstallCommand, 2138 _UninstallCommand, 2139 _SetWebViewProviderCommand, 2140 _LaunchCommand, 2141 _StopCommand, 2142 _ClearDataCommand, 2143 _ArgvCommand, 2144 _GdbCommand, 2145 _LldbCommand, 2146 _LogcatCommand, 2147 _PsCommand, 2148 _DiskUsageCommand, 2149 _MemUsageCommand, 2150 _ShellCommand, 2151 _CompileDexCommand, 2152 _PrintCertsCommand, 2153 _ProfileCommand, 2154 _RunCommand, 2155 _StackCommand, 2156 _ManifestCommand, 2157] 2158 2159# Commands specific to app bundles. 2160_BUNDLE_COMMANDS = [ 2161 _BuildBundleApks, 2162] 2163 2164 2165def _ParseArgs(parser, from_wrapper_script, is_bundle, is_test_apk): 2166 subparsers = parser.add_subparsers() 2167 command_list = _COMMANDS + (_BUNDLE_COMMANDS if is_bundle else []) 2168 commands = [ 2169 clazz(from_wrapper_script, is_bundle, is_test_apk) 2170 for clazz in command_list 2171 ] 2172 2173 for command in commands: 2174 if from_wrapper_script or not command.needs_output_directory: 2175 command.RegisterArgs(subparsers) 2176 2177 # Show extended help when no command is passed. 2178 argv = sys.argv[1:] 2179 if not argv: 2180 argv = ['--help'] 2181 2182 return parser.parse_args(argv) 2183 2184 2185def _RunInternal(parser, 2186 output_directory=None, 2187 additional_apk_paths=None, 2188 bundle_generation_info=None, 2189 is_test_apk=False): 2190 colorama.init() 2191 parser.set_defaults( 2192 additional_apk_paths=additional_apk_paths, 2193 output_directory=output_directory) 2194 from_wrapper_script = bool(output_directory) 2195 args = _ParseArgs(parser, 2196 from_wrapper_script, 2197 is_bundle=bool(bundle_generation_info), 2198 is_test_apk=is_test_apk) 2199 run_tests_helper.SetLogLevel(args.verbose_count) 2200 if bundle_generation_info: 2201 args.command.RegisterBundleGenerationInfo(bundle_generation_info) 2202 if args.additional_apk_paths: 2203 for path in additional_apk_paths: 2204 if not path or not os.path.exists(path): 2205 raise Exception('Invalid additional APK path "{}"'.format(path)) 2206 args.command.ProcessArgs(args) 2207 args.command.Run() 2208 # Incremental install depends on the cache being cleared when uninstalling. 2209 if args.command.name != 'uninstall': 2210 _SaveDeviceCaches(args.command.devices, output_directory) 2211 2212 2213def Run(output_directory, apk_path, additional_apk_paths, incremental_json, 2214 command_line_flags_file, target_cpu, proguard_mapping_path): 2215 """Entry point for generated wrapper scripts.""" 2216 constants.SetOutputDirectory(output_directory) 2217 devil_chromium.Initialize(output_directory=output_directory) 2218 parser = argparse.ArgumentParser() 2219 exists_or_none = lambda p: p if p and os.path.exists(p) else None 2220 2221 parser.set_defaults( 2222 command_line_flags_file=command_line_flags_file, 2223 target_cpu=target_cpu, 2224 apk_path=exists_or_none(apk_path), 2225 incremental_json=exists_or_none(incremental_json), 2226 proguard_mapping_path=proguard_mapping_path) 2227 _RunInternal( 2228 parser, 2229 output_directory=output_directory, 2230 additional_apk_paths=additional_apk_paths) 2231 2232 2233def RunForBundle(output_directory, bundle_path, bundle_apks_path, 2234 additional_apk_paths, aapt2_path, keystore_path, 2235 keystore_password, keystore_alias, package_name, 2236 command_line_flags_file, proguard_mapping_path, target_cpu, 2237 system_image_locales, default_modules): 2238 """Entry point for generated app bundle wrapper scripts. 2239 2240 Args: 2241 output_dir: Chromium output directory path. 2242 bundle_path: Input bundle path. 2243 bundle_apks_path: Output bundle .apks archive path. 2244 additional_apk_paths: Additional APKs to install prior to bundle install. 2245 aapt2_path: Aapt2 tool path. 2246 keystore_path: Keystore file path. 2247 keystore_password: Keystore password. 2248 keystore_alias: Signing key name alias in keystore file. 2249 package_name: Application's package name. 2250 command_line_flags_file: Optional. Name of an on-device file that will be 2251 used to store command-line flags for this bundle. 2252 proguard_mapping_path: Input path to the Proguard mapping file, used to 2253 deobfuscate Java stack traces. 2254 target_cpu: Chromium target CPU name, used by the 'gdb' command. 2255 system_image_locales: List of Chromium locales that should be included in 2256 system image APKs. 2257 default_modules: List of modules that are installed in addition to those 2258 given by the '-m' switch. 2259 """ 2260 constants.SetOutputDirectory(output_directory) 2261 devil_chromium.Initialize(output_directory=output_directory) 2262 bundle_generation_info = BundleGenerationInfo( 2263 bundle_path=bundle_path, 2264 bundle_apks_path=bundle_apks_path, 2265 aapt2_path=aapt2_path, 2266 keystore_path=keystore_path, 2267 keystore_password=keystore_password, 2268 keystore_alias=keystore_alias, 2269 system_image_locales=system_image_locales) 2270 _InstallCommand.default_modules = default_modules 2271 2272 parser = argparse.ArgumentParser() 2273 parser.set_defaults( 2274 package_name=package_name, 2275 command_line_flags_file=command_line_flags_file, 2276 proguard_mapping_path=proguard_mapping_path, 2277 target_cpu=target_cpu) 2278 _RunInternal( 2279 parser, 2280 output_directory=output_directory, 2281 additional_apk_paths=additional_apk_paths, 2282 bundle_generation_info=bundle_generation_info) 2283 2284 2285def RunForTestApk(*, output_directory, package_name, test_apk_path, 2286 test_apk_json, proguard_mapping_path, additional_apk_paths): 2287 """Entry point for generated test apk wrapper scripts. 2288 2289 This is intended to make commands like logcat (with proguard deobfuscation) 2290 available. The run_* scripts should be used to actually run tests. 2291 2292 Args: 2293 output_dir: Chromium output directory path. 2294 package_name: The package name for the test apk. 2295 test_apk_path: The test apk to install. 2296 test_apk_json: The incremental json dict for the test apk. 2297 proguard_mapping_path: Input path to the Proguard mapping file, used to 2298 deobfuscate Java stack traces. 2299 additional_apk_paths: Additional APKs to install. 2300 """ 2301 constants.SetOutputDirectory(output_directory) 2302 devil_chromium.Initialize(output_directory=output_directory) 2303 2304 parser = argparse.ArgumentParser() 2305 exists_or_none = lambda p: p if p and os.path.exists(p) else None 2306 2307 parser.set_defaults(apk_path=exists_or_none(test_apk_path), 2308 incremental_json=exists_or_none(test_apk_json), 2309 package_name=package_name, 2310 proguard_mapping_path=proguard_mapping_path) 2311 2312 _RunInternal(parser, 2313 output_directory=output_directory, 2314 additional_apk_paths=additional_apk_paths, 2315 is_test_apk=True) 2316 2317 2318def main(): 2319 devil_chromium.Initialize() 2320 _RunInternal(argparse.ArgumentParser()) 2321 2322 2323if __name__ == '__main__': 2324 main() 2325