| #!/usr/bin/env vpython3 |
| # Copyright 2017 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| # Using colorama.Fore/Back/Style members |
| # pylint: disable=no-member |
| |
| |
| import argparse |
| import collections |
| import json |
| import logging |
| import os |
| import posixpath |
| import random |
| import re |
| import shlex |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import textwrap |
| import zipfile |
| |
| import adb_command_line |
| import devil_chromium |
| from devil import devil_env |
| from devil.android import apk_helper |
| from devil.android import device_errors |
| from devil.android import device_utils |
| from devil.android import flag_changer |
| from devil.android.sdk import adb_wrapper |
| from devil.android.sdk import build_tools |
| from devil.android.sdk import intent |
| from devil.android.sdk import version_codes |
| from devil.utils import run_tests_helper |
| |
| _DIR_SOURCE_ROOT = os.path.normpath( |
| os.path.join(os.path.dirname(__file__), '..', '..')) |
| _JAVA_HOME = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'jdk', 'current') |
| |
| with devil_env.SysPath( |
| os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'colorama', 'src')): |
| import colorama |
| |
| from incremental_install import installer |
| from pylib import constants |
| from pylib.symbols import deobfuscator |
| from pylib.utils import simpleperf |
| from pylib.utils import app_bundle_utils |
| |
| with devil_env.SysPath( |
| os.path.join(_DIR_SOURCE_ROOT, 'build', 'android', 'gyp')): |
| import bundletool |
| |
| BASE_MODULE = 'base' |
| |
| |
| def _Colorize(text, style=''): |
| return (style |
| + text |
| + colorama.Style.RESET_ALL) |
| |
| |
| def _InstallApk(devices, apk, install_dict): |
| def install(device): |
| if install_dict: |
| installer.Install(device, install_dict, apk=apk, permissions=[]) |
| else: |
| device.Install(apk, permissions=[], allow_downgrade=True, reinstall=True) |
| |
| logging.info('Installing %sincremental apk.', '' if install_dict else 'non-') |
| device_utils.DeviceUtils.parallel(devices).pMap(install) |
| |
| |
| # A named tuple containing the information needed to convert a bundle into |
| # an installable .apks archive. |
| # Fields: |
| # bundle_path: Path to input bundle file. |
| # bundle_apk_path: Path to output bundle .apks archive file. |
| # aapt2_path: Path to aapt2 tool. |
| # keystore_path: Path to keystore file. |
| # keystore_password: Password for the keystore file. |
| # keystore_alias: Signing key name alias within the keystore file. |
| # system_image_locales: List of Chromium locales to include in system .apks. |
| BundleGenerationInfo = collections.namedtuple( |
| 'BundleGenerationInfo', |
| 'bundle_path,bundle_apks_path,aapt2_path,keystore_path,keystore_password,' |
| 'keystore_alias,system_image_locales') |
| |
| |
| def _GenerateBundleApks(info, |
| output_path=None, |
| minimal=False, |
| minimal_sdk_version=None, |
| mode=None, |
| optimize_for=None): |
| """Generate an .apks archive from a bundle on demand. |
| |
| Args: |
| info: A BundleGenerationInfo instance. |
| output_path: Path of output .apks archive. |
| minimal: Create the minimal set of apks possible (english-only). |
| minimal_sdk_version: When minimal=True, use this sdkVersion. |
| mode: Build mode, either None, or one of app_bundle_utils.BUILD_APKS_MODES. |
| optimize_for: Override split config, either None, or one of |
| app_bundle_utils.OPTIMIZE_FOR_OPTIONS. |
| """ |
| logging.info('Generating .apks file') |
| app_bundle_utils.GenerateBundleApks( |
| info.bundle_path, |
| # Store .apks file beside the .aab file by default so that it gets cached. |
| output_path or info.bundle_apks_path, |
| info.aapt2_path, |
| info.keystore_path, |
| info.keystore_password, |
| info.keystore_alias, |
| system_image_locales=info.system_image_locales, |
| mode=mode, |
| minimal=minimal, |
| minimal_sdk_version=minimal_sdk_version, |
| optimize_for=optimize_for) |
| |
| |
| def _InstallBundle(devices, apk_helper_instance, modules, fake_modules): |
| |
| def Install(device): |
| device.Install(apk_helper_instance, |
| permissions=[], |
| modules=modules, |
| fake_modules=fake_modules, |
| allow_downgrade=True, |
| reinstall=True) |
| |
| # Basic checks for |modules| and |fake_modules|. |
| # * |fake_modules| cannot include 'base'. |
| # * If |fake_modules| is given, ensure |modules| includes 'base'. |
| # * They must be disjoint (checked by device.Install). |
| modules_set = set(modules) if modules else set() |
| fake_modules_set = set(fake_modules) if fake_modules else set() |
| if BASE_MODULE in fake_modules_set: |
| raise Exception('\'-f {}\' is disallowed.'.format(BASE_MODULE)) |
| if fake_modules_set and BASE_MODULE not in modules_set: |
| raise Exception( |
| '\'-f FAKE\' must be accompanied by \'-m {}\''.format(BASE_MODULE)) |
| |
| logging.info('Installing bundle.') |
| device_utils.DeviceUtils.parallel(devices).pMap(Install) |
| |
| |
| def _UninstallApk(devices, install_dict, package_name): |
| def uninstall(device): |
| if install_dict: |
| installer.Uninstall(device, package_name) |
| else: |
| device.Uninstall(package_name) |
| device_utils.DeviceUtils.parallel(devices).pMap(uninstall) |
| |
| |
| def _IsWebViewProvider(apk_helper_instance): |
| meta_data = apk_helper_instance.GetAllMetadata() |
| meta_data_keys = [pair[0] for pair in meta_data] |
| return 'com.android.webview.WebViewLibrary' in meta_data_keys |
| |
| |
| def _SetWebViewProvider(devices, package_name): |
| |
| def switch_provider(device): |
| if device.build_version_sdk < version_codes.NOUGAT: |
| logging.error('No need to switch provider on pre-Nougat devices (%s)', |
| device.serial) |
| else: |
| device.SetWebViewImplementation(package_name) |
| |
| device_utils.DeviceUtils.parallel(devices).pMap(switch_provider) |
| |
| |
| def _NormalizeProcessName(debug_process_name, package_name): |
| if not debug_process_name: |
| debug_process_name = package_name |
| elif debug_process_name.startswith(':'): |
| debug_process_name = package_name + debug_process_name |
| elif '.' not in debug_process_name: |
| debug_process_name = package_name + ':' + debug_process_name |
| return debug_process_name |
| |
| |
| def _ResolveActivity(device, package_name, category, action): |
| # E.g.: |
| # Activity Resolver Table: |
| # Schemes: |
| # http: |
| # 67e97c0 org.chromium.pkg/.MainActivityfilter c91d43e |
| # Action: "android.intent.action.VIEW" |
| # Category: "android.intent.category.DEFAULT" |
| # Category: "android.intent.category.BROWSABLE" |
| # Scheme: "http" |
| # Scheme: "https" |
| # |
| # Non-Data Actions: |
| # android.intent.action.MAIN: |
| # 67e97c0 org.chromium.pkg/.MainActivity filter 4a34cf9 |
| # Action: "android.intent.action.MAIN" |
| # Category: "android.intent.category.LAUNCHER" |
| lines = device.RunShellCommand(['dumpsys', 'package', package_name], |
| check_return=True) |
| |
| # Extract the Activity Resolver Table: section. |
| start_idx = next((i for i, l in enumerate(lines) |
| if l.startswith('Activity Resolver Table:')), None) |
| if start_idx is None: |
| if not device.IsApplicationInstalled(package_name): |
| raise Exception('Package not installed: ' + package_name) |
| raise Exception('No Activity Resolver Table in:\n' + '\n'.join(lines)) |
| line_count = next(i for i, l in enumerate(lines[start_idx + 1:]) |
| if l and not l[0].isspace()) |
| data = '\n'.join(lines[start_idx:start_idx + line_count]) |
| |
| # Split on each Activity entry. |
| entries = re.split(r'^ [0-9a-f]+ ', data, flags=re.MULTILINE) |
| |
| def activity_name_from_entry(entry): |
| assert entry.startswith(package_name), 'Got: ' + entry |
| activity_name = entry[len(package_name) + 1:].split(' ', 1)[0] |
| if activity_name[0] == '.': |
| activity_name = package_name + activity_name |
| return activity_name |
| |
| # Find the one with the text we want. |
| category_text = f'Category: "{category}"' |
| action_text = f'Action: "{action}"' |
| matched_entries = [ |
| e for e in entries[1:] if category_text in e and action_text in e |
| ] |
| |
| if not matched_entries: |
| raise Exception(f'Did not find {category_text}, {action_text} in\n{data}') |
| if len(matched_entries) > 1: |
| # When there are multiple matches, look for the one marked as default. |
| # Necessary for Monochrome, which also has MonochromeLauncherActivity. |
| default_entries = [ |
| e for e in matched_entries if 'android.intent.category.DEFAULT' in e |
| ] |
| matched_entries = default_entries or matched_entries |
| |
| # See if all matches point to the same activity. |
| activity_names = {activity_name_from_entry(e) for e in matched_entries} |
| |
| if len(activity_names) > 1: |
| raise Exception('Found multiple launcher activities:\n * ' + |
| '\n * '.join(sorted(activity_names))) |
| return next(iter(activity_names)) |
| |
| |
| def _LaunchUrl(devices, |
| package_name, |
| argv=None, |
| command_line_flags_file=None, |
| url=None, |
| wait_for_java_debugger=False, |
| debug_process_name=None, |
| nokill=None): |
| if argv and command_line_flags_file is None: |
| raise Exception('This apk does not support any flags.') |
| |
| debug_process_name = _NormalizeProcessName(debug_process_name, package_name) |
| |
| if url is None: |
| category = 'android.intent.category.LAUNCHER' |
| action = 'android.intent.action.MAIN' |
| else: |
| category = 'android.intent.category.BROWSABLE' |
| action = 'android.intent.action.VIEW' |
| |
| def launch(device): |
| activity = _ResolveActivity(device, package_name, category, action) |
| # --persistent is required to have Settings.Global.DEBUG_APP be set, which |
| # we currently use to allow reading of flags. https://crbug.com/784947 |
| if not nokill: |
| cmd = ['am', 'set-debug-app', '--persistent', debug_process_name] |
| if wait_for_java_debugger: |
| cmd[-1:-1] = ['-w'] |
| # Ignore error since it will fail if apk is not debuggable. |
| device.RunShellCommand(cmd, check_return=False) |
| |
| # The flags are first updated with input args. |
| if command_line_flags_file: |
| changer = flag_changer.FlagChanger(device, command_line_flags_file) |
| flags = [] |
| if argv: |
| adb_command_line.CheckBuildTypeSupportsFlags(device, |
| command_line_flags_file) |
| flags = shlex.split(argv) |
| try: |
| changer.ReplaceFlags(flags) |
| except device_errors.AdbShellCommandFailedError: |
| logging.exception('Failed to set flags') |
| |
| launch_intent = intent.Intent(action=action, |
| activity=activity, |
| data=url, |
| package=package_name) |
| logging.info('Sending launch intent for %s', activity) |
| device.StartActivity(launch_intent) |
| |
| device_utils.DeviceUtils.parallel(devices).pMap(launch) |
| if wait_for_java_debugger: |
| print('Waiting for debugger to attach to process: ' + |
| _Colorize(debug_process_name, colorama.Fore.YELLOW)) |
| |
| |
| def _ChangeFlags(devices, argv, command_line_flags_file): |
| if argv is None: |
| _DisplayArgs(devices, command_line_flags_file) |
| else: |
| flags = shlex.split(argv) |
| def update(device): |
| adb_command_line.CheckBuildTypeSupportsFlags(device, |
| command_line_flags_file) |
| changer = flag_changer.FlagChanger(device, command_line_flags_file) |
| changer.ReplaceFlags(flags) |
| device_utils.DeviceUtils.parallel(devices).pMap(update) |
| |
| |
| def _TargetCpuToTargetArch(target_cpu): |
| if target_cpu == 'x64': |
| return 'x86_64' |
| if target_cpu == 'mipsel': |
| return 'mips' |
| return target_cpu |
| |
| |
| def _RunGdb(device, package_name, debug_process_name, pid, output_directory, |
| target_cpu, port, ide, verbose): |
| if not pid: |
| debug_process_name = _NormalizeProcessName(debug_process_name, package_name) |
| pid = device.GetApplicationPids(debug_process_name, at_most_one=True) |
| if not pid: |
| # Attaching gdb makes the app run so slow that it takes *minutes* to start |
| # up (as of 2018). Better to just fail than to start & attach. |
| raise Exception('App not running.') |
| |
| gdb_script_path = os.path.dirname(__file__) + '/adb_gdb' |
| cmd = [ |
| gdb_script_path, |
| '--package-name=%s' % package_name, |
| '--output-directory=%s' % output_directory, |
| '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(), |
| '--device=%s' % device.serial, |
| '--pid=%s' % pid, |
| '--port=%d' % port, |
| ] |
| if ide: |
| cmd.append('--ide') |
| # Enable verbose output of adb_gdb if it's set for this script. |
| if verbose: |
| cmd.append('--verbose') |
| if target_cpu: |
| cmd.append('--target-arch=%s' % _TargetCpuToTargetArch(target_cpu)) |
| logging.warning('Running: %s', ' '.join(shlex.quote(x) for x in cmd)) |
| print(_Colorize('All subsequent output is from adb_gdb script.', |
| colorama.Fore.YELLOW)) |
| os.execv(gdb_script_path, cmd) |
| |
| |
| def _RunLldb(device, |
| package_name, |
| debug_process_name, |
| pid, |
| output_directory, |
| port, |
| target_cpu=None, |
| ndk_dir=None, |
| lldb_server=None, |
| lldb=None, |
| verbose=None): |
| if not pid: |
| debug_process_name = _NormalizeProcessName(debug_process_name, package_name) |
| pid = device.GetApplicationPids(debug_process_name, at_most_one=True) |
| if not pid: |
| # Attaching lldb makes the app run so slow that it takes *minutes* to start |
| # up (as of 2018). Better to just fail than to start & attach. |
| raise Exception('App not running.') |
| |
| lldb_script_path = os.path.dirname(__file__) + '/connect_lldb.sh' |
| cmd = [ |
| lldb_script_path, |
| '--package-name=%s' % package_name, |
| '--output-directory=%s' % output_directory, |
| '--adb=%s' % adb_wrapper.AdbWrapper.GetAdbPath(), |
| '--device=%s' % device.serial, |
| '--pid=%s' % pid, |
| '--port=%d' % port, |
| ] |
| # Enable verbose output of connect_lldb.sh if it's set for this script. |
| if verbose: |
| cmd.append('--verbose') |
| if target_cpu: |
| cmd.append('--target-arch=%s' % _TargetCpuToTargetArch(target_cpu)) |
| if ndk_dir: |
| cmd.append('--ndk-dir=%s' % ndk_dir) |
| if lldb_server: |
| cmd.append('--lldb-server=%s' % lldb_server) |
| if lldb: |
| cmd.append('--lldb=%s' % lldb) |
| logging.warning('Running: %s', ' '.join(shlex.quote(x) for x in cmd)) |
| print( |
| _Colorize('All subsequent output is from connect_lldb.sh script.', |
| colorama.Fore.YELLOW)) |
| os.execv(lldb_script_path, cmd) |
| |
| |
| def _PrintPerDeviceOutput(devices, results, single_line=False): |
| for d, result in zip(devices, results): |
| if not single_line and d is not devices[0]: |
| sys.stdout.write('\n') |
| sys.stdout.write( |
| _Colorize('{} ({}):'.format(d, d.build_description), |
| colorama.Fore.YELLOW)) |
| sys.stdout.write(' ' if single_line else '\n') |
| yield result |
| |
| |
| def _RunMemUsage(devices, package_name, query_app=False): |
| cmd_args = ['dumpsys', 'meminfo'] |
| if not query_app: |
| cmd_args.append('--local') |
| |
| def mem_usage_helper(d): |
| ret = [] |
| for process in sorted(_GetPackageProcesses(d, package_name)): |
| meminfo = d.RunShellCommand(cmd_args + [str(process.pid)]) |
| ret.append((process.name, '\n'.join(meminfo))) |
| return ret |
| |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| all_results = parallel_devices.pMap(mem_usage_helper).pGet(None) |
| for result in _PrintPerDeviceOutput(devices, all_results): |
| if not result: |
| print('No processes found.') |
| else: |
| for name, usage in sorted(result): |
| print(_Colorize('==== Output of "dumpsys meminfo %s" ====' % name, |
| colorama.Fore.GREEN)) |
| print(usage) |
| |
| |
| def _DuHelper(device, path_spec, run_as=None): |
| """Runs "du -s -k |path_spec|" on |device| and returns parsed result. |
| |
| Args: |
| device: A DeviceUtils instance. |
| path_spec: The list of paths to run du on. May contain shell expansions |
| (will not be escaped). |
| run_as: Package name to run as, or None to run as shell user. If not None |
| and app is not android:debuggable (run-as fails), then command will be |
| run as root. |
| |
| Returns: |
| A dict of path->size in KiB containing all paths in |path_spec| that exist |
| on device. Paths that do not exist are silently ignored. |
| """ |
| # Example output for: du -s -k /data/data/org.chromium.chrome/{*,.*} |
| # 144 /data/data/org.chromium.chrome/cache |
| # 8 /data/data/org.chromium.chrome/files |
| # <snip> |
| # du: .*: No such file or directory |
| |
| # The -d flag works differently across android version, so use -s instead. |
| # Without the explicit 2>&1, stderr and stdout get combined at random :(. |
| cmd_str = 'du -s -k ' + path_spec + ' 2>&1' |
| lines = device.RunShellCommand(cmd_str, run_as=run_as, shell=True, |
| check_return=False) |
| output = '\n'.join(lines) |
| # run-as: Package 'com.android.chrome' is not debuggable |
| if output.startswith('run-as:'): |
| # check_return=False needed for when some paths in path_spec do not exist. |
| lines = device.RunShellCommand(cmd_str, as_root=True, shell=True, |
| check_return=False) |
| ret = {} |
| try: |
| for line in lines: |
| # du: .*: No such file or directory |
| if line.startswith('du:'): |
| continue |
| size, subpath = line.split(None, 1) |
| ret[subpath] = int(size) |
| return ret |
| except ValueError: |
| logging.error('du command was: %s', cmd_str) |
| logging.error('Failed to parse du output:\n%s', output) |
| raise |
| |
| |
| def _RunDiskUsage(devices, package_name): |
| # Measuring dex size is a bit complicated: |
| # https://source.android.com/devices/tech/dalvik/jit-compiler |
| # |
| # For KitKat and below: |
| # dumpsys package contains: |
| # dataDir=/data/data/org.chromium.chrome |
| # codePath=/data/app/org.chromium.chrome-1.apk |
| # resourcePath=/data/app/org.chromium.chrome-1.apk |
| # nativeLibraryPath=/data/app-lib/org.chromium.chrome-1 |
| # To measure odex: |
| # ls -l /data/dalvik-cache/data@app@org.chromium.chrome-1.apk@classes.dex |
| # |
| # For Android L and M (and maybe for N+ system apps): |
| # dumpsys package contains: |
| # codePath=/data/app/org.chromium.chrome-1 |
| # resourcePath=/data/app/org.chromium.chrome-1 |
| # legacyNativeLibraryDir=/data/app/org.chromium.chrome-1/lib |
| # To measure odex: |
| # # Option 1: |
| # /data/dalvik-cache/arm/data@app@org.chromium.chrome-1@base.apk@classes.dex |
| # /data/dalvik-cache/arm/data@app@org.chromium.chrome-1@base.apk@classes.vdex |
| # ls -l /data/dalvik-cache/profiles/org.chromium.chrome |
| # (these profiles all appear to be 0 bytes) |
| # # Option 2: |
| # ls -l /data/app/org.chromium.chrome-1/oat/arm/base.odex |
| # |
| # For Android N+: |
| # dumpsys package contains: |
| # dataDir=/data/user/0/org.chromium.chrome |
| # codePath=/data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w== |
| # resourcePath=/data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w== |
| # legacyNativeLibraryDir=/data/app/org.chromium.chrome-GUID/lib |
| # Instruction Set: arm |
| # path: /data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==/base.apk |
| # status: /data/.../oat/arm/base.odex[status=kOatUpToDate, compilation_f |
| # ilter=quicken] |
| # Instruction Set: arm64 |
| # path: /data/app/org.chromium.chrome-UuCZ71IE-i5sZgHAkU49_w==/base.apk |
| # status: /data/.../oat/arm64/base.odex[status=..., compilation_filter=q |
| # uicken] |
| # To measure odex: |
| # ls -l /data/app/.../oat/arm/base.odex |
| # ls -l /data/app/.../oat/arm/base.vdex (optional) |
| # To measure the correct odex size: |
| # cmd package compile -m speed org.chromium.chrome # For webview |
| # cmd package compile -m speed-profile org.chromium.chrome # For others |
| def disk_usage_helper(d): |
| package_output = '\n'.join(d.RunShellCommand( |
| ['dumpsys', 'package', package_name], check_return=True)) |
| # Does not return error when apk is not installed. |
| if not package_output or 'Unable to find package:' in package_output: |
| return None |
| |
| # Ignore system apks that have updates installed. |
| package_output = re.sub(r'Hidden system packages:.*?^\b', '', |
| package_output, flags=re.S | re.M) |
| |
| try: |
| data_dir = re.search(r'dataDir=(.*)', package_output).group(1) |
| code_path = re.search(r'codePath=(.*)', package_output).group(1) |
| lib_path = re.search(r'(?:legacyN|n)ativeLibrary(?:Dir|Path)=(.*)', |
| package_output).group(1) |
| except AttributeError as e: |
| raise Exception('Error parsing dumpsys output: ' + package_output) from e |
| |
| if code_path.startswith('/system'): |
| logging.warning('Measurement of system image apks can be innacurate') |
| |
| compilation_filters = set() |
| # Match "compilation_filter=value", where a line break can occur at any spot |
| # (refer to examples above). |
| awful_wrapping = r'\s*'.join('compilation_filter=') |
| for m in re.finditer(awful_wrapping + r'([\s\S]+?)[\],]', package_output): |
| compilation_filters.add(re.sub(r'\s+', '', m.group(1))) |
| # Starting Android Q, output looks like: |
| # arm: [status=speed-profile] [reason=install] |
| for m in re.finditer(r'\[status=(.+?)\]', package_output): |
| compilation_filters.add(m.group(1)) |
| compilation_filter = ','.join(sorted(compilation_filters)) |
| |
| data_dir_sizes = _DuHelper(d, '%s/{*,.*}' % data_dir, run_as=package_name) |
| # Measure code_cache separately since it can be large. |
| code_cache_sizes = {} |
| code_cache_dir = next( |
| (k for k in data_dir_sizes if k.endswith('/code_cache')), None) |
| if code_cache_dir: |
| data_dir_sizes.pop(code_cache_dir) |
| code_cache_sizes = _DuHelper(d, '%s/{*,.*}' % code_cache_dir, |
| run_as=package_name) |
| |
| apk_path_spec = code_path |
| if not apk_path_spec.endswith('.apk'): |
| apk_path_spec += '/*.apk' |
| apk_sizes = _DuHelper(d, apk_path_spec) |
| if lib_path.endswith('/lib'): |
| # Shows architecture subdirectory. |
| lib_sizes = _DuHelper(d, '%s/{*,.*}' % lib_path) |
| else: |
| lib_sizes = _DuHelper(d, lib_path) |
| |
| # Look at all possible locations for odex files. |
| odex_paths = [] |
| for apk_path in apk_sizes: |
| mangled_apk_path = apk_path[1:].replace('/', '@') |
| apk_basename = posixpath.basename(apk_path)[:-4] |
| for ext in ('dex', 'odex', 'vdex', 'art'): |
| # Easier to check all architectures than to determine active ones. |
| for arch in ('arm', 'arm64', 'x86', 'x86_64', 'mips', 'mips64'): |
| odex_paths.append( |
| '%s/oat/%s/%s.%s' % (code_path, arch, apk_basename, ext)) |
| # No app could possibly have more than 6 dex files. |
| for suffix in ('', '2', '3', '4', '5'): |
| odex_paths.append('/data/dalvik-cache/%s/%s@classes%s.%s' % ( |
| arch, mangled_apk_path, suffix, ext)) |
| # This path does not have |arch|, so don't repeat it for every arch. |
| if arch == 'arm': |
| odex_paths.append('/data/dalvik-cache/%s@classes%s.dex' % ( |
| mangled_apk_path, suffix)) |
| |
| odex_sizes = _DuHelper(d, ' '.join(shlex.quote(p) for p in odex_paths)) |
| |
| return (data_dir_sizes, code_cache_sizes, apk_sizes, lib_sizes, odex_sizes, |
| compilation_filter) |
| |
| def print_sizes(desc, sizes): |
| print('%s: %d KiB' % (desc, sum(sizes.values()))) |
| for path, size in sorted(sizes.items()): |
| print(' %s: %s KiB' % (path, size)) |
| |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| all_results = parallel_devices.pMap(disk_usage_helper).pGet(None) |
| for result in _PrintPerDeviceOutput(devices, all_results): |
| if not result: |
| print('APK is not installed.') |
| continue |
| |
| (data_dir_sizes, code_cache_sizes, apk_sizes, lib_sizes, odex_sizes, |
| compilation_filter) = result |
| total = sum(sum(sizes.values()) for sizes in result[:-1]) |
| |
| print_sizes('Apk', apk_sizes) |
| print_sizes('App Data (non-code cache)', data_dir_sizes) |
| print_sizes('App Data (code cache)', code_cache_sizes) |
| print_sizes('Native Libs', lib_sizes) |
| show_warning = compilation_filter and 'speed' not in compilation_filter |
| compilation_filter = compilation_filter or 'n/a' |
| print_sizes('odex (compilation_filter=%s)' % compilation_filter, odex_sizes) |
| if show_warning: |
| logging.warning('For a more realistic odex size, run:') |
| logging.warning(' %s compile-dex [speed|speed-profile]', sys.argv[0]) |
| print('Total: %s KiB (%.1f MiB)' % (total, total / 1024.0)) |
| |
| |
| class _LogcatProcessor: |
| ParsedLine = collections.namedtuple( |
| 'ParsedLine', |
| ['date', 'invokation_time', 'pid', 'tid', 'priority', 'tag', 'message']) |
| |
| class NativeStackSymbolizer: |
| """Buffers lines from native stacks and symbolizes them when done.""" |
| # E.g.: #06 pc 0x0000d519 /apex/com.android.runtime/lib/libart.so |
| # E.g.: #01 pc 00180c8d /data/data/.../lib/libbase.cr.so |
| _STACK_PATTERN = re.compile(r'\s*#\d+\s+(?:pc )?(0x)?[0-9a-f]{8,16}\s') |
| |
| def __init__(self, stack_script_context, print_func): |
| # To symbolize native stacks, we need to pass all lines at once. |
| self._stack_script_context = stack_script_context |
| self._print_func = print_func |
| self._crash_lines_buffer = None |
| |
| def _FlushLines(self): |
| """Prints queued lines after sending them through stack.py.""" |
| if self._crash_lines_buffer is None: |
| return |
| |
| crash_lines = self._crash_lines_buffer |
| self._crash_lines_buffer = None |
| with tempfile.NamedTemporaryFile(mode='w') as f: |
| f.writelines(x[0].message + '\n' for x in crash_lines) |
| f.flush() |
| proc = self._stack_script_context.Popen( |
| input_file=f.name, stdout=subprocess.PIPE) |
| lines = proc.communicate()[0].splitlines() |
| |
| for i, line in enumerate(lines): |
| parsed_line, dim = crash_lines[min(i, len(crash_lines) - 1)] |
| d = parsed_line._asdict() |
| d['message'] = line |
| parsed_line = _LogcatProcessor.ParsedLine(**d) |
| self._print_func(parsed_line, dim) |
| |
| def AddLine(self, parsed_line, dim): |
| # Assume all lines from DEBUG are stacks. |
| # Also look for "stack-looking" lines to catch manual stack prints. |
| # It's important to not buffer non-stack lines because stack.py does not |
| # pass them through. |
| is_crash_line = parsed_line.tag == 'DEBUG' or (self._STACK_PATTERN.match( |
| parsed_line.message)) |
| |
| if is_crash_line: |
| if self._crash_lines_buffer is None: |
| self._crash_lines_buffer = [] |
| self._crash_lines_buffer.append((parsed_line, dim)) |
| return |
| |
| self._FlushLines() |
| |
| self._print_func(parsed_line, dim) |
| |
| |
| # Logcat tags for messages that are generally relevant but are not from PIDs |
| # associated with the apk. |
| _ALLOWLISTED_TAGS = { |
| 'ActivityManager', # Shows activity lifecycle messages. |
| 'ActivityTaskManager', # More activity lifecycle messages. |
| 'AndroidRuntime', # Java crash dumps |
| 'AppZygoteInit', # Android's native application zygote support. |
| 'DEBUG', # Native crash dump. |
| } |
| |
| # Matches messages only on pre-L (Dalvik) that are spammy and unimportant. |
| _DALVIK_IGNORE_PATTERN = re.compile('|'.join([ |
| r'^Added shared lib', |
| r'^Could not find ', |
| r'^DexOpt:', |
| r'^GC_', |
| r'^Late-enabling CheckJNI', |
| r'^Link of class', |
| r'^No JNI_OnLoad found in', |
| r'^Trying to load lib', |
| r'^Unable to resolve superclass', |
| r'^VFY:', |
| r'^WAIT_', |
| ])) |
| |
| def __init__(self, |
| device, |
| package_name, |
| stack_script_context, |
| deobfuscate=None, |
| verbose=False, |
| exit_on_match=None, |
| extra_package_names=None): |
| self._device = device |
| self._package_name = package_name |
| self._extra_package_names = extra_package_names or [] |
| self._verbose = verbose |
| self._deobfuscator = deobfuscate |
| if exit_on_match is not None: |
| self._exit_on_match = re.compile(exit_on_match) |
| else: |
| self._exit_on_match = None |
| self._found_exit_match = False |
| self._native_stack_symbolizer = _LogcatProcessor.NativeStackSymbolizer( |
| stack_script_context, self._PrintParsedLine) |
| # Process ID for the app's main process (with no :name suffix). |
| self._primary_pid = None |
| # Set of all Process IDs that belong to the app. |
| self._my_pids = set() |
| # Set of all Process IDs that we've parsed at some point. |
| self._seen_pids = set() |
| # Start proc 22953:com.google.chromeremotedesktop/ |
| self._pid_pattern = re.compile(r'Start proc (\d+):{}/'.format(package_name)) |
| # START u0 {act=android.intent.action.MAIN \ |
| # cat=[android.intent.category.LAUNCHER] \ |
| # flg=0x10000000 pkg=com.google.chromeremotedesktop} from uid 2000 |
| self._start_pattern = re.compile(r'START .*(?:cmp|pkg)=' + package_name) |
| |
| self.nonce = 'Chromium apk_operations.py nonce={}'.format(random.random()) |
| # Holds lines buffered on start-up, before we find our nonce message. |
| self._initial_buffered_lines = [] |
| self._UpdateMyPids() |
| # Give preference to PID reported by "ps" over those found from |
| # _start_pattern. There can be multiple "Start proc" messages from prior |
| # runs of the app. |
| self._found_initial_pid = self._primary_pid is not None |
| # Retrieve any additional patterns that are relevant for the User. |
| self._user_defined_highlight = None |
| user_regex = os.environ.get('CHROMIUM_LOGCAT_HIGHLIGHT') |
| if user_regex: |
| self._user_defined_highlight = re.compile(user_regex) |
| if not self._user_defined_highlight: |
| print(_Colorize( |
| 'Rejecting invalid regular expression: {}'.format(user_regex), |
| colorama.Fore.RED + colorama.Style.BRIGHT)) |
| |
| def _UpdateMyPids(self): |
| # We intentionally do not clear self._my_pids to make sure that the |
| # ProcessLine method below also includes lines from processes which may |
| # have already exited. |
| self._primary_pid = None |
| for package_name in [self._package_name] + self._extra_package_names: |
| for process in _GetPackageProcesses(self._device, package_name): |
| # We take only the first "main" process found in order to account for |
| # possibly forked() processes. |
| if ':' not in process.name and self._primary_pid is None: |
| self._primary_pid = process.pid |
| self._my_pids.add(process.pid) |
| |
| def _GetPidStyle(self, pid, dim=False): |
| if pid == self._primary_pid: |
| return colorama.Fore.WHITE |
| if pid in self._my_pids: |
| # TODO(wnwen): Use one separate persistent color per process, pop LRU |
| return colorama.Fore.YELLOW |
| if dim: |
| return colorama.Style.DIM |
| return '' |
| |
| def _GetPriorityStyle(self, priority, dim=False): |
| # pylint:disable=no-self-use |
| if dim: |
| return '' |
| style = colorama.Fore.BLACK |
| if priority in ('E', 'F'): |
| style += colorama.Back.RED |
| elif priority == 'W': |
| style += colorama.Back.YELLOW |
| elif priority == 'I': |
| style += colorama.Back.GREEN |
| elif priority == 'D': |
| style += colorama.Back.BLUE |
| return style |
| |
| def _ParseLine(self, line): |
| tokens = line.split(None, 6) |
| |
| def consume_token_or_default(default): |
| return tokens.pop(0) if len(tokens) > 0 else default |
| |
| def consume_integer_token_or_default(default): |
| if len(tokens) == 0: |
| return default |
| |
| try: |
| return int(tokens.pop(0)) |
| except ValueError: |
| return default |
| |
| date = consume_token_or_default('') |
| invokation_time = consume_token_or_default('') |
| pid = consume_integer_token_or_default(-1) |
| tid = consume_integer_token_or_default(-1) |
| priority = consume_token_or_default('') |
| tag = consume_token_or_default('') |
| original_message = consume_token_or_default('') |
| |
| # Example: |
| # 09-19 06:35:51.113 9060 9154 W GCoreFlp: No location... |
| # 09-19 06:01:26.174 9060 10617 I Auth : [ReflectiveChannelBinder]... |
| # Parsing "GCoreFlp:" vs "Auth :", we only want tag to contain the word, |
| # and we don't want to keep the colon for the message. |
| if tag and tag[-1] == ':': |
| tag = tag[:-1] |
| elif len(original_message) > 2: |
| original_message = original_message[2:] |
| return self.ParsedLine( |
| date, invokation_time, pid, tid, priority, tag, original_message) |
| |
| def _PrintParsedLine(self, parsed_line, dim=False): |
| if self._exit_on_match and self._exit_on_match.search(parsed_line.message): |
| self._found_exit_match = True |
| |
| tid_style = colorama.Style.NORMAL |
| user_match = self._user_defined_highlight and ( |
| re.search(self._user_defined_highlight, parsed_line.tag) |
| or re.search(self._user_defined_highlight, parsed_line.message)) |
| |
| # Make the main thread bright. |
| if not dim and parsed_line.pid == parsed_line.tid: |
| tid_style = colorama.Style.BRIGHT |
| pid_style = self._GetPidStyle(parsed_line.pid, dim) |
| msg_style = pid_style if not user_match else (colorama.Fore.GREEN + |
| colorama.Style.BRIGHT) |
| # We have to pad before adding color as that changes the width of the tag. |
| pid_str = _Colorize('{:5}'.format(parsed_line.pid), pid_style) |
| tid_str = _Colorize('{:5}'.format(parsed_line.tid), tid_style) |
| tag = _Colorize('{:8}'.format(parsed_line.tag), |
| pid_style + ('' if dim else colorama.Style.BRIGHT)) |
| priority = _Colorize(parsed_line.priority, |
| self._GetPriorityStyle(parsed_line.priority)) |
| messages = [parsed_line.message] |
| if self._deobfuscator: |
| messages = self._deobfuscator.TransformLines(messages) |
| for message in messages: |
| message = _Colorize(message, msg_style) |
| sys.stdout.write('{} {} {} {} {} {}: {}\n'.format( |
| parsed_line.date, parsed_line.invokation_time, pid_str, tid_str, |
| priority, tag, message)) |
| |
| def _TriggerNonceFound(self): |
| # Once the nonce is hit, we have confidence that we know which lines |
| # belong to the current run of the app. Process all of the buffered lines. |
| if self._primary_pid: |
| for args in self._initial_buffered_lines: |
| self._native_stack_symbolizer.AddLine(*args) |
| self._initial_buffered_lines = None |
| self.nonce = None |
| |
| def FoundExitMatch(self): |
| return self._found_exit_match |
| |
| def ProcessLine(self, line): |
| if not line or line.startswith('------'): |
| return |
| |
| if self.nonce and self.nonce in line: |
| self._TriggerNonceFound() |
| |
| nonce_found = self.nonce is None |
| |
| log = self._ParseLine(line) |
| if log.pid not in self._seen_pids: |
| self._seen_pids.add(log.pid) |
| if nonce_found: |
| # Update list of owned PIDs each time a new PID is encountered. |
| self._UpdateMyPids() |
| |
| # Search for "Start proc $pid:$package_name/" message. |
| if not nonce_found: |
| # Capture logs before the nonce. Start with the most recent "am start". |
| if self._start_pattern.match(log.message): |
| self._initial_buffered_lines = [] |
| |
| # If we didn't find the PID via "ps", then extract it from log messages. |
| # This will happen if the app crashes too quickly. |
| if not self._found_initial_pid: |
| m = self._pid_pattern.match(log.message) |
| if m: |
| # Find the most recent "Start proc" line before the nonce. |
| # Track only the primary pid in this mode. |
| # The main use-case is to find app logs when no current PIDs exist. |
| # E.g.: When the app crashes on launch. |
| self._primary_pid = m.group(1) |
| self._my_pids.clear() |
| self._my_pids.add(m.group(1)) |
| |
| owned_pid = log.pid in self._my_pids |
| if owned_pid and not self._verbose and log.tag == 'dalvikvm': |
| if self._DALVIK_IGNORE_PATTERN.match(log.message): |
| return |
| |
| if owned_pid or self._verbose or (log.priority == 'F' or # Java crash dump |
| log.tag in self._ALLOWLISTED_TAGS): |
| if nonce_found: |
| self._native_stack_symbolizer.AddLine(log, not owned_pid) |
| else: |
| self._initial_buffered_lines.append((log, not owned_pid)) |
| |
| |
| def _RunLogcat(device, |
| package_name, |
| stack_script_context, |
| deobfuscate, |
| verbose, |
| exit_on_match=None, |
| extra_package_names=None): |
| logcat_processor = _LogcatProcessor(device, |
| package_name, |
| stack_script_context, |
| deobfuscate, |
| verbose, |
| exit_on_match=exit_on_match, |
| extra_package_names=extra_package_names) |
| device.RunShellCommand(['log', logcat_processor.nonce]) |
| for line in device.adb.Logcat(logcat_format='threadtime'): |
| try: |
| logcat_processor.ProcessLine(line) |
| if logcat_processor.FoundExitMatch(): |
| return |
| except: |
| sys.stderr.write('Failed to process line: ' + line + '\n') |
| # Skip stack trace for the common case of the adb server being |
| # restarted. |
| if 'unexpected EOF' in line: |
| sys.exit(1) |
| raise |
| |
| |
| def _GetPackageProcesses(device, package_name): |
| my_names = (package_name, package_name + '_zygote') |
| return [ |
| p for p in device.ListProcesses(package_name) |
| if p.name in my_names or p.name.startswith(package_name + ':') |
| ] |
| |
| |
| def _RunPs(devices, package_name): |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| all_processes = parallel_devices.pMap( |
| lambda d: _GetPackageProcesses(d, package_name)).pGet(None) |
| for processes in _PrintPerDeviceOutput(devices, all_processes): |
| if not processes: |
| print('No processes found.') |
| else: |
| proc_map = collections.defaultdict(list) |
| for p in processes: |
| proc_map[p.name].append(str(p.pid)) |
| for name, pids in sorted(proc_map.items()): |
| print(name, ','.join(pids)) |
| |
| |
| def _RunShell(devices, package_name, cmd): |
| if cmd: |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| outputs = parallel_devices.RunShellCommand( |
| cmd, run_as=package_name).pGet(None) |
| for output in _PrintPerDeviceOutput(devices, outputs): |
| for line in output: |
| print(line) |
| else: |
| adb_path = adb_wrapper.AdbWrapper.GetAdbPath() |
| cmd = [adb_path, '-s', devices[0].serial, 'shell'] |
| # Pre-N devices do not support -t flag. |
| if devices[0].build_version_sdk >= version_codes.NOUGAT: |
| cmd += ['-t', 'run-as', package_name] |
| else: |
| print('Upon entering the shell, run:') |
| print('run-as', package_name) |
| print() |
| os.execv(adb_path, cmd) |
| |
| |
| def _RunCompileDex(devices, package_name, compilation_filter): |
| cmd = ['cmd', 'package', 'compile', '-f', '-m', compilation_filter, |
| package_name] |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| outputs = parallel_devices.RunShellCommand(cmd, timeout=120).pGet(None) |
| for output in _PrintPerDeviceOutput(devices, outputs): |
| for line in output: |
| print(line) |
| |
| |
| def _RunProfile(device, package_name, host_build_directory, pprof_out_path, |
| process_specifier, thread_specifier, events, extra_args): |
| simpleperf.PrepareDevice(device) |
| device_simpleperf_path = simpleperf.InstallSimpleperf(device, package_name) |
| with tempfile.NamedTemporaryFile() as fh: |
| host_simpleperf_out_path = fh.name |
| |
| with simpleperf.RunSimpleperf(device, device_simpleperf_path, package_name, |
| process_specifier, thread_specifier, |
| events, extra_args, host_simpleperf_out_path): |
| sys.stdout.write('Profiler is running; press Enter to stop...\n') |
| sys.stdin.read(1) |
| sys.stdout.write('Post-processing data...\n') |
| |
| simpleperf.ConvertSimpleperfToPprof(host_simpleperf_out_path, |
| host_build_directory, pprof_out_path) |
| print(textwrap.dedent(""" |
| Profile data written to %(s)s. |
| |
| To view profile as a call graph in browser: |
| pprof -web %(s)s |
| |
| To print the hottest methods: |
| pprof -top %(s)s |
| |
| pprof has many useful customization options; `pprof --help` for details. |
| """ % {'s': pprof_out_path})) |
| |
| |
| class _StackScriptContext: |
| """Maintains temporary files needed by stack.py.""" |
| |
| def __init__(self, |
| output_directory, |
| apk_path, |
| bundle_generation_info, |
| quiet=False): |
| self._output_directory = output_directory |
| self._apk_path = apk_path |
| self._bundle_generation_info = bundle_generation_info |
| self._staging_dir = None |
| self._quiet = quiet |
| |
| def _CreateStaging(self): |
| # In many cases, stack decoding requires APKs to map trace lines to native |
| # libraries. Create a temporary directory, and either unpack a bundle's |
| # APKS into it, or simply symlink the standalone APK into it. This |
| # provides an unambiguous set of APK files for the stack decoding process |
| # to inspect. |
| logging.debug('Creating stack staging directory') |
| self._staging_dir = tempfile.mkdtemp() |
| bundle_generation_info = self._bundle_generation_info |
| |
| if bundle_generation_info: |
| # TODO(wnwen): Use apk_helper instead. |
| _GenerateBundleApks(bundle_generation_info) |
| logging.debug('Extracting .apks file') |
| with zipfile.ZipFile(bundle_generation_info.bundle_apks_path, 'r') as z: |
| files_to_extract = [ |
| f for f in z.namelist() if f.endswith('-master.apk') |
| ] |
| z.extractall(self._staging_dir, files_to_extract) |
| elif self._apk_path: |
| # Otherwise an incremental APK and an empty apks directory is correct. |
| output = os.path.join(self._staging_dir, os.path.basename(self._apk_path)) |
| os.symlink(self._apk_path, output) |
| |
| def Close(self): |
| if self._staging_dir: |
| logging.debug('Clearing stack staging directory') |
| shutil.rmtree(self._staging_dir) |
| self._staging_dir = None |
| |
| def Popen(self, input_file=None, **kwargs): |
| if self._staging_dir is None: |
| self._CreateStaging() |
| stack_script = os.path.join( |
| constants.host_paths.ANDROID_PLATFORM_DEVELOPMENT_SCRIPTS_PATH, |
| 'stack.py') |
| cmd = [ |
| stack_script, '--output-directory', self._output_directory, |
| '--apks-directory', self._staging_dir |
| ] |
| if self._quiet: |
| cmd.append('--quiet') |
| if input_file: |
| cmd.append(input_file) |
| logging.info('Running: %s', shlex.join(cmd)) |
| return subprocess.Popen(cmd, universal_newlines=True, **kwargs) |
| |
| |
| def _GenerateAvailableDevicesMessage(devices): |
| devices_obj = device_utils.DeviceUtils.parallel(devices) |
| descriptions = devices_obj.pMap(lambda d: d.build_description).pGet(None) |
| msg = 'Available devices:\n' |
| for d, desc in zip(devices, descriptions): |
| msg += ' %s (%s)\n' % (d, desc) |
| return msg |
| |
| |
| # TODO(agrieve):add "--all" in the MultipleDevicesError message and use it here. |
| def _GenerateMissingAllFlagMessage(devices): |
| return ('More than one device available. Use --all to select all devices, ' + |
| 'or use --device to select a device by serial.\n\n' + |
| _GenerateAvailableDevicesMessage(devices)) |
| |
| |
| def _DisplayArgs(devices, command_line_flags_file): |
| def flags_helper(d): |
| changer = flag_changer.FlagChanger(d, command_line_flags_file) |
| return changer.GetCurrentFlags() |
| |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| outputs = parallel_devices.pMap(flags_helper).pGet(None) |
| print('Existing flags per-device (via /data/local/tmp/{}):'.format( |
| command_line_flags_file)) |
| for flags in _PrintPerDeviceOutput(devices, outputs, single_line=True): |
| quoted_flags = ' '.join(shlex.quote(f) for f in flags) |
| print(quoted_flags or 'No flags set.') |
| |
| |
| def _DeviceCachePath(device, output_directory): |
| file_name = 'device_cache_%s.json' % device.serial |
| return os.path.join(output_directory, file_name) |
| |
| |
| def _LoadDeviceCaches(devices, output_directory): |
| if not output_directory: |
| return |
| for d in devices: |
| cache_path = _DeviceCachePath(d, output_directory) |
| if os.path.exists(cache_path): |
| logging.debug('Using device cache: %s', cache_path) |
| with open(cache_path) as f: |
| d.LoadCacheData(f.read()) |
| # Delete the cached file so that any exceptions cause it to be cleared. |
| os.unlink(cache_path) |
| else: |
| logging.debug('No cache present for device: %s', d) |
| |
| |
| def _SaveDeviceCaches(devices, output_directory): |
| if not output_directory: |
| return |
| for d in devices: |
| cache_path = _DeviceCachePath(d, output_directory) |
| with open(cache_path, 'w') as f: |
| f.write(d.DumpCacheData()) |
| logging.info('Wrote device cache: %s', cache_path) |
| |
| |
| class _Command: |
| name = None |
| description = None |
| long_description = None |
| needs_package_name = False |
| needs_output_directory = False |
| needs_apk_helper = False |
| supports_incremental = False |
| accepts_command_line_flags = False |
| accepts_args = False |
| need_device_args = True |
| all_devices_by_default = False |
| calls_exec = False |
| supports_multiple_devices = True |
| |
| def __init__(self, from_wrapper_script, is_bundle, is_test_apk): |
| self._parser = None |
| self._from_wrapper_script = from_wrapper_script |
| self.args = None |
| self.apk_helper = None |
| self.additional_apk_helpers = None |
| self.install_dict = None |
| self.devices = None |
| self.is_bundle = is_bundle |
| self.is_test_apk = is_test_apk |
| self.bundle_generation_info = None |
| # Only support incremental install from APK wrapper scripts. |
| if is_bundle or not from_wrapper_script: |
| self.supports_incremental = False |
| |
| def RegisterBundleGenerationInfo(self, bundle_generation_info): |
| self.bundle_generation_info = bundle_generation_info |
| |
| def _RegisterExtraArgs(self, group): |
| pass |
| |
| def RegisterArgs(self, parser): |
| subp = parser.add_parser( |
| self.name, help=self.description, |
| description=self.long_description or self.description, |
| formatter_class=argparse.RawDescriptionHelpFormatter) |
| self._parser = subp |
| subp.set_defaults(command=self) |
| if self.need_device_args: |
| subp.add_argument('--all', |
| action='store_true', |
| default=self.all_devices_by_default, |
| help='Operate on all connected devices.',) |
| subp.add_argument('-d', |
| '--device', |
| action='append', |
| default=[], |
| dest='devices', |
| help='Target device for script to work on. Enter ' |
| 'multiple times for multiple devices.') |
| subp.add_argument('-v', |
| '--verbose', |
| action='count', |
| default=0, |
| dest='verbose_count', |
| help='Verbose level (multiple times for more)') |
| group = subp.add_argument_group('%s arguments' % self.name) |
| |
| if self.needs_package_name: |
| # Three cases to consider here, since later code assumes |
| # self.args.package_name always exists, even if None: |
| # |
| # - Called from a bundle wrapper script, the package_name is already |
| # set through parser.set_defaults(), so don't call add_argument() |
| # to avoid overriding its value. |
| # |
| # - Called from an apk wrapper script. The --package-name argument |
| # should not appear, but self.args.package_name will be gleaned from |
| # the --apk-path file later. |
| # |
| # - Called directly, then --package-name is required on the command-line. |
| # |
| if not self.is_bundle: |
| group.add_argument( |
| '--package-name', |
| help=argparse.SUPPRESS if self._from_wrapper_script else ( |
| "App's package name.")) |
| |
| if self.needs_apk_helper or self.needs_package_name: |
| # Adding this argument to the subparser would override the set_defaults() |
| # value set by on the parent parser (even if None). |
| if not self._from_wrapper_script and not self.is_bundle: |
| group.add_argument( |
| '--apk-path', required=self.needs_apk_helper, help='Path to .apk') |
| |
| if self.supports_incremental: |
| group.add_argument('--incremental', |
| action='store_true', |
| default=False, |
| help='Always install an incremental apk.') |
| group.add_argument('--non-incremental', |
| action='store_true', |
| default=False, |
| help='Always install a non-incremental apk.') |
| |
| # accepts_command_line_flags and accepts_args are mutually exclusive. |
| # argparse will throw if they are both set. |
| if self.accepts_command_line_flags: |
| group.add_argument( |
| '--args', help='Command-line flags. Use = to assign args.') |
| |
| if self.accepts_args: |
| group.add_argument( |
| '--args', help='Extra arguments. Use = to assign args') |
| |
| if not self._from_wrapper_script and self.accepts_command_line_flags: |
| # Provided by wrapper scripts. |
| group.add_argument( |
| '--command-line-flags-file', |
| help='Name of the command-line flags file') |
| |
| self._RegisterExtraArgs(group) |
| |
| def _CreateApkHelpers(self, args, incremental_apk_path, install_dict): |
| """Returns true iff self.apk_helper was created and assigned.""" |
| if self.apk_helper is None: |
| if args.apk_path: |
| self.apk_helper = apk_helper.ToHelper(args.apk_path) |
| elif incremental_apk_path: |
| self.install_dict = install_dict |
| self.apk_helper = apk_helper.ToHelper(incremental_apk_path) |
| elif self.is_bundle: |
| _GenerateBundleApks(self.bundle_generation_info) |
| self.apk_helper = apk_helper.ToHelper( |
| self.bundle_generation_info.bundle_apks_path) |
| if args.additional_apk_paths and self.additional_apk_helpers is None: |
| self.additional_apk_helpers = [ |
| apk_helper.ToHelper(apk_path) |
| for apk_path in args.additional_apk_paths |
| ] |
| return self.apk_helper is not None |
| |
| def _FindSupportedDevices(self, devices): |
| """Returns supported devices and reasons for each not supported one.""" |
| app_abis = self.apk_helper.GetAbis() |
| calling_script_name = os.path.basename(sys.argv[0]) |
| is_webview = 'webview' in calling_script_name |
| requires_32_bit = self.apk_helper.Get32BitAbiOverride() == '0xffffffff' |
| logging.debug('App supports (requires 32bit: %r, is webview: %r): %r', |
| requires_32_bit, is_webview, app_abis) |
| # Webview 32_64 targets can work even on 64-bit only devices since only the |
| # webview library in the target needs the correct bitness. |
| if requires_32_bit and not is_webview: |
| app_abis = [abi for abi in app_abis if '64' not in abi] |
| logging.debug('App supports (filtered): %r', app_abis) |
| if not app_abis: |
| # The app does not have any native libs, so all devices can support it. |
| return devices, None |
| fully_supported = [] |
| not_supported_reasons = {} |
| for device in devices: |
| device_abis = device.GetSupportedABIs() |
| device_primary_abi = device_abis[0] |
| logging.debug('Device primary: %s', device_primary_abi) |
| logging.debug('Device supports: %r', device_abis) |
| |
| # x86/x86_64 emulators sometimes advertises arm support but arm builds do |
| # not work on them. Thus these non-functional ABIs need to be filtered out |
| # here to avoid resulting in hard to understand runtime failures. |
| if device_primary_abi in ('x86', 'x86_64'): |
| device_abis = [abi for abi in device_abis if not abi.startswith('arm')] |
| logging.debug('Device supports (filtered): %r', device_abis) |
| |
| if any(abi in app_abis for abi in device_abis): |
| fully_supported.append(device) |
| else: # No common supported ABIs between the device and app. |
| if device_primary_abi == 'x86': |
| target_cpu = 'x86' |
| elif device_primary_abi == 'x86_64': |
| target_cpu = 'x64' |
| elif device_primary_abi.startswith('arm64'): |
| target_cpu = 'arm64' |
| elif device_primary_abi.startswith('armeabi'): |
| target_cpu = 'arm' |
| else: |
| target_cpu = '<something else>' |
| # pylint: disable=line-too-long |
| native_lib_link = 'https://chromium.googlesource.com/chromium/src/+/main/docs/android_native_libraries.md' |
| not_supported_reasons[device.serial] = ( |
| f"none of the app's ABIs ({','.join(app_abis)}) match this " |
| f"device's ABIs ({','.join(device_abis)}), you may need to set " |
| f'target_cpu="{target_cpu}" in your args.gn. If you already set ' |
| 'the target_cpu arg, you may need to use one of the _64 or _64_32 ' |
| f'targets, see {native_lib_link} for more details.') |
| return fully_supported, not_supported_reasons |
| |
| def ProcessArgs(self, args): |
| self.args = args |
| # Ensure these keys always exist. They are set by wrapper scripts, but not |
| # always added when not using wrapper scripts. |
| args.__dict__.setdefault('apk_path', None) |
| args.__dict__.setdefault('incremental_json', None) |
| |
| incremental_apk_path = None |
| install_dict = None |
| if args.incremental_json and not (self.supports_incremental and |
| args.non_incremental): |
| with open(args.incremental_json) as f: |
| install_dict = json.load(f) |
| incremental_apk_path = os.path.join(args.output_directory, |
| install_dict['apk_path']) |
| if not os.path.exists(incremental_apk_path): |
| incremental_apk_path = None |
| |
| if self.supports_incremental: |
| if args.incremental and args.non_incremental: |
| self._parser.error('Must use only one of --incremental and ' |
| '--non-incremental') |
| elif args.non_incremental: |
| if not args.apk_path: |
| self._parser.error('Apk has not been built.') |
| elif args.incremental: |
| if not incremental_apk_path: |
| self._parser.error('Incremental apk has not been built.') |
| args.apk_path = None |
| |
| if args.apk_path and incremental_apk_path: |
| self._parser.error('Both incremental and non-incremental apks exist. ' |
| 'Select using --incremental or --non-incremental') |
| |
| |
| # Gate apk_helper creation with _CreateApkHelpers since for bundles it takes |
| # a while to unpack the apks file from the aab file, so avoid this slowdown |
| # for simple commands that don't need apk_helper. |
| if self.needs_apk_helper: |
| if not self._CreateApkHelpers(args, incremental_apk_path, install_dict): |
| self._parser.error('App is not built.') |
| |
| if self.needs_package_name and not args.package_name: |
| if self._CreateApkHelpers(args, incremental_apk_path, install_dict): |
| args.package_name = self.apk_helper.GetPackageName() |
| elif self._from_wrapper_script: |
| self._parser.error('App is not built.') |
| else: |
| self._parser.error('One of --package-name or --apk-path is required.') |
| |
| self.devices = [] |
| if self.need_device_args: |
| # Avoid filtering by ABIs with catapult since some x86 or x86_64 emulators |
| # can still work with the right target_cpu GN arg and the right targets. |
| # Doing this manually allows us to output more informative warnings to |
| # help devs towards the right course, see: https://crbug.com/1335139 |
| available_devices = device_utils.DeviceUtils.HealthyDevices( |
| device_arg=args.devices, |
| enable_device_files_cache=bool(args.output_directory), |
| default_retries=0) |
| if not available_devices: |
| raise Exception('Cannot find any available devices.') |
| |
| if not self._CreateApkHelpers(args, incremental_apk_path, install_dict): |
| self.devices = available_devices |
| else: |
| fully_supported, not_supported_reasons = self._FindSupportedDevices( |
| available_devices) |
| if fully_supported: |
| self.devices = fully_supported |
| else: |
| reason_string = '\n'.join( |
| 'The device (serial={}) is not supported because {}'.format( |
| serial, reason) |
| for serial, reason in not_supported_reasons.items()) |
| raise Exception('Cannot find any supported devices for this app.\n\n' |
| f'{reason_string}') |
| |
| # TODO(agrieve): Device cache should not depend on output directory. |
| # Maybe put into /tmp? |
| _LoadDeviceCaches(self.devices, args.output_directory) |
| |
| try: |
| if len(self.devices) > 1: |
| if not self.supports_multiple_devices: |
| self._parser.error(device_errors.MultipleDevicesError(self.devices)) |
| if not args.all and not args.devices: |
| self._parser.error(_GenerateMissingAllFlagMessage(self.devices)) |
| # Save cache now if command will not get a chance to afterwards. |
| if self.calls_exec: |
| _SaveDeviceCaches(self.devices, args.output_directory) |
| except: |
| _SaveDeviceCaches(self.devices, args.output_directory) |
| raise |
| |
| |
| class _DevicesCommand(_Command): |
| name = 'devices' |
| description = 'Describe attached devices.' |
| all_devices_by_default = True |
| |
| def Run(self): |
| print(_GenerateAvailableDevicesMessage(self.devices)) |
| |
| |
| class _PackageInfoCommand(_Command): |
| name = 'package-info' |
| description = 'Show various attributes of this app.' |
| need_device_args = False |
| needs_package_name = True |
| needs_apk_helper = True |
| |
| def Run(self): |
| # Format all (even ints) as strings, to handle cases where APIs return None |
| print('Package name: "%s"' % self.args.package_name) |
| print('versionCode: %s' % self.apk_helper.GetVersionCode()) |
| print('versionName: "%s"' % self.apk_helper.GetVersionName()) |
| print('minSdkVersion: %s' % self.apk_helper.GetMinSdkVersion()) |
| print('targetSdkVersion: %s' % self.apk_helper.GetTargetSdkVersion()) |
| print('Supported ABIs: %r' % self.apk_helper.GetAbis()) |
| |
| |
| class _InstallCommand(_Command): |
| name = 'install' |
| description = 'Installs the APK or bundle to one or more devices.' |
| needs_apk_helper = True |
| supports_incremental = True |
| default_modules = [] |
| |
| def _RegisterExtraArgs(self, group): |
| if self.is_bundle: |
| group.add_argument( |
| '-m', |
| '--module', |
| action='append', |
| default=self.default_modules, |
| help='Module to install. Can be specified multiple times.') |
| group.add_argument( |
| '-f', |
| '--fake', |
| action='append', |
| default=[], |
| help='Fake bundle module install. Can be specified multiple times. ' |
| 'Requires \'-m {0}\' to be given, and \'-f {0}\' is illegal.'.format( |
| BASE_MODULE)) |
| # Add even if |self.default_modules| is empty, for consistency. |
| group.add_argument('--no-module', |
| action='append', |
| choices=self.default_modules, |
| default=[], |
| help='Module to exclude from default install.') |
| |
| def Run(self): |
| if self.additional_apk_helpers: |
| for additional_apk_helper in self.additional_apk_helpers: |
| _InstallApk(self.devices, additional_apk_helper, None) |
| if self.is_bundle: |
| modules = list( |
| set(self.args.module) - set(self.args.no_module) - |
| set(self.args.fake)) |
| _InstallBundle(self.devices, self.apk_helper, modules, self.args.fake) |
| else: |
| _InstallApk(self.devices, self.apk_helper, self.install_dict) |
| |
| |
| class _UninstallCommand(_Command): |
| name = 'uninstall' |
| description = 'Removes the APK or bundle from one or more devices.' |
| needs_package_name = True |
| |
| def Run(self): |
| _UninstallApk(self.devices, self.install_dict, self.args.package_name) |
| |
| |
| class _SetWebViewProviderCommand(_Command): |
| name = 'set-webview-provider' |
| description = ("Sets the device's WebView provider to this APK's " |
| "package name.") |
| needs_package_name = True |
| needs_apk_helper = True |
| |
| def Run(self): |
| if not _IsWebViewProvider(self.apk_helper): |
| raise Exception('This package does not have a WebViewLibrary meta-data ' |
| 'tag. Are you sure it contains a WebView implementation?') |
| _SetWebViewProvider(self.devices, self.args.package_name) |
| |
| |
| class _LaunchCommand(_Command): |
| name = 'launch' |
| description = ('Sends a launch intent for the APK or bundle after first ' |
| 'writing the command-line flags file.') |
| needs_package_name = True |
| accepts_command_line_flags = True |
| all_devices_by_default = True |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument('-w', '--wait-for-java-debugger', action='store_true', |
| help='Pause execution until debugger attaches. Applies ' |
| 'only to the main process. To have renderers wait, ' |
| 'use --args="--renderer-wait-for-java-debugger"') |
| group.add_argument('--debug-process-name', |
| help='Name of the process to debug. ' |
| 'E.g. "privileged_process0", or "foo.bar:baz"') |
| group.add_argument('--nokill', action='store_true', |
| help='Do not set the debug-app, nor set command-line ' |
| 'flags. Useful to load a URL without having the ' |
| 'app restart.') |
| group.add_argument('url', nargs='?', help='A URL to launch with.') |
| |
| def Run(self): |
| if self.is_test_apk: |
| raise Exception('Use the bin/run_* scripts to run test apks.') |
| _LaunchUrl(self.devices, |
| self.args.package_name, |
| argv=self.args.args, |
| command_line_flags_file=self.args.command_line_flags_file, |
| url=self.args.url, |
| wait_for_java_debugger=self.args.wait_for_java_debugger, |
| debug_process_name=self.args.debug_process_name, |
| nokill=self.args.nokill) |
| |
| |
| class _StopCommand(_Command): |
| name = 'stop' |
| description = 'Force-stops the app.' |
| needs_package_name = True |
| all_devices_by_default = True |
| |
| def Run(self): |
| device_utils.DeviceUtils.parallel(self.devices).ForceStop( |
| self.args.package_name) |
| |
| |
| class _ClearDataCommand(_Command): |
| name = 'clear-data' |
| descriptions = 'Clears all app data.' |
| needs_package_name = True |
| all_devices_by_default = True |
| |
| def Run(self): |
| device_utils.DeviceUtils.parallel(self.devices).ClearApplicationState( |
| self.args.package_name) |
| |
| |
| class _ArgvCommand(_Command): |
| name = 'argv' |
| description = 'Display and optionally update command-line flags file.' |
| needs_package_name = True |
| accepts_command_line_flags = True |
| all_devices_by_default = True |
| |
| def Run(self): |
| _ChangeFlags(self.devices, self.args.args, |
| self.args.command_line_flags_file) |
| |
| |
| class _GdbCommand(_Command): |
| name = 'gdb' |
| description = 'Runs //build/android/adb_gdb with apk-specific args.' |
| long_description = description + """ |
| |
| To attach to a process other than the APK's main process, use --pid=1234. |
| To list all PIDs, use the "ps" command. |
| |
| If no apk process is currently running, sends a launch intent. |
| """ |
| needs_package_name = True |
| needs_output_directory = True |
| calls_exec = True |
| supports_multiple_devices = False |
| |
| def Run(self): |
| _RunGdb(self.devices[0], self.args.package_name, |
| self.args.debug_process_name, self.args.pid, |
| self.args.output_directory, self.args.target_cpu, self.args.port, |
| self.args.ide, bool(self.args.verbose_count)) |
| |
| def _RegisterExtraArgs(self, group): |
| pid_group = group.add_mutually_exclusive_group() |
| pid_group.add_argument('--debug-process-name', |
| help='Name of the process to attach to. ' |
| 'E.g. "privileged_process0", or "foo.bar:baz"') |
| pid_group.add_argument('--pid', |
| help='The process ID to attach to. Defaults to ' |
| 'the main process for the package.') |
| group.add_argument('--ide', action='store_true', |
| help='Rather than enter a gdb prompt, set up the ' |
| 'gdb connection and wait for an IDE to ' |
| 'connect.') |
| # Same default port that ndk-gdb.py uses. |
| group.add_argument('--port', type=int, default=5039, |
| help='Use the given port for the GDB connection') |
| |
| |
| class _LldbCommand(_Command): |
| name = 'lldb' |
| description = 'Runs //build/android/connect_lldb.sh with apk-specific args.' |
| long_description = description + """ |
| |
| To attach to a process other than the APK's main process, use --pid=1234. |
| To list all PIDs, use the "ps" command. |
| |
| If no apk process is currently running, sends a launch intent. |
| """ |
| needs_package_name = True |
| needs_output_directory = True |
| calls_exec = True |
| supports_multiple_devices = False |
| |
| def Run(self): |
| _RunLldb(device=self.devices[0], |
| package_name=self.args.package_name, |
| debug_process_name=self.args.debug_process_name, |
| pid=self.args.pid, |
| output_directory=self.args.output_directory, |
| port=self.args.port, |
| target_cpu=self.args.target_cpu, |
| ndk_dir=self.args.ndk_dir, |
| lldb_server=self.args.lldb_server, |
| lldb=self.args.lldb, |
| verbose=bool(self.args.verbose_count)) |
| |
| def _RegisterExtraArgs(self, group): |
| pid_group = group.add_mutually_exclusive_group() |
| pid_group.add_argument('--debug-process-name', |
| help='Name of the process to attach to. ' |
| 'E.g. "privileged_process0", or "foo.bar:baz"') |
| pid_group.add_argument('--pid', |
| help='The process ID to attach to. Defaults to ' |
| 'the main process for the package.') |
| group.add_argument('--ndk-dir', |
| help='Select alternative NDK root directory.') |
| group.add_argument('--lldb-server', |
| help='Select alternative on-device lldb-server.') |
| group.add_argument('--lldb', help='Select alternative client lldb.sh.') |
| # Same default port that ndk-gdb.py uses. |
| group.add_argument('--port', |
| type=int, |
| default=5039, |
| help='Use the given port for the LLDB connection') |
| |
| |
| class _LogcatCommand(_Command): |
| name = 'logcat' |
| description = 'Runs "adb logcat" with filters relevant the current APK.' |
| long_description = description + """ |
| |
| "Relevant filters" means: |
| * Log messages from processes belonging to the apk, |
| * Plus log messages from log tags: ActivityManager|DEBUG, |
| * Plus fatal logs from any process, |
| * Minus spamy dalvikvm logs (for pre-L devices). |
| |
| Colors: |
| * Primary process is white |
| * Other processes (gpu, renderer) are yellow |
| * Non-apk processes are grey |
| * UI thread has a bolded Thread-ID |
| |
| Java stack traces are detected and deobfuscated (for release builds). |
| |
| To disable filtering, (but keep coloring), use --verbose. |
| """ |
| needs_package_name = True |
| supports_multiple_devices = False |
| |
| def Run(self): |
| deobfuscate = None |
| if self.args.proguard_mapping_path and not self.args.no_deobfuscate: |
| deobfuscate = deobfuscator.Deobfuscator(self.args.proguard_mapping_path) |
| |
| stack_script_context = _StackScriptContext( |
| self.args.output_directory, |
| self.args.apk_path, |
| self.bundle_generation_info, |
| quiet=True) |
| |
| extra_package_names = [] |
| if self.is_test_apk and self.additional_apk_helpers: |
| for additional_apk_helper in self.additional_apk_helpers: |
| extra_package_names.append(additional_apk_helper.GetPackageName()) |
| |
| try: |
| _RunLogcat(self.devices[0], |
| self.args.package_name, |
| stack_script_context, |
| deobfuscate, |
| bool(self.args.verbose_count), |
| self.args.exit_on_match, |
| extra_package_names=extra_package_names) |
| except KeyboardInterrupt: |
| pass # Don't show stack trace upon Ctrl-C |
| finally: |
| stack_script_context.Close() |
| if deobfuscate: |
| deobfuscate.Close() |
| |
| def _RegisterExtraArgs(self, group): |
| if self._from_wrapper_script: |
| group.add_argument('--no-deobfuscate', action='store_true', |
| help='Disables ProGuard deobfuscation of logcat.') |
| else: |
| group.set_defaults(no_deobfuscate=False) |
| group.add_argument('--proguard-mapping-path', |
| help='Path to ProGuard map (enables deobfuscation)') |
| group.add_argument('--exit-on-match', |
| help='Exits logcat when a message matches this regex.') |
| |
| |
| class _PsCommand(_Command): |
| name = 'ps' |
| description = 'Show PIDs of any APK processes currently running.' |
| needs_package_name = True |
| all_devices_by_default = True |
| |
| def Run(self): |
| _RunPs(self.devices, self.args.package_name) |
| |
| |
| class _DiskUsageCommand(_Command): |
| name = 'disk-usage' |
| description = 'Show how much device storage is being consumed by the app.' |
| needs_package_name = True |
| all_devices_by_default = True |
| |
| def Run(self): |
| _RunDiskUsage(self.devices, self.args.package_name) |
| |
| |
| class _MemUsageCommand(_Command): |
| name = 'mem-usage' |
| description = 'Show memory usage of currently running APK processes.' |
| needs_package_name = True |
| all_devices_by_default = True |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument('--query-app', action='store_true', |
| help='Do not add --local to "dumpsys meminfo". This will output ' |
| 'additional metrics (e.g. Context count), but also cause memory ' |
| 'to be used in order to gather the metrics.') |
| |
| def Run(self): |
| _RunMemUsage(self.devices, self.args.package_name, |
| query_app=self.args.query_app) |
| |
| |
| class _ShellCommand(_Command): |
| name = 'shell' |
| description = ('Same as "adb shell <command>", but runs as the apk\'s uid ' |
| '(via run-as). Useful for inspecting the app\'s data ' |
| 'directory.') |
| needs_package_name = True |
| |
| @property |
| def calls_exec(self): |
| return not self.args.cmd |
| |
| @property |
| def supports_multiple_devices(self): |
| return not self.args.cmd |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument( |
| 'cmd', nargs=argparse.REMAINDER, help='Command to run.') |
| |
| def Run(self): |
| _RunShell(self.devices, self.args.package_name, self.args.cmd) |
| |
| |
| class _CompileDexCommand(_Command): |
| name = 'compile-dex' |
| description = ('Applicable only for Android N+. Forces .odex files to be ' |
| 'compiled with the given compilation filter. To see existing ' |
| 'filter, use "disk-usage" command.') |
| needs_package_name = True |
| all_devices_by_default = True |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument( |
| 'compilation_filter', |
| choices=['verify', 'quicken', 'space-profile', 'space', |
| 'speed-profile', 'speed'], |
| help='For WebView/Monochrome, use "speed". For other apks, use ' |
| '"speed-profile".') |
| |
| def Run(self): |
| _RunCompileDex(self.devices, self.args.package_name, |
| self.args.compilation_filter) |
| |
| |
| class _PrintCertsCommand(_Command): |
| name = 'print-certs' |
| description = 'Print info about certificates used to sign this APK.' |
| need_device_args = False |
| needs_apk_helper = True |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument( |
| '--full-cert', |
| action='store_true', |
| help=("Print the certificate's full signature, Base64-encoded. " |
| "Useful when configuring an Android image's " |
| "config_webview_packages.xml.")) |
| |
| def Run(self): |
| keytool = os.path.join(_JAVA_HOME, 'bin', 'keytool') |
| pem_certificate_pattern = re.compile( |
| r'-+BEGIN CERTIFICATE-+([\r\n0-9A-Za-z+/=]+)-+END CERTIFICATE-+[\r\n]*') |
| if self.is_bundle: |
| # Bundles are not signed until converted to .apks. The wrapper scripts |
| # record which key will be used to sign though. |
| with tempfile.NamedTemporaryFile() as f: |
| logging.warning('Bundles are not signed until turned into .apk files.') |
| logging.warning('Showing signing info based on associated keystore.') |
| cmd = [ |
| keytool, '-exportcert', '-keystore', |
| self.bundle_generation_info.keystore_path, '-storepass', |
| self.bundle_generation_info.keystore_password, '-alias', |
| self.bundle_generation_info.keystore_alias, '-file', f.name |
| ] |
| logging.warning('Running: %s', shlex.join(cmd)) |
| subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
| cmd = [keytool, '-printcert', '-file', f.name] |
| logging.warning('Running: %s', shlex.join(cmd)) |
| subprocess.check_call(cmd) |
| if self.args.full_cert: |
| # Redirect stderr to hide a keytool warning about using non-standard |
| # keystore format. |
| cmd += ['-rfc'] |
| logging.warning('Running: %s', shlex.join(cmd)) |
| pem_encoded_certificate = subprocess.check_output( |
| cmd, stderr=subprocess.STDOUT).decode() |
| else: |
| |
| def run_apksigner(min_sdk_version): |
| cmd = [ |
| build_tools.GetPath('apksigner'), 'verify', '--min-sdk-version', |
| str(min_sdk_version), '--print-certs-pem', '--verbose', |
| self.apk_helper.path |
| ] |
| logging.warning('Running: %s', shlex.join(cmd)) |
| env = os.environ.copy() |
| env['PATH'] = os.path.pathsep.join( |
| [os.path.join(_JAVA_HOME, 'bin'), |
| env.get('PATH')]) |
| # Redirect stderr to hide verification failures (see explanation below). |
| return subprocess.check_output(cmd, |
| env=env, |
| universal_newlines=True, |
| stderr=subprocess.STDOUT) |
| |
| # apksigner's default behavior is nonintuitive: it will print "Verified |
| # using <scheme number>...: false" for any scheme which is obsolete for |
| # the APK's minSdkVersion even if it actually was signed with that scheme |
| # (ex. it prints "Verified using v1 scheme: false" for Monochrome because |
| # v1 was obsolete by N). To workaround this, we force apksigner to use the |
| # lowest possible minSdkVersion. We need to fallback to higher |
| # minSdkVersions in case the APK fails to verify for that minSdkVersion |
| # (which means the APK is genuinely not signed with that scheme). These |
| # SDK values are the highest SDK version before the next scheme is |
| # available: |
| versions = [ |
| version_codes.MARSHMALLOW, # before v2 launched in N |
| version_codes.OREO_MR1, # before v3 launched in P |
| version_codes.Q, # before v4 launched in R |
| version_codes.R, |
| ] |
| stdout = None |
| for min_sdk_version in versions: |
| try: |
| stdout = run_apksigner(min_sdk_version) |
| break |
| except subprocess.CalledProcessError: |
| # Doesn't verify with this min-sdk-version, so try again with a higher |
| # one |
| continue |
| if not stdout: |
| raise RuntimeError('apksigner was not able to verify APK') |
| |
| # Separate what the '--print-certs' flag would output vs. the additional |
| # signature output included by '--print-certs-pem'. The additional PEM |
| # output is only printed when self.args.full_cert is specified. |
| verification_hash_info = pem_certificate_pattern.sub('', stdout) |
| print(verification_hash_info) |
| if self.args.full_cert: |
| m = pem_certificate_pattern.search(stdout) |
| if not m: |
| raise Exception('apksigner did not print a certificate') |
| pem_encoded_certificate = m.group(0) |
| |
| |
| if self.args.full_cert: |
| m = pem_certificate_pattern.search(pem_encoded_certificate) |
| if not m: |
| raise Exception( |
| 'Unable to parse certificate:\n{}'.format(pem_encoded_certificate)) |
| signature = re.sub(r'[\r\n]+', '', m.group(1)) |
| print() |
| print('Full Signature:') |
| print(signature) |
| |
| |
| class _ProfileCommand(_Command): |
| name = 'profile' |
| description = ('Run the simpleperf sampling CPU profiler on the currently-' |
| 'running APK. If --args is used, the extra arguments will be ' |
| 'passed on to simpleperf; otherwise, the following default ' |
| 'arguments are used: -g -f 1000 -o /data/local/tmp/perf.data') |
| needs_package_name = True |
| needs_output_directory = True |
| supports_multiple_devices = False |
| accepts_args = True |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument( |
| '--profile-process', default='browser', |
| help=('Which process to profile. This may be a process name or pid ' |
| 'such as you would get from running `%s ps`; or ' |
| 'it can be one of (browser, renderer, gpu).' % sys.argv[0])) |
| group.add_argument( |
| '--profile-thread', default=None, |
| help=('(Optional) Profile only a single thread. This may be either a ' |
| 'thread ID such as you would get by running `adb shell ps -t` ' |
| '(pre-Oreo) or `adb shell ps -e -T` (Oreo and later); or it may ' |
| 'be one of (io, compositor, main, render), in which case ' |
| '--profile-process is also required. (Note that "render" thread ' |
| 'refers to a thread in the browser process that manages a ' |
| 'renderer; to profile the main thread of the renderer process, ' |
| 'use --profile-thread=main).')) |
| group.add_argument('--profile-output', default='profile.pb', |
| help='Output file for profiling data') |
| group.add_argument('--profile-events', default='cpu-cycles', |
| help=('A comma separated list of perf events to capture ' |
| '(e.g. \'cpu-cycles,branch-misses\'). Run ' |
| '`simpleperf list` on your device to see available ' |
| 'events.')) |
| |
| def Run(self): |
| extra_args = shlex.split(self.args.args or '') |
| _RunProfile(self.devices[0], self.args.package_name, |
| self.args.output_directory, self.args.profile_output, |
| self.args.profile_process, self.args.profile_thread, |
| self.args.profile_events, extra_args) |
| |
| |
| class _RunCommand(_InstallCommand, _LaunchCommand, _LogcatCommand): |
| name = 'run' |
| description = 'Install, launch, and show logcat (when targeting one device).' |
| all_devices_by_default = False |
| supports_multiple_devices = True |
| |
| def _RegisterExtraArgs(self, group): |
| _InstallCommand._RegisterExtraArgs(self, group) |
| _LaunchCommand._RegisterExtraArgs(self, group) |
| _LogcatCommand._RegisterExtraArgs(self, group) |
| group.add_argument('--no-logcat', action='store_true', |
| help='Install and launch, but do not enter logcat.') |
| |
| def Run(self): |
| if self.is_test_apk: |
| raise Exception('Use the bin/run_* scripts to run test apks.') |
| logging.warning('Installing...') |
| _InstallCommand.Run(self) |
| logging.warning('Sending launch intent...') |
| _LaunchCommand.Run(self) |
| if len(self.devices) == 1 and not self.args.no_logcat: |
| logging.warning('Entering logcat...') |
| _LogcatCommand.Run(self) |
| |
| |
| class _BuildBundleApks(_Command): |
| name = 'build-bundle-apks' |
| description = ('Build the .apks archive from an Android app bundle, and ' |
| 'optionally copy it to a specific destination.') |
| need_device_args = False |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument( |
| '--output-apks', required=True, help='Destination path for .apks file.') |
| group.add_argument( |
| '--minimal', |
| action='store_true', |
| help='Build .apks archive that targets the bundle\'s minSdkVersion and ' |
| 'contains only english splits. It still contains optional splits.') |
| group.add_argument( |
| '--sdk-version', help='The sdkVersion to build the .apks for.') |
| group.add_argument( |
| '--build-mode', |
| choices=app_bundle_utils.BUILD_APKS_MODES, |
| help='Specify which type of APKs archive to build. "default" ' |
| 'generates regular splits, "universal" generates an archive with a ' |
| 'single universal APK, "system" generates an archive with a system ' |
| 'image APK, while "system_compressed" generates a compressed system ' |
| 'APK, with an additional stub APK for the system image.') |
| group.add_argument( |
| '--optimize-for', |
| choices=app_bundle_utils.OPTIMIZE_FOR_OPTIONS, |
| help='Override split configuration.') |
| |
| def Run(self): |
| _GenerateBundleApks( |
| self.bundle_generation_info, |
| output_path=self.args.output_apks, |
| minimal=self.args.minimal, |
| minimal_sdk_version=self.args.sdk_version, |
| mode=self.args.build_mode, |
| optimize_for=self.args.optimize_for) |
| |
| |
| class _ManifestCommand(_Command): |
| name = 'dump-manifest' |
| description = 'Dump the android manifest as XML, to stdout.' |
| need_device_args = False |
| needs_apk_helper = True |
| |
| def Run(self): |
| if self.is_bundle: |
| sys.stdout.write( |
| bundletool.RunBundleTool([ |
| 'dump', 'manifest', '--bundle', |
| self.bundle_generation_info.bundle_path |
| ])) |
| else: |
| apkanalyzer = os.path.join(_DIR_SOURCE_ROOT, 'third_party', 'android_sdk', |
| 'public', 'cmdline-tools', 'latest', 'bin', |
| 'apkanalyzer') |
| cmd = [apkanalyzer, 'manifest', 'print', self.apk_helper.path] |
| logging.info('Running: %s', shlex.join(cmd)) |
| subprocess.check_call(cmd) |
| |
| |
| class _StackCommand(_Command): |
| name = 'stack' |
| description = 'Decodes an Android stack.' |
| need_device_args = False |
| |
| def _RegisterExtraArgs(self, group): |
| group.add_argument( |
| 'file', |
| nargs='?', |
| help='File to decode. If not specified, stdin is processed.') |
| |
| def Run(self): |
| context = _StackScriptContext(self.args.output_directory, |
| self.args.apk_path, |
| self.bundle_generation_info) |
| try: |
| proc = context.Popen(input_file=self.args.file) |
| if proc.wait(): |
| raise Exception('stack script returned {}'.format(proc.returncode)) |
| finally: |
| context.Close() |
| |
| |
| # Shared commands for regular APKs and app bundles. |
| _COMMANDS = [ |
| _DevicesCommand, |
| _PackageInfoCommand, |
| _InstallCommand, |
| _UninstallCommand, |
| _SetWebViewProviderCommand, |
| _LaunchCommand, |
| _StopCommand, |
| _ClearDataCommand, |
| _ArgvCommand, |
| _GdbCommand, |
| _LldbCommand, |
| _LogcatCommand, |
| _PsCommand, |
| _DiskUsageCommand, |
| _MemUsageCommand, |
| _ShellCommand, |
| _CompileDexCommand, |
| _PrintCertsCommand, |
| _ProfileCommand, |
| _RunCommand, |
| _StackCommand, |
| _ManifestCommand, |
| ] |
| |
| # Commands specific to app bundles. |
| _BUNDLE_COMMANDS = [ |
| _BuildBundleApks, |
| ] |
| |
| |
| def _ParseArgs(parser, from_wrapper_script, is_bundle, is_test_apk): |
| subparsers = parser.add_subparsers() |
| command_list = _COMMANDS + (_BUNDLE_COMMANDS if is_bundle else []) |
| commands = [ |
| clazz(from_wrapper_script, is_bundle, is_test_apk) |
| for clazz in command_list |
| ] |
| |
| for command in commands: |
| if from_wrapper_script or not command.needs_output_directory: |
| command.RegisterArgs(subparsers) |
| |
| # Show extended help when no command is passed. |
| argv = sys.argv[1:] |
| if not argv: |
| argv = ['--help'] |
| |
| return parser.parse_args(argv) |
| |
| |
| def _RunInternal(parser, |
| output_directory=None, |
| additional_apk_paths=None, |
| bundle_generation_info=None, |
| is_test_apk=False): |
| colorama.init() |
| parser.set_defaults( |
| additional_apk_paths=additional_apk_paths, |
| output_directory=output_directory) |
| from_wrapper_script = bool(output_directory) |
| args = _ParseArgs(parser, |
| from_wrapper_script, |
| is_bundle=bool(bundle_generation_info), |
| is_test_apk=is_test_apk) |
| run_tests_helper.SetLogLevel(args.verbose_count) |
| if bundle_generation_info: |
| args.command.RegisterBundleGenerationInfo(bundle_generation_info) |
| if args.additional_apk_paths: |
| for path in additional_apk_paths: |
| if not path or not os.path.exists(path): |
| raise Exception('Invalid additional APK path "{}"'.format(path)) |
| args.command.ProcessArgs(args) |
| args.command.Run() |
| # Incremental install depends on the cache being cleared when uninstalling. |
| if args.command.name != 'uninstall': |
| _SaveDeviceCaches(args.command.devices, output_directory) |
| |
| |
| def Run(output_directory, apk_path, additional_apk_paths, incremental_json, |
| command_line_flags_file, target_cpu, proguard_mapping_path): |
| """Entry point for generated wrapper scripts.""" |
| constants.SetOutputDirectory(output_directory) |
| devil_chromium.Initialize(output_directory=output_directory) |
| parser = argparse.ArgumentParser() |
| exists_or_none = lambda p: p if p and os.path.exists(p) else None |
| |
| parser.set_defaults( |
| command_line_flags_file=command_line_flags_file, |
| target_cpu=target_cpu, |
| apk_path=exists_or_none(apk_path), |
| incremental_json=exists_or_none(incremental_json), |
| proguard_mapping_path=proguard_mapping_path) |
| _RunInternal( |
| parser, |
| output_directory=output_directory, |
| additional_apk_paths=additional_apk_paths) |
| |
| |
| def RunForBundle(output_directory, bundle_path, bundle_apks_path, |
| additional_apk_paths, aapt2_path, keystore_path, |
| keystore_password, keystore_alias, package_name, |
| command_line_flags_file, proguard_mapping_path, target_cpu, |
| system_image_locales, default_modules): |
| """Entry point for generated app bundle wrapper scripts. |
| |
| Args: |
| output_dir: Chromium output directory path. |
| bundle_path: Input bundle path. |
| bundle_apks_path: Output bundle .apks archive path. |
| additional_apk_paths: Additional APKs to install prior to bundle install. |
| aapt2_path: Aapt2 tool path. |
| keystore_path: Keystore file path. |
| keystore_password: Keystore password. |
| keystore_alias: Signing key name alias in keystore file. |
| package_name: Application's package name. |
| command_line_flags_file: Optional. Name of an on-device file that will be |
| used to store command-line flags for this bundle. |
| proguard_mapping_path: Input path to the Proguard mapping file, used to |
| deobfuscate Java stack traces. |
| target_cpu: Chromium target CPU name, used by the 'gdb' command. |
| system_image_locales: List of Chromium locales that should be included in |
| system image APKs. |
| default_modules: List of modules that are installed in addition to those |
| given by the '-m' switch. |
| """ |
| constants.SetOutputDirectory(output_directory) |
| devil_chromium.Initialize(output_directory=output_directory) |
| bundle_generation_info = BundleGenerationInfo( |
| bundle_path=bundle_path, |
| bundle_apks_path=bundle_apks_path, |
| aapt2_path=aapt2_path, |
| keystore_path=keystore_path, |
| keystore_password=keystore_password, |
| keystore_alias=keystore_alias, |
| system_image_locales=system_image_locales) |
| _InstallCommand.default_modules = default_modules |
| |
| parser = argparse.ArgumentParser() |
| parser.set_defaults( |
| package_name=package_name, |
| command_line_flags_file=command_line_flags_file, |
| proguard_mapping_path=proguard_mapping_path, |
| target_cpu=target_cpu) |
| _RunInternal( |
| parser, |
| output_directory=output_directory, |
| additional_apk_paths=additional_apk_paths, |
| bundle_generation_info=bundle_generation_info) |
| |
| |
| def RunForTestApk(*, output_directory, package_name, test_apk_path, |
| test_apk_json, proguard_mapping_path, additional_apk_paths): |
| """Entry point for generated test apk wrapper scripts. |
| |
| This is intended to make commands like logcat (with proguard deobfuscation) |
| available. The run_* scripts should be used to actually run tests. |
| |
| Args: |
| output_dir: Chromium output directory path. |
| package_name: The package name for the test apk. |
| test_apk_path: The test apk to install. |
| test_apk_json: The incremental json dict for the test apk. |
| proguard_mapping_path: Input path to the Proguard mapping file, used to |
| deobfuscate Java stack traces. |
| additional_apk_paths: Additional APKs to install. |
| """ |
| constants.SetOutputDirectory(output_directory) |
| devil_chromium.Initialize(output_directory=output_directory) |
| |
| parser = argparse.ArgumentParser() |
| exists_or_none = lambda p: p if p and os.path.exists(p) else None |
| |
| parser.set_defaults(apk_path=exists_or_none(test_apk_path), |
| incremental_json=exists_or_none(test_apk_json), |
| package_name=package_name, |
| proguard_mapping_path=proguard_mapping_path) |
| |
| _RunInternal(parser, |
| output_directory=output_directory, |
| additional_apk_paths=additional_apk_paths, |
| is_test_apk=True) |
| |
| |
| def main(): |
| devil_chromium.Initialize() |
| _RunInternal(argparse.ArgumentParser()) |
| |
| |
| if __name__ == '__main__': |
| main() |