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