|  | # 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 |