blob: 09c794b517d06e1289d7fa7c90ee54a353d5b25e [file] [log] [blame]
# 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.
"""Controller objects that control the context in which chrome runs.
This is responsible for the setup necessary for launching chrome, and for
creating a DevToolsConnection. There are remote device and local
desktop-specific versions.
"""
import contextlib
import datetime
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__), '..', '..', '..'))
import chrome_cache
import device_setup
import devtools_monitor
import emulation
import options
sys.path.append(os.path.join(_SRC_DIR, 'third_party', 'catapult', 'devil'))
from devil.android.sdk import intent
OPTIONS = options.OPTIONS
# An estimate of time to wait for the device to become idle after expensive
# operations, such as opening the launcher activity.
_TIME_TO_DEVICE_IDLE_SECONDS = 2
class ChromeControllerBase(object):
"""Base class for all controllers.
Defines common operations but should not be created directly.
"""
def __init__(self):
self._chrome_args = [
'--disable-fre',
'--enable-test-events',
'--remote-debugging-port=%d' % OPTIONS.devtools_port,
]
self._metadata = {}
self._emulated_device = None
self._emulated_network = None
self._clear_cache = False
def AddChromeArgument(self, arg):
"""Add command-line argument to the chrome execution."""
self._chrome_args.append(arg)
def SetClearCache(self, clear_cache=True):
self._clear_cache = clear_cache
@contextlib.contextmanager
def Open(self):
"""Context that returns a connection/chrome instance.
Returns:
DevToolsConnection instance for which monitoring has been set up but not
started.
"""
raise NotImplementedError
def ChromeMetadata(self):
"""Return metadata such as emulation information.
Returns:
Metadata as JSON dictionary.
"""
return self._metadata
def GetDevice(self):
"""Returns an android device, or None if chrome is local."""
return None
def SetDeviceEmulation(self, device_name):
"""Set device emulation.
Args:
device_name: (str) Key from --devices_file.
"""
devices = emulation.LoadEmulatedDevices(file(OPTIONS.devices_file))
self._emulated_device = devices[device_name]
def SetNetworkEmulation(self, network_name):
"""Set network emulation.
Args:
network_name: (str) Key from emulation.NETWORK_CONDITIONS.
"""
self._emulated_network = emulation.NETWORK_CONDITIONS[network_name]
def _StartConnection(self, connection):
"""This should be called after opening an appropriate connection."""
if self._emulated_device:
self._metadata.update(emulation.SetUpDeviceEmulationAndReturnMetadata(
connection, self._emulated_device))
if self._emulated_network:
emulation.SetUpNetworkEmulation(connection, **self._emulated_network)
self._metadata.update(self._emulated_network)
self._metadata.update(date=datetime.datetime.utcnow().isoformat(),
seconds_since_epoch=time.time())
if self._clear_cache:
connection.AddHook(connection.ClearCache)
class RemoteChromeController(ChromeControllerBase):
"""A controller for an android device, aka remote chrome instance."""
# Seconds to sleep after starting chrome activity.
POST_ACTIVITY_SLEEP_SECONDS = 2
def __init__(self, device):
"""Initialize the controller.
Args:
device: an andriod device.
"""
assert device is not None, 'Should you be using LocalController instead?'
self._device = device
super(RemoteChromeController, self).__init__()
self._slow_death = False
@contextlib.contextmanager
def Open(self):
"""Overridden connection creation."""
package_info = OPTIONS.ChromePackage()
command_line_path = '/data/local/chrome-command-line'
self._device.EnableRoot()
self._device.KillAll(package_info.package, quiet=True)
with device_setup.FlagReplacer(
self._device, command_line_path, self._chrome_args):
start_intent = intent.Intent(
package=package_info.package, activity=package_info.activity,
data='about:blank')
self._device.StartActivity(start_intent, blocking=True)
time.sleep(self.POST_ACTIVITY_SLEEP_SECONDS)
with device_setup.ForwardPort(
self._device, 'tcp:%d' % OPTIONS.devtools_port,
'localabstract:chrome_devtools_remote'):
connection = devtools_monitor.DevToolsConnection(
OPTIONS.devtools_hostname, OPTIONS.devtools_port)
self._StartConnection(connection)
yield connection
if self._slow_death:
self._device.adb.Shell('am start com.google.android.launcher')
time.sleep(_TIME_TO_DEVICE_IDLE_SECONDS)
self._device.KillAll(OPTIONS.chrome_package_name, quiet=True)
time.sleep(_TIME_TO_DEVICE_IDLE_SECONDS)
self._device.KillAll(package_info.package, quiet=True)
def PushBrowserCache(self, cache_path):
"""Push a chrome cache.
Args:
cache_path: The directory's path containing the cache locally.
"""
chrome_cache.PushBrowserCache(self._device, cache_path)
def PullBrowserCache(self):
"""Pull a chrome cache.
Returns:
Temporary directory containing all the browser cache. Caller will need to
remove this directory manually.
"""
return chrome_cache.PullBrowserCache(self._device)
def SetSlowDeath(self, slow_death=True):
"""Set to pause before final kill of chrome.
Gives time for caches to write.
Args:
slow_death: (bool) True if you want that which comes to all who live, to
be slow.
"""
self._slow_death = slow_death
class LocalChromeController(ChromeControllerBase):
"""Controller for a local (desktop) chrome instance.
TODO(gabadie): implement cache push/pull and declare up in base class.
"""
def __init__(self):
super(LocalChromeController, self).__init__()
if OPTIONS.no_sandbox:
self.AddChromeArgument('--no-sandbox')
@contextlib.contextmanager
def Open(self):
"""Override for connection context."""
binary_filename = OPTIONS.local_binary
profile_dir = OPTIONS.local_profile_dir
using_temp_profile_dir = profile_dir is None
flags = self._chrome_args
if using_temp_profile_dir:
profile_dir = tempfile.mkdtemp()
flags = ['--user-data-dir=%s' % profile_dir] + flags
chrome_out = None if OPTIONS.local_noisy else file('/dev/null', 'w')
process = subprocess.Popen(
[binary_filename] + flags, shell=False, stderr=chrome_out)
try:
time.sleep(10)
process_result = process.poll()
if process_result is not None:
logging.error('Unexpected process exit: %s', process_result)
else:
connection = devtools_monitor.DevToolsConnection(
OPTIONS.devtools_hostname, OPTIONS.devtools_port)
self._StartConnection(connection)
yield connection
finally:
process.kill()
if using_temp_profile_dir:
shutil.rmtree(profile_dir)