blob: 191d6d1de938973bcf690b0a7aec3384d28a9ba1 [file] [log] [blame]
#!/usr/bin/env python
# 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 skypy.paths
from skypy.skyserver import SkyServer
import argparse
import json
import logging
import os
import pipes
import requests
import signal
import subprocess
import sys
import time
import urlparse
SRC_ROOT = skypy.paths.Paths('ignored').src_root
sys.path.insert(0, os.path.join(SRC_ROOT, 'build', 'android'))
from pylib import android_commands
from pylib import constants
from pylib import forwarder
SUPPORTED_MIME_TYPES = [
'text/html',
'text/sky',
'text/plain',
]
DEFAULT_SKY_COMMAND_PORT = 7777
SKY_SERVER_PORT = 9999
PID_FILE_PATH = "/tmp/skydb.pids"
DEFAULT_URL = "https://raw.githubusercontent.com/domokit/mojo/master/sky/examples/home.sky"
# FIXME: Move this into mopy.config
def gn_args_from_build_dir(build_dir):
gn_cmd = [
'gn', 'args',
build_dir,
'--list', '--short'
]
config = {}
for line in subprocess.check_output(gn_cmd).strip().split('\n'):
# FIXME: This doesn't handle = in values.
key, value = line.split(' = ')
config[key] = value
return config
class SkyDebugger(object):
def __init__(self):
self.pids = {}
self.paths = None
def _server_root_for_url(self, url_or_path):
path = os.path.abspath(url_or_path)
if os.path.commonprefix([path, SRC_ROOT]) == SRC_ROOT:
server_root = SRC_ROOT
else:
server_root = os.path.dirname(path)
logging.warn(
'%s is outside of mojo root, using %s as server root' %
(path, server_root))
return server_root
def _in_chromoting(self):
return os.environ.get('CHROME_REMOTE_DESKTOP_SESSION', False)
def _wrap_for_android(self, shell_args):
build_dir_url = SkyServer.url_for_path(
self.pids['remote_sky_server_port'],
self.pids['sky_server_root'],
self.pids['build_dir'])
shell_args += ['--origin=%s' % build_dir_url]
# am shell --esa: (someone shoot me now)
# [--esa <EXTRA_KEY> <EXTRA_STRING_VALUE>[,<EXTRA_STRING_VALUE...]]
# (to embed a comma into a string escape it using "\,")
escaped_args = map(lambda arg: arg.replace(',', '\\,'), shell_args)
return [
'adb', 'shell',
'am', 'start',
'-W',
'-S',
'-a', 'android.intent.action.VIEW',
'-n', 'org.chromium.mojo.shell/.MojoShellActivity',
# FIXME: This quoting is very error-prone. Perhaps we should read
# our args from a file instead?
'--esa', 'parameters', ','.join(escaped_args),
]
def _build_mojo_shell_command(self, args):
content_handlers = ['%s,%s' % (mime_type, 'mojo:sky_viewer')
for mime_type in SUPPORTED_MIME_TYPES]
remote_command_port = self.pids.get('remote_sky_command_port', self.pids['sky_command_port'])
shell_args = [
'--v=1',
'--content-handlers=%s' % ','.join(content_handlers),
'--url-mappings=mojo:window_manager=mojo:sky_debugger',
'--args-for=mojo:sky_debugger_prompt %d' % remote_command_port,
'mojo:window_manager',
]
# FIXME: This probably is wrong for android?
if args.use_osmesa:
shell_args.append('--args-for=mojo:native_viewport_service --use-osmesa')
if 'remote_sky_server_port' in self.pids:
shell_command = self._wrap_for_android(shell_args)
else:
shell_command = [self.paths.mojo_shell_path] + shell_args
return shell_command
def _connect_to_device(self):
device = android_commands.AndroidCommands(
android_commands.GetAttachedDevices()[0])
device.EnableAdbRoot()
return device
def sky_server_for_args(self, args):
# FIXME: This is a hack. sky_server should just take a build_dir
# not a magical "configuration" name.
configuration = os.path.basename(os.path.normpath(args.build_dir))
server_root = self._server_root_for_url(args.url_or_path)
sky_server = SkyServer(self.paths, SKY_SERVER_PORT,
configuration, server_root)
return sky_server
def start_command(self, args):
# skypy.paths.Paths takes a root-relative build_dir argument. :(
build_dir = os.path.abspath(args.build_dir)
root_relative_build_dir = os.path.relpath(build_dir, SRC_ROOT)
# FIXME: Lame that we use self for a command-specific variable.
self.paths = skypy.paths.Paths(root_relative_build_dir)
self.stop_command(None) # Quit any existing process.
self.pids = {} # Clear out our pid file.
# FIXME: This is probably not the right way to compute is_android
# from the build directory?
gn_args = gn_args_from_build_dir(args.build_dir)
is_android = 'android_sdk_version' in gn_args
sky_server = self.sky_server_for_args(args)
self.pids['sky_server_pid'] = sky_server.start()
self.pids['sky_server_port'] = sky_server.port
self.pids['sky_server_root'] = sky_server.root
self.pids['build_dir'] = args.build_dir
self.pids['sky_command_port'] = args.command_port
if is_android:
# Pray to the build/android gods in their misspelled tongue.
constants.SetOutputDirectort(args.build_dir)
device = self._connect_to_device()
self.pids['device_serial'] = device.GetDevice()
forwarder.Forwarder.Map([(0, sky_server.port)], device)
device_http_port = forwarder.Forwarder.DevicePortForHostPort(
sky_server.port)
self.pids['remote_sky_server_port'] = device_http_port
port_string = 'tcp:%s' % args.command_port
subprocess.check_call([
'adb', 'forward', port_string, port_string
])
self.pids['remote_sky_command_port'] = args.command_port
shell_command = self._build_mojo_shell_command(args)
print ' '.join(map(pipes.quote, shell_command))
self.pids['mojo_shell_pid'] = subprocess.Popen(shell_command).pid
if args.gdb:
print "Sorry, I'm not sure how best to wire up --gdb to work"
print "with mojo_shell as a background process. For now use:"
print "gdb --pid %s" % self.pids['mojo_shell_pid']
shell_command = ['gdb'] + shell_command
if not self._wait_for_sky_command_port():
logging.error('Failed to start sky')
self.stop_command(None)
else:
self.load_command(args)
def _kill_if_exists(self, key, name):
pid = self.pids.pop(key, None)
if not pid:
logging.info('No pid for %s, nothing to do.' % name)
return
logging.info('Killing %s (%s).' % (name, pid))
try:
os.kill(pid, signal.SIGTERM)
except OSError:
logging.info('%s (%s) already gone.' % (name, pid))
def stop_command(self, args):
# TODO(eseidel): mojo_shell crashes when attempting graceful shutdown.
# self._send_command_to_sky('/quit')
self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
self._kill_if_exists('sky_server_pid', 'sky_server')
# We could be much more surgical here:
if 'remote_sky_command_port' in self.pids:
device = android_commands.AndroidCommands(
self.pids['device_serial'])
forwarder.Forwarder.UnmapAllDevicePorts(device)
if 'remote_sky_command_port' in self.pids:
# adb forward --remove takes the *host* port, not the remote port.
port_string = 'tcp:%s' % self.pids['sky_command_port']
subprocess.call(['adb', 'forward', '--remove', port_string])
def load_command(self, args):
if not urlparse.urlparse(args.url_or_path).scheme:
# The load happens on the remote device, use the remote port.
remote_sky_server_port = self.pids.get('remote_sky_server_port',
self.pids['sky_server_port'])
url = SkyServer.url_for_path(remote_sky_server_port,
self.pids['sky_server_root'], args.url_or_path)
else:
url = args.url_or_path
self._send_command_to_sky('/load', url)
def _command_base_url(self):
return 'http://localhost:%s' % self.pids['sky_command_port']
def _send_command_to_sky(self, command_path, payload=None):
url = 'http://localhost:%s%s' % (
self.pids['sky_command_port'], command_path)
if payload:
response = requests.post(url, payload)
else:
response = requests.get(url)
print response.text
# FIXME: These could be made into a context object with __enter__/__exit__.
def _load_pid_file(self, path):
try:
with open(path, 'r') as pid_file:
return json.load(pid_file)
except:
if os.path.exists(path):
logging.warn('Failed to read pid file: %s' % path)
return {}
def _write_pid_file(self, path, pids):
try:
with open(path, 'w') as pid_file:
json.dump(pids, pid_file, indent=2, sort_keys=True)
except:
logging.warn('Failed to write pid file: %s' % path)
def _add_basic_command(self, subparsers, name, url_path, help_text):
parser = subparsers.add_parser(name, help=help_text)
command = lambda args: self._send_command_to_sky(url_path)
parser.set_defaults(func=command)
def _wait_for_sky_command_port(self):
tries = 0
while True:
try:
self._send_command_to_sky('/')
return True
except:
tries += 1
if tries == 3:
logging.warn('Still waiting for sky on port %s' %
self.pids['sky_command_port'])
if tries > 10:
return False
time.sleep(1)
def logcat_command(self, args):
TAGS = [
'AndroidHandler',
'MojoMain',
'MojoShellActivity',
'MojoShellApplication',
'chromium',
]
subprocess.call(['adb', 'logcat', '-s'] + TAGS)
def main(self):
logging.basicConfig(level=logging.INFO)
logging.getLogger("requests").setLevel(logging.WARNING)
self.pids = self._load_pid_file(PID_FILE_PATH)
parser = argparse.ArgumentParser(description='Sky launcher/debugger')
subparsers = parser.add_subparsers(help='sub-command help')
start_parser = subparsers.add_parser('start',
help='launch a new mojo_shell with sky')
start_parser.add_argument('--gdb', action='store_true')
start_parser.add_argument('--command-port', type=int,
default=DEFAULT_SKY_COMMAND_PORT)
start_parser.add_argument('--use-osmesa', action='store_true',
default=self._in_chromoting())
start_parser.add_argument('build_dir', type=str)
start_parser.add_argument('url_or_path', nargs='?', type=str,
default=DEFAULT_URL)
start_parser.add_argument('--show-command', action='store_true',
help='Display the shell command and exit')
start_parser.set_defaults(func=self.start_command)
stop_parser = subparsers.add_parser('stop',
help=('stop sky (as listed in %s)' % PID_FILE_PATH))
stop_parser.set_defaults(func=self.stop_command)
logcat_parser = subparsers.add_parser('logcat',
help=('dump sky-related logs from device'))
logcat_parser.set_defaults(func=self.logcat_command)
self._add_basic_command(subparsers, 'trace', '/trace',
'toggle tracing')
self._add_basic_command(subparsers, 'reload', '/reload',
'reload the current page')
self._add_basic_command(subparsers, 'inspect', '/inspect',
'stop the running sky instance')
load_parser = subparsers.add_parser('load',
help='load a new page in the currently running sky')
load_parser.add_argument('url_or_path', type=str)
load_parser.set_defaults(func=self.load_command)
args = parser.parse_args()
args.func(args)
self._write_pid_file(PID_FILE_PATH, self.pids)
if __name__ == '__main__':
SkyDebugger().main()