mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Looks like a variable rename was missed. R=esprehn@chromium.org Review URL: https://codereview.chromium.org/815143004
404 lines
15 KiB
Python
Executable File
404 lines
15 KiB
Python
Executable File
#!/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.
|
|
|
|
from skypy.skyserver import SkyServer
|
|
import argparse
|
|
import json
|
|
import logging
|
|
import os
|
|
import pipes
|
|
import requests
|
|
import signal
|
|
import skypy.paths
|
|
import StringIO
|
|
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
|
|
GDB_PORT = 8888
|
|
SKY_SERVER_PORT = 9999
|
|
PID_FILE_PATH = "/tmp/skydb.pids"
|
|
DEFAULT_URL = "https://raw.githubusercontent.com/domokit/mojo/master/sky/examples/home.sky"
|
|
|
|
ANDROID_PACKAGE = "org.chromium.mojo.shell"
|
|
ANDROID_ACTIVITY = "%s/.MojoShellActivity" % ANDROID_PACKAGE
|
|
|
|
# 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', ANDROID_ACTIVITY,
|
|
# 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',
|
|
]
|
|
|
|
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 _create_paths_for_build_dir(self, build_dir):
|
|
# skypy.paths.Paths takes a root-relative build_dir argument. :(
|
|
abs_build_dir = os.path.abspath(build_dir)
|
|
root_relative_build_dir = os.path.relpath(abs_build_dir, SRC_ROOT)
|
|
return skypy.paths.Paths(root_relative_build_dir)
|
|
|
|
def start_command(self, args):
|
|
# FIXME: Lame that we use self for a command-specific variable.
|
|
self.paths = self._create_paths_for_build_dir(args.build_dir)
|
|
self.stop_command(None) # Quit any existing process.
|
|
self.pids = {} # Clear out our pid file.
|
|
|
|
if not os.path.exists(self.paths.mojo_shell_path):
|
|
print "mojo_shell not found in build_dir '%s'" % args.build_dir
|
|
print "Are you sure you sure that's a valid build_dir location?"
|
|
print "See skydb start --help for more info"
|
|
sys.exit(2)
|
|
|
|
# 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)
|
|
|
|
if not is_android:
|
|
# Desktop-only work-around for mojo crashing under chromoting.
|
|
if args.use_osmesa:
|
|
shell_command.append(
|
|
'--args-for=mojo:native_viewport_service --use-osmesa')
|
|
|
|
# On android we can't launch inside gdb, but rather have to attach.
|
|
if args.gdb:
|
|
shell_command = ['gdbserver', ':%s' % GDB_PORT] + shell_command
|
|
|
|
print ' '.join(map(pipes.quote, shell_command))
|
|
self.pids['mojo_shell_pid'] = subprocess.Popen(shell_command).pid
|
|
|
|
if args.gdb and is_android:
|
|
gdbserver_cmd = ['gdbserver', '--attach', ':%s' % GDB_PORT]
|
|
self.pids['remote_gdbserver_pid'] = subprocess.Popen(shell_command).pid
|
|
|
|
port_string = 'tcp:%s' % GDB_PORT
|
|
subprocess.check_call([
|
|
'adb', 'forward', port_string, port_string
|
|
])
|
|
self.pids['remote_gdbserver_port'] = GDB_PORT
|
|
|
|
if not args.gdb:
|
|
if not self._wait_for_sky_command_port():
|
|
logging.error('Failed to start sky')
|
|
self.stop_command(None)
|
|
else:
|
|
self.load_command(args)
|
|
else:
|
|
print 'No load issued, connect with gdb first and then run load.'
|
|
|
|
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_server_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])
|
|
|
|
subprocess.call([
|
|
'adb', 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
|
|
|
|
if 'remote_gdbserver_port' in self.pids:
|
|
port_string = 'tcp:%s' % self.pids['remote_gdbserver_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', '-d', '-s'] + TAGS)
|
|
|
|
def gdb_attach_command(self, args):
|
|
self.paths = self._create_paths_for_build_dir(self.pids['build_dir'])
|
|
gdb_command = [
|
|
'/usr/bin/gdb', self.paths.mojo_shell_path,
|
|
'--eval-command', 'target remote localhost:%s' % GDB_PORT
|
|
]
|
|
print " ".join(gdb_command)
|
|
# We don't want python listenting for signals or anything, so exec
|
|
# gdb and let it take the entire process.
|
|
os.execv(gdb_command[0], gdb_command)
|
|
|
|
def print_crash_command(self, args):
|
|
logcat_cmd = ['adb', 'logcat', '-d']
|
|
logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
|
|
|
|
stack_path = os.path.join(SRC_ROOT,
|
|
'tools', 'android_stack_parser', 'stack')
|
|
stack = subprocess.Popen([stack_path, '-'], stdin=logcat.stdout)
|
|
logcat.wait()
|
|
stack.wait()
|
|
|
|
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)
|
|
|
|
print_crash_parser = subparsers.add_parser('print_crash',
|
|
help=('dump (and symbolicate) recent crash-stacks'))
|
|
print_crash_parser.set_defaults(func=self.print_crash_command)
|
|
|
|
gdb_attach_parser = subparsers.add_parser('gdb_attach',
|
|
help='launch gdb and attach to gdbserver launched from start --gdb')
|
|
gdb_attach_parser.set_defaults(func=self.gdb_attach_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()
|