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