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