| #!/usr/bin/env python |
| # Copyright 2017 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import json |
| import logging |
| import os |
| import pipes |
| import posixpath |
| import random |
| import re |
| import shlex |
| import sys |
| |
| 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 intent |
| from devil.android.sdk import version_codes |
| from devil.utils import run_tests_helper |
| |
| with devil_env.SysPath(os.path.join(os.path.dirname(__file__), '..', '..', |
| 'third_party', 'colorama', 'src')): |
| import colorama |
| |
| from incremental_install import installer |
| from pylib import constants |
| from pylib.symbols import deobfuscator |
| |
| |
| def _Colorize(color, text): |
| # |color| as a string to avoid pylint's no-member warning :(. |
| # pylint: disable=no-member |
| return getattr(colorama.Fore, color) + text + colorama.Fore.RESET |
| |
| |
| def _InstallApk(devices, apk, install_dict): |
| def install(device): |
| if install_dict: |
| installer.Install(device, install_dict, apk=apk) |
| else: |
| device.Install(apk) |
| |
| logging.info('Installing %sincremental apk.', '' if install_dict else 'non-') |
| 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 _LaunchUrl(devices, input_args, device_args_file, url, apk): |
| if input_args and device_args_file is None: |
| raise Exception('This apk does not support any flags.') |
| if url: |
| view_activity = apk.GetViewActivityName() |
| if not view_activity: |
| raise Exception('APK does not support launching with URLs.') |
| |
| def launch(device): |
| # The flags are first updated with input args. |
| changer = flag_changer.FlagChanger(device, device_args_file) |
| flags = [] |
| if input_args: |
| flags = shlex.split(input_args) |
| changer.ReplaceFlags(flags) |
| # Then launch the apk. |
| if url is None: |
| # Simulate app icon click if no url is present. |
| cmd = ['monkey', '-p', apk.GetPackageName(), '-c', |
| 'android.intent.category.LAUNCHER', '1'] |
| device.RunShellCommand(cmd, check_return=True) |
| else: |
| launch_intent = intent.Intent(action='android.intent.action.VIEW', |
| activity=view_activity, data=url, |
| package=apk.GetPackageName()) |
| device.StartActivity(launch_intent) |
| device_utils.DeviceUtils.parallel(devices).pMap(launch) |
| |
| |
| def _ChangeFlags(devices, input_args, device_args_file): |
| if input_args is None: |
| _DisplayArgs(devices, device_args_file) |
| else: |
| flags = shlex.split(input_args) |
| def update(device): |
| flag_changer.FlagChanger(device, device_args_file).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, output_directory, target_cpu, extra_args, |
| verbose): |
| 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, |
| # Use one lib dir per device so that changing between devices does require |
| # refetching the device libs. |
| '--pull-libs-dir=/tmp/adb-gdb-libs-%s' % device.serial, |
| ] |
| # 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)) |
| cmd.extend(extra_args) |
| logging.warning('Running: %s', ' '.join(pipes.quote(x) for x in cmd)) |
| print _Colorize('YELLOW', 'All subsequent output is from adb_gdb script.') |
| os.execv(gdb_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('YELLOW', '%s (%s):' % (d, d.build_description))) |
| sys.stdout.write(' ' if single_line else '\n') |
| yield result |
| |
| |
| def _RunMemUsage(devices, package_name): |
| def mem_usage_helper(d): |
| ret = [] |
| proc_map = d.GetPids(package_name) |
| for name, pids in proc_map.iteritems(): |
| for pid in pids: |
| ret.append((name, pid, d.GetMemoryUsageForPid(pid))) |
| 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, pid, usage in sorted(result): |
| print '%s(%s):' % (name, pid) |
| for k, v in sorted(usage.iteritems()): |
| print ' %s=%d' % (k, v) |
| print |
| |
| |
| 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 kb 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. |
| cmd_str = 'du -s -k ' + path_spec |
| 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('Failed to parse du output:\n%s', output) |
| |
| |
| def _RunDiskUsage(devices, package_name, verbose): |
| # 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)) |
| # Prints a message but does not return error when apk is not installed. |
| if 'Unable to find package:' in package_output: |
| return None |
| # Ignore system apks. |
| idx = package_output.find('Hidden system packages:') |
| if idx != -1: |
| package_output = package_output[:idx] |
| |
| 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: |
| raise Exception('Error parsing dumpsys output: ' + package_output) |
| 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))) |
| 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(pipes.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: %dkb' % (desc, sum(sizes.itervalues())) |
| if verbose: |
| for path, size in sorted(sizes.iteritems()): |
| print ' %s: %skb' % (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.itervalues()) 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: %skb (%.1fmb)' % (total, total / 1024.0) |
| |
| |
| def _RunLogcat(device, package_name, verbose, mapping_path): |
| if mapping_path: |
| try: |
| deobfuscate = deobfuscator.Deobfuscator(mapping_path) |
| except OSError: |
| sys.stderr.write('Error executing "bin/java_deobfuscate". ' |
| 'Did you forget to build it?\n') |
| sys.exit(1) |
| |
| def get_my_pids(): |
| my_pids = [] |
| for pids in device.GetPids(package_name).values(): |
| my_pids.extend(pids) |
| return [int(pid) for pid in my_pids] |
| |
| def process_line(line, fast=False): |
| if verbose: |
| if fast: |
| return |
| else: |
| if not line or line.startswith('------'): |
| return |
| tokens = line.split(None, 4) |
| pid = int(tokens[2]) |
| priority = tokens[4] |
| if pid in my_pids or (not fast and priority == 'F'): |
| pass # write |
| elif pid in not_my_pids: |
| return |
| elif fast: |
| # Skip checking whether our package spawned new processes. |
| not_my_pids.add(pid) |
| return |
| else: |
| # Check and add the pid if it is a new one from our package. |
| my_pids.update(get_my_pids()) |
| if pid not in my_pids: |
| not_my_pids.add(pid) |
| return |
| if mapping_path: |
| line = '\n'.join(deobfuscate.TransformLines([line.rstrip()])) + '\n' |
| sys.stdout.write(line) |
| |
| try: |
| my_pids = set(get_my_pids()) |
| not_my_pids = set() |
| |
| nonce = 'apk_wrappers.py nonce={}'.format(random.random()) |
| device.RunShellCommand(['log', nonce]) |
| fast = True |
| for line in device.adb.Logcat(logcat_format='threadtime'): |
| try: |
| process_line(line, fast) |
| except: |
| sys.stderr.write('Failed to process line: ' + line) |
| raise |
| if fast and nonce in line: |
| fast = False |
| except KeyboardInterrupt: |
| pass # Don't show stack trace upon Ctrl-C |
| finally: |
| if mapping_path: |
| deobfuscate.Close() |
| |
| |
| def _RunPs(devices, package_name): |
| parallel_devices = device_utils.DeviceUtils.parallel(devices) |
| all_pids = parallel_devices.GetPids(package_name).pGet(None) |
| for proc_map in _PrintPerDeviceOutput(devices, all_pids): |
| if not proc_map: |
| print 'No processes found.' |
| else: |
| 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).pGet(None) |
| for output in _PrintPerDeviceOutput(devices, outputs): |
| for line in output: |
| print line |
| |
| |
| 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, device_args_file): |
| def flags_helper(d): |
| changer = flag_changer.FlagChanger(d, device_args_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/%s):' % device_args_file |
| for flags in _PrintPerDeviceOutput(devices, outputs, single_line=True): |
| quoted_flags = ' '.join(pipes.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(object): |
| name = None |
| description = None |
| needs_package_name = False |
| needs_output_directory = False |
| needs_apk_path = False |
| supports_incremental = False |
| accepts_command_line_flags = False |
| accepts_args = False |
| accepts_url = False |
| all_devices_by_default = False |
| calls_exec = False |
| |
| def __init__(self, from_wrapper_script): |
| self._parser = None |
| self._from_wrapper_script = from_wrapper_script |
| self.args = None |
| self.apk_helper = None |
| self.install_dict = None |
| self.devices = None |
| # Do not support incremental install outside the context of wrapper scripts. |
| if not from_wrapper_script: |
| self.supports_incremental = False |
| |
| def _RegisterExtraArgs(self, subp): |
| pass |
| |
| def RegisterArgs(self, parser): |
| subp = parser.add_parser(self.name, help=self.description) |
| self._parser = subp |
| subp.set_defaults(command=self) |
| 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: |
| # Always gleaned from apk when using wrapper scripts. |
| group.add_argument('--package-name', |
| help=argparse.SUPPRESS if self._from_wrapper_script else ( |
| "App's package name.")) |
| |
| if self.needs_apk_path 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: |
| group.add_argument('--apk-path', |
| required=self.needs_apk_path, |
| 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.') |
| |
| if self.accepts_args: |
| group.add_argument('--args', help='Extra arguments.') |
| |
| if self.accepts_url: |
| group.add_argument('url', nargs='?', help='A URL to launch with.') |
| |
| if not self._from_wrapper_script and self.accepts_command_line_flags: |
| # Provided by wrapper scripts. |
| group.add_argument( |
| '--command-line-flags-file-name', |
| help='Name of the command-line flags file') |
| |
| self._RegisterExtraArgs(group) |
| |
| def ProcessArgs(self, args): |
| devices = device_utils.DeviceUtils.HealthyDevices( |
| device_arg=args.devices, |
| enable_device_files_cache=bool(args.output_directory), |
| default_retries=0) |
| self.args = args |
| self.devices = devices |
| # TODO(agrieve): Device cache should not depend on output directory. |
| # Maybe put int /tmp? |
| _LoadDeviceCaches(devices, args.output_directory) |
| # 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) |
| |
| try: |
| if len(devices) > 1: |
| if self.calls_exec: |
| self._parser.error(device_errors.MultipleDevicesError(devices)) |
| if not args.all and not args.devices: |
| self._parser.error(_GenerateMissingAllFlagMessage(devices)) |
| |
| 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.') |
| args.incremental_json = None |
| elif args.incremental: |
| if not args.incremental_json: |
| self._parser.error('Incremental apk has not been built.') |
| args.apk_path = None |
| |
| if args.apk_path and args.incremental_json: |
| self._parser.error('Both incremental and non-incremental apks exist. ' |
| 'Select using --incremental or --non-incremental') |
| |
| if self.needs_apk_path or args.apk_path or args.incremental_json: |
| if args.incremental_json: |
| with open(args.incremental_json) as f: |
| install_dict = json.load(f) |
| apk_path = os.path.join(args.output_directory, |
| install_dict['apk_path']) |
| if os.path.exists(apk_path): |
| self.install_dict = install_dict |
| self.apk_helper = apk_helper.ToHelper( |
| os.path.join(args.output_directory, |
| self.install_dict['apk_path'])) |
| if not self.apk_helper and args.apk_path: |
| self.apk_helper = apk_helper.ToHelper(args.apk_path) |
| if not self.apk_helper: |
| self._parser.error( |
| 'Neither incremental nor non-incremental apk is built.') |
| |
| if self.needs_package_name and not args.package_name: |
| if self.apk_helper: |
| args.package_name = self.apk_helper.GetPackageName() |
| elif self._from_wrapper_script: |
| self._parser.error( |
| 'Neither incremental nor non-incremental apk is built.') |
| else: |
| self._parser.error('One of --package-name or --apk-path is required.') |
| |
| # Save cache now if command will not get a chance to afterwards. |
| if self.calls_exec: |
| _SaveDeviceCaches(devices, args.output_directory) |
| except: |
| _SaveDeviceCaches(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 _InstallCommand(_Command): |
| name = 'install' |
| description = 'Installs the APK to one or more devices.' |
| needs_apk_path = True |
| supports_incremental = True |
| |
| def Run(self): |
| _InstallApk(self.devices, self.apk_helper, self.install_dict) |
| |
| |
| class _UninstallCommand(_Command): |
| name = 'uninstall' |
| description = 'Removes the APK to one or more devices.' |
| needs_package_name = True |
| |
| def Run(self): |
| _UninstallApk(self.devices, self.install_dict, self.args.package_name) |
| |
| |
| class _LaunchCommand(_Command): |
| name = 'launch' |
| description = ('Sends a launch intent for the APK after first writing the ' |
| 'command-line flags file.') |
| # TODO(agrieve): Launch could be changed to require only package name by |
| # parsing "dumpsys package" for launch & view activities. |
| needs_apk_path = True |
| accepts_command_line_flags = True |
| accepts_url = True |
| all_devices_by_default = True |
| |
| def Run(self): |
| _LaunchUrl(self.devices, self.args.args, self.args.command_line_flags_file, |
| self.args.url, self.apk_helper) |
| |
| |
| class _RunCommand(_Command): |
| name = 'run' |
| description = 'Install and then launch.' |
| needs_apk_path = True |
| supports_incremental = True |
| needs_package_name = True |
| accepts_command_line_flags = True |
| accepts_url = True |
| |
| def Run(self): |
| logging.warning('Installing...') |
| _InstallApk(self.devices, self.apk_helper, self.install_dict) |
| logging.warning('Sending launch intent...') |
| _LaunchUrl(self.devices, self.args.args, self.args.command_line_flags_file, |
| self.args.url, self.apk_helper) |
| |
| |
| 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.' |
| needs_package_name = True |
| needs_output_directory = True |
| accepts_args = True |
| calls_exec = True |
| |
| def Run(self): |
| extra_args = shlex.split(self.args.args or '') |
| _RunGdb(self.devices[0], self.args.package_name, self.args.output_directory, |
| self.args.target_cpu, extra_args, bool(self.args.verbose_count)) |
| |
| |
| class _LogcatCommand(_Command): |
| name = 'logcat' |
| description = 'Runs "adb logcat" filtering to just the current APK processes' |
| needs_package_name = True |
| calls_exec = True |
| |
| def Run(self): |
| mapping = self.args.proguard_mapping_path |
| if self.args.no_deobfuscate: |
| mapping = None |
| _RunLogcat(self.devices[0], self.args.package_name, |
| bool(self.args.verbose_count), mapping) |
| |
| 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)') |
| |
| |
| 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, |
| bool(self.args.verbose_count)) |
| |
| |
| 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 Run(self): |
| _RunMemUsage(self.devices, self.args.package_name) |
| |
| |
| 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 |
| |
| 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) |
| |
| |
| _COMMANDS = [ |
| _DevicesCommand, |
| _InstallCommand, |
| _UninstallCommand, |
| _LaunchCommand, |
| _RunCommand, |
| _StopCommand, |
| _ClearDataCommand, |
| _ArgvCommand, |
| _GdbCommand, |
| _LogcatCommand, |
| _PsCommand, |
| _DiskUsageCommand, |
| _MemUsageCommand, |
| _ShellCommand, |
| _CompileDexCommand, |
| ] |
| |
| |
| def _ParseArgs(parser, from_wrapper_script): |
| subparsers = parser.add_subparsers() |
| commands = [clazz(from_wrapper_script) for clazz in _COMMANDS] |
| 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): |
| colorama.init() |
| parser.set_defaults(output_directory=output_directory) |
| from_wrapper_script = bool(output_directory) |
| args = _ParseArgs(parser, from_wrapper_script) |
| run_tests_helper.SetLogLevel(args.verbose_count) |
| 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) |
| |
| |
| # TODO(agrieve): Remove =None from target_cpu on or after October 2017. |
| # It exists only so that stale wrapper scripts continue to work. |
| def Run(output_directory, apk_path, 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) |
| |
| |
| def main(): |
| devil_chromium.Initialize() |
| _RunInternal(argparse.ArgumentParser(), output_directory=None) |
| |
| |
| if __name__ == '__main__': |
| main() |