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