| # Copyright 2016 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 collections |
| import contextlib |
| import logging |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import tempfile |
| import time |
| |
| _SRC_DIR = os.path.abspath(os.path.join( |
| os.path.dirname(__file__), '..', '..', '..')) |
| |
| _CATAPULT_DIR = os.path.join(_SRC_DIR, 'third_party', 'catapult') |
| sys.path.append(os.path.join(_CATAPULT_DIR, 'devil')) |
| from devil.android import device_utils |
| from devil.android import flag_changer |
| from devil.android import forwarder |
| from devil.android.sdk import intent |
| |
| sys.path.append(os.path.join(_SRC_DIR, 'build', 'android')) |
| from pylib import constants |
| from video_recorder import video_recorder |
| |
| sys.path.append(os.path.join(_SRC_DIR, 'tools', 'perf')) |
| from chrome_telemetry_build import chromium_config |
| |
| sys.path.append(chromium_config.GetTelemetryDir()) |
| from telemetry.internal.image_processing import video |
| from telemetry.internal.util import webpagereplay |
| |
| sys.path.append(os.path.join( |
| _CATAPULT_DIR, 'telemetry', 'third_party', 'webpagereplay')) |
| import adb_install_cert |
| import certutils |
| |
| import common_util |
| import devtools_monitor |
| import emulation |
| import options |
| |
| |
| OPTIONS = options.OPTIONS |
| |
| # The speed index's video recording's bit rate in Mb/s. |
| _SPEED_INDEX_VIDEO_BITRATE = 4 |
| |
| |
| class DeviceSetupException(Exception): |
| def __init__(self, msg): |
| super(DeviceSetupException, self).__init__(msg) |
| logging.error(msg) |
| |
| |
| def GetFirstDevice(): |
| """Returns the first connected device. |
| |
| Raises: |
| DeviceSetupException if there is no such device. |
| """ |
| devices = device_utils.DeviceUtils.HealthyDevices() |
| if not devices: |
| raise DeviceSetupException('No devices found') |
| return devices[0] |
| |
| |
| def GetDeviceFromSerial(android_device_serial): |
| """Returns the DeviceUtils instance.""" |
| devices = device_utils.DeviceUtils.HealthyDevices() |
| for device in devices: |
| if device.adb._device_serial == android_device_serial: |
| return device |
| raise DeviceSetupException( |
| 'Device {} not found'.format(android_device_serial)) |
| |
| |
| def Reboot(device): |
| """Reboot the device. |
| |
| Args: |
| device: Device to reboot, from DeviceUtils. |
| """ |
| # Kills the device -> host forwarder running on the device so that |
| # forwarder.Forwarder have correct state tracking after having rebooted. |
| forwarder.Forwarder.UnmapAllDevicePorts(device) |
| # Reboot the device. |
| device.Reboot() |
| # Pass through the lock screen. |
| time.sleep(3) |
| device.RunShellCommand(['input', 'keyevent', '82']) |
| |
| |
| def DeviceSubmitShellCommandQueue(device, command_queue): |
| """Executes on the device a command queue. |
| |
| Args: |
| device: The device to execute the shell commands to. |
| command_queue: a list of commands to be executed in that order. |
| """ |
| REMOTE_COMMAND_FILE_PATH = '/data/local/tmp/adb_command_file.sh' |
| if not command_queue: |
| return |
| with tempfile.NamedTemporaryFile(prefix='adb_command_file_', |
| suffix='.sh') as command_file: |
| command_file.write('#!/bin/sh\n') |
| command_file.write('# Shell file generated by {}\'s {}\n'.format( |
| __file__, DeviceSubmitShellCommandQueue.__name__)) |
| command_file.write('set -e\n') |
| for command in command_queue: |
| command_file.write(subprocess.list2cmdline(command) + ' ;\n') |
| command_file.write('exit 0;\n'.format( |
| REMOTE_COMMAND_FILE_PATH)) |
| command_file.flush() |
| device.adb.Push(command_file.name, REMOTE_COMMAND_FILE_PATH) |
| device.adb.Shell('sh {p} && rm {p}'.format(p=REMOTE_COMMAND_FILE_PATH)) |
| |
| |
| @contextlib.contextmanager |
| def FlagReplacer(device, command_line_path, new_flags): |
| """Replaces chrome flags in a context, restores them afterwards. |
| |
| Args: |
| device: Device to target, from DeviceUtils. Can be None, in which case this |
| context manager is a no-op. |
| command_line_path: Full path to the command-line file. |
| new_flags: Flags to replace. |
| """ |
| # If we're logging requests from a local desktop chrome instance there is no |
| # device. |
| if not device: |
| yield |
| return |
| changer = flag_changer.FlagChanger(device, command_line_path) |
| changer.ReplaceFlags(new_flags) |
| try: |
| yield |
| finally: |
| changer.Restore() |
| |
| |
| @contextlib.contextmanager |
| def ForwardPort(device, local, remote): |
| """Forwards a local port to a remote one on a device in a context.""" |
| # If we're logging requests from a local desktop chrome instance there is no |
| # device. |
| if not device: |
| yield |
| return |
| device.adb.Forward(local, remote) |
| try: |
| yield |
| finally: |
| device.adb.ForwardRemove(local) |
| |
| |
| # WPR specific attributes to set up chrome. |
| # |
| # Members: |
| # chrome_args: Additional flags list that may be used for chromium to load web |
| # page through the running web page replay host. |
| # chrome_env_override: Dictionary of environment variables to override at |
| # Chrome's launch time. |
| WprAttribute = collections.namedtuple('WprAttribute', |
| ['chrome_args', 'chrome_env_override']) |
| |
| |
| @contextlib.contextmanager |
| def _WprHost(wpr_archive_path, record=False, |
| network_condition_name=None, |
| disable_script_injection=False, |
| wpr_ca_cert_path=None, |
| out_log_path=None): |
| assert wpr_archive_path |
| |
| def PathWorkaround(path): |
| # webpagereplay.ReplayServer is doing a os.path.exist(os.path.dirname(p)) |
| # that fails if p = 'my_file.txt' because os.path.dirname(p) = '' != '.'. |
| # This workaround just sends absolute path to work around this bug. |
| return os.path.abspath(path) |
| |
| wpr_server_args = ['--use_closest_match'] |
| if record: |
| wpr_server_args.append('--record') |
| if os.path.exists(wpr_archive_path): |
| os.remove(wpr_archive_path) |
| else: |
| assert os.path.exists(wpr_archive_path) |
| if network_condition_name: |
| condition = emulation.NETWORK_CONDITIONS[network_condition_name] |
| if record: |
| logging.warning('WPR network condition is ignored when recording.') |
| else: |
| wpr_server_args.extend([ |
| '--down', emulation.BandwidthToString(condition['download']), |
| '--up', emulation.BandwidthToString(condition['upload']), |
| '--delay_ms', str(condition['latency']), |
| '--shaping_type', 'proxy']) |
| |
| if disable_script_injection: |
| # Remove default WPR injected scripts like deterministic.js which |
| # overrides Math.random. |
| wpr_server_args.extend(['--inject_scripts', '']) |
| if wpr_ca_cert_path: |
| wpr_server_args.extend(['--should_generate_certs', |
| '--https_root_ca_cert_path=' + PathWorkaround(wpr_ca_cert_path)]) |
| if out_log_path: |
| # --log_level debug to extract the served URLs requests from the log. |
| wpr_server_args.extend(['--log_level', 'debug', |
| '--log_file', PathWorkaround(out_log_path)]) |
| # Don't append to previously existing log. |
| if os.path.exists(out_log_path): |
| os.remove(out_log_path) |
| |
| # Set up WPR server and device forwarder. |
| wpr_server = webpagereplay.ReplayServer(PathWorkaround(wpr_archive_path), |
| '127.0.0.1', 0, 0, None, wpr_server_args) |
| http_port, https_port = wpr_server.StartServer()[:-1] |
| |
| logging.info('WPR server listening on HTTP=%s, HTTPS=%s (options=%s)' % ( |
| http_port, https_port, wpr_server_args)) |
| try: |
| yield http_port, https_port |
| finally: |
| wpr_server.StopServer() |
| |
| |
| def _VerifySilentWprHost(record, network_condition_name): |
| assert not record, 'WPR cannot record without a specified archive.' |
| assert not network_condition_name, ('WPR cannot emulate network condition' + |
| ' without a specified archive.') |
| |
| |
| def _FormatWPRRelatedChromeArgumentFor(http_port, https_port, escape): |
| HOST_RULES='MAP * 127.0.0.1,EXCLUDE localhost' |
| chrome_args = [ |
| '--testing-fixed-http-port={}'.format(http_port), |
| '--testing-fixed-https-port={}'.format(https_port)] |
| if escape: |
| chrome_args.append('--host-resolver-rules="{}"'.format(HOST_RULES)) |
| else: |
| chrome_args.append('--host-resolver-rules={}'.format(HOST_RULES)) |
| return chrome_args |
| |
| |
| @contextlib.contextmanager |
| def LocalWprHost(wpr_archive_path, record=False, |
| network_condition_name=None, |
| disable_script_injection=False, |
| out_log_path=None): |
| """Launches web page replay host. |
| |
| Args: |
| wpr_archive_path: host sided WPR archive's path. |
| record: Enables or disables WPR archive recording. |
| network_condition_name: Network condition name available in |
| emulation.NETWORK_CONDITIONS. |
| disable_script_injection: Disable JavaScript file injections that is |
| fighting against resources name entropy. |
| out_log_path: Path of the WPR host's log. |
| |
| Returns: |
| WprAttribute |
| """ |
| if wpr_archive_path == None: |
| _VerifySilentWprHost(record, network_condition_name) |
| yield [] |
| return |
| |
| with common_util.TemporaryDirectory() as temp_home_dir: |
| # Generate a root certification authority certificate for WPR. |
| private_ca_cert_path = os.path.join(temp_home_dir, 'wpr.pem') |
| ca_cert_path = os.path.join(temp_home_dir, 'wpr-cert.pem') |
| certutils.write_dummy_ca_cert(*certutils.generate_dummy_ca_cert(), |
| cert_path=private_ca_cert_path) |
| assert os.path.isfile(ca_cert_path) |
| certutils.install_cert_in_nssdb(temp_home_dir, ca_cert_path) |
| |
| with _WprHost( |
| wpr_archive_path, |
| record=record, |
| network_condition_name=network_condition_name, |
| disable_script_injection=disable_script_injection, |
| wpr_ca_cert_path=private_ca_cert_path, |
| out_log_path=out_log_path) as (http_port, https_port): |
| chrome_args = _FormatWPRRelatedChromeArgumentFor(http_port, https_port, |
| escape=False) |
| yield WprAttribute(chrome_args=chrome_args, |
| chrome_env_override={'HOME': temp_home_dir}) |
| |
| |
| @contextlib.contextmanager |
| def RemoteWprHost(device, wpr_archive_path, record=False, |
| network_condition_name=None, |
| disable_script_injection=False, |
| out_log_path=None): |
| """Launches web page replay host. |
| |
| Args: |
| device: Android device. |
| wpr_archive_path: host sided WPR archive's path. |
| record: Enables or disables WPR archive recording. |
| network_condition_name: Network condition name available in |
| emulation.NETWORK_CONDITIONS. |
| disable_script_injection: Disable JavaScript file injections that is |
| fighting against resources name entropy. |
| out_log_path: Path of the WPR host's log. |
| |
| Returns: |
| WprAttribute |
| """ |
| assert device |
| if wpr_archive_path == None: |
| _VerifySilentWprHost(record, network_condition_name) |
| yield [] |
| return |
| # Deploy certification authority to the device. |
| temp_certificate_dir = tempfile.mkdtemp() |
| wpr_ca_cert_path = os.path.join(temp_certificate_dir, 'testca.pem') |
| certutils.write_dummy_ca_cert(*certutils.generate_dummy_ca_cert(), |
| cert_path=wpr_ca_cert_path) |
| device_cert_util = adb_install_cert.AndroidCertInstaller( |
| device.adb.GetDeviceSerial(), None, wpr_ca_cert_path) |
| device_cert_util.install_cert(overwrite_cert=True) |
| try: |
| # Set up WPR server |
| with _WprHost( |
| wpr_archive_path, |
| record=record, |
| network_condition_name=network_condition_name, |
| disable_script_injection=disable_script_injection, |
| wpr_ca_cert_path=wpr_ca_cert_path, |
| out_log_path=out_log_path) as (http_port, https_port): |
| # Set up the forwarder. |
| forwarder.Forwarder.Map([(0, http_port), (0, https_port)], device) |
| device_http_port = forwarder.Forwarder.DevicePortForHostPort(http_port) |
| device_https_port = forwarder.Forwarder.DevicePortForHostPort(https_port) |
| try: |
| chrome_args = _FormatWPRRelatedChromeArgumentFor(device_http_port, |
| device_https_port, |
| escape=True) |
| yield WprAttribute(chrome_args=chrome_args, chrome_env_override={}) |
| finally: |
| # Tear down the forwarder. |
| forwarder.Forwarder.UnmapDevicePort(device_http_port, device) |
| forwarder.Forwarder.UnmapDevicePort(device_https_port, device) |
| finally: |
| # Remove certification authority from the device. |
| device_cert_util.remove_cert() |
| shutil.rmtree(temp_certificate_dir) |
| |
| |
| # Deprecated |
| @contextlib.contextmanager |
| def _RemoteVideoRecorder(device, local_output_path, megabits_per_second): |
| """Record a video on Device. |
| |
| Args: |
| device: (device_utils.DeviceUtils) Android device to connect to. |
| local_output_path: Output path were to save the video locally. |
| megabits_per_second: Video recorder Mb/s. |
| |
| Yields: |
| None |
| """ |
| assert device |
| if megabits_per_second > 100: |
| raise ValueError('Android video capture cannot capture at %dmbps. ' |
| 'Max capture rate is 100mbps.' % megabits_per_second) |
| assert local_output_path.endswith('.mp4') |
| recorder = video_recorder.VideoRecorder(device, megabits_per_second) |
| recorder.Start() |
| try: |
| yield |
| recorder.Stop() |
| recorder.Pull(host_file=local_output_path) |
| recorder = None |
| finally: |
| if recorder: |
| recorder.Stop() |
| |
| |
| @contextlib.contextmanager |
| def RemoteSpeedIndexRecorder(device, connection, local_output_path): |
| """Records on a device a video compatible for speed-index computation. |
| |
| Note: |
| Chrome should be opened with the --disable-infobars command line argument to |
| avoid web page viewport size to be changed, that can change speed-index |
| value. |
| |
| Args: |
| device: (device_utils.DeviceUtils) Android device to connect to. |
| connection: devtools connection. |
| local_output_path: Output path were to save the video locally. |
| |
| Yields: |
| None |
| """ |
| # Paint the current HTML document with the ORANGE that video is detecting with |
| # the view-port position and size. |
| color = video.HIGHLIGHT_ORANGE_FRAME |
| connection.ExecuteJavaScript(""" |
| (function() { |
| var screen = document.createElement('div'); |
| screen.style.background = 'rgb(%d, %d, %d)'; |
| screen.style.position = 'fixed'; |
| screen.style.top = '0'; |
| screen.style.left = '0'; |
| screen.style.width = '100%%'; |
| screen.style.height = '100%%'; |
| screen.style.zIndex = '2147483638'; |
| document.body.appendChild(screen); |
| requestAnimationFrame(function() { |
| requestAnimationFrame(function() { |
| window.__speedindex_screen = screen; |
| }); |
| }); |
| })(); |
| """ % (color.r, color.g, color.b)) |
| connection.PollForJavaScriptExpression('!!window.__speedindex_screen', 1) |
| |
| with _RemoteVideoRecorder(device, local_output_path, |
| megabits_per_second=_SPEED_INDEX_VIDEO_BITRATE): |
| # Paint the current HTML document with white so that it is not troubling the |
| # speed index measurement. |
| connection.ExecuteJavaScript(""" |
| (function() { |
| requestAnimationFrame(function() { |
| var screen = window.__speedindex_screen; |
| screen.style.background = 'rgb(255, 255, 255)'; |
| }); |
| })(); |
| """) |
| yield |