blob: 09b516fa3c723c52481cf31572438c1b4e97b682 [file] [log] [blame]
# Copyright 2014 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 atexit
import json
import logging
import os
import os.path
import subprocess
import sys
import threading
import time
import SimpleHTTPServer
import SocketServer
from mopy.config import Config
from mopy.paths import Paths
sys.path.insert(0, os.path.join(Paths().src_root, 'build', 'android'))
from pylib import android_commands
from pylib import constants
from pylib import forwarder
# Tags used by the mojo shell application logs.
LOGCAT_TAGS = [
'AndroidHandler',
'MojoFileHelper',
'MojoMain',
'MojoShellActivity',
'MojoShellApplication',
'chromium',
]
MOJO_SHELL_PACKAGE_NAME = 'org.chromium.mojo.shell'
class Context(object):
"""
The context object allowing to run multiple runs of the shell.
"""
def __init__(self, device, device_port):
self.device = device
self.device_port = device_port
class _SilentTCPServer(SocketServer.TCPServer):
"""
A TCPServer that won't display any error, unless debugging is enabled. This is
useful because the client might stop while it is fetching an URL, which causes
spurious error messages.
"""
def handle_error(self, request, client_address):
"""
Override the base class method to have conditional logging.
"""
if logging.getLogger().isEnabledFor(logging.DEBUG):
super(_SilentTCPServer, self).handle_error(request, client_address)
def _GetHandlerClassForPath(base_path):
class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
"""
Handler for SocketServer.TCPServer that will serve the files from
|base_path| directory over http.
"""
def translate_path(self, path):
path_from_current = (
SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(self, path))
return os.path.join(base_path, os.path.relpath(path_from_current))
def log_message(self, *_):
"""
Override the base class method to disable logging.
"""
pass
return RequestHandler
def _ExitIfNeeded(process):
"""
Exits |process| if it is still alive.
"""
if process.poll() is None:
process.kill()
def _ReadFifo(context, fifo_path, pipe, on_fifo_closed, max_attempts=5):
"""
Reads |fifo_path| on the device and write the contents to |pipe|. Calls
|on_fifo_closed| when the fifo is closed. This method will try to find the
path up to |max_attempts|, waiting 1 second between each attempt. If it cannot
find |fifo_path|, a exception will be raised.
"""
def Run():
def _WaitForFifo():
for _ in xrange(max_attempts):
if context.device.FileExistsOnDevice(fifo_path):
return
time.sleep(1)
if on_fifo_closed:
on_fifo_closed()
raise Exception("Unable to find fifo.")
_WaitForFifo()
stdout_cat = subprocess.Popen([constants.GetAdbPath(),
'shell',
'cat',
fifo_path],
stdout=pipe)
atexit.register(_ExitIfNeeded, stdout_cat)
stdout_cat.wait()
if on_fifo_closed:
on_fifo_closed()
thread = threading.Thread(target=Run, name="StdoutRedirector")
thread.start()
def PrepareShellRun(config):
"""
Returns a context allowing a shell to be run.
This will start an internal http server to serve mojo applications, forward a
local port on the device to this http server and install the current version
of the mojo shell.
"""
build_dir = Paths(config).build_dir
constants.SetOutputDirectort(build_dir)
httpd = _SilentTCPServer(('127.0.0.1', 0), _GetHandlerClassForPath(build_dir))
atexit.register(httpd.shutdown)
host_port = httpd.server_address[1]
http_thread = threading.Thread(target=httpd.serve_forever)
http_thread.daemon = True
http_thread.start()
device = android_commands.AndroidCommands(
android_commands.GetAttachedDevices()[0])
device.EnableAdbRoot()
device.ManagedInstall(os.path.join(build_dir, 'apks', 'MojoShell.apk'),
keep_data=True,
package_name=MOJO_SHELL_PACKAGE_NAME)
atexit.register(forwarder.Forwarder.UnmapAllDevicePorts, device)
forwarder.Forwarder.Map([(0, host_port)], device)
context = Context(device,
forwarder.Forwarder.DevicePortForHostPort(host_port))
atexit.register(StopShell, context)
return context
def StartShell(context, arguments, stdout=None, on_application_stop=None):
"""
Starts the mojo shell, passing it the given arguments.
If stdout is not None, it should be a valid argument for subprocess.Popen.
"""
STDOUT_PIPE = "/data/data/%s/stdout.fifo" % MOJO_SHELL_PACKAGE_NAME
cmd = [constants.GetAdbPath(),
'shell',
'am',
'start',
'-W',
'-S',
'-a', 'android.intent.action.VIEW',
'-n', '%s/.MojoShellActivity' % MOJO_SHELL_PACKAGE_NAME]
parameters = ['--origin=http://127.0.0.1:%d/' % context.device_port]
if stdout or on_application_stop:
context.device.RunShellCommand('rm %s' % STDOUT_PIPE)
parameters.append('--fifo-path=%s' % STDOUT_PIPE)
_ReadFifo(context, STDOUT_PIPE, stdout, on_application_stop)
parameters += arguments
if parameters:
encodedParameters = json.dumps(parameters)
cmd += [ '--es', 'encodedParameters', encodedParameters]
with open(os.devnull, 'w') as devnull:
subprocess.Popen(cmd, stdout=devnull).wait()
def StopShell(context):
"""
Stops the mojo shell.
"""
context.device.RunShellCommand('am force-stop %s' % MOJO_SHELL_PACKAGE_NAME)
def CleanLogs(context):
"""
Cleans the logs on the device.
"""
context.device.RunShellCommand('logcat -c')
def ShowLogs():
"""
Displays the log for the mojo shell.
Returns the process responsible for reading the logs.
"""
logcat = subprocess.Popen([constants.GetAdbPath(),
'logcat',
'-s',
' '.join(LOGCAT_TAGS)],
stdout=sys.stdout)
atexit.register(_ExitIfNeeded, logcat)
return logcat
def GetFilePath(filename):
"""
Returns a path suitable for the application to create a file.
"""
return '/data/data/%s/files/%s' % (MOJO_SHELL_PACKAGE_NAME, filename)