mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
This only barely works. We pull down system libraries once before we attach. The libraries we pull from the device do not have debug symbols, but have enough that we have decent callstacks. We launch a background process to repeatedly update a cache directory with symlinks into our build directory corresponding to the cache names used on the device, however gdb doesn't watch the solib-search-path directories to notice the links as we add them. Better solutions would be to add support for pulling down full android symboled system images and using those instead of pulling libraries off the device as well as figure out how to get android binaries to support build-id so that we can present a directory of build-id associated libraries to gdb on boot and have it to build-id based lookups of libraries instead of our current broken watch-logs-and-add-symlinks approach. If you know what you're doing with this you can actually make debugging work on the device. It's not particularly user friendly yet, but we'll work on it. I added a build_dir member to skypy.paths.Paths as a temporary measure before we move off of skypy's paths and onto mopy's paths. This helped eliminate problems with using a relative path in args.build_dir as is common. R=abarth@chromium.org, ojan@chromium.org, qsr@chromium.org BUG= Review URL: https://codereview.chromium.org/855663003
526 lines
20 KiB
Python
Executable File
526 lines
20 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 re
|
|
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, is_android):
|
|
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',
|
|
]
|
|
|
|
# Desktop-only work-around for mojo crashing under chromoting.
|
|
if not is_android and args.use_osmesa:
|
|
shell_args.append(
|
|
'--args-for=mojo:native_viewport_service --use-osmesa')
|
|
|
|
if is_android and args.gdb:
|
|
shell_args.append('--wait_for_debugger')
|
|
|
|
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(self.paths.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 _find_remote_pid_for_package(self, package):
|
|
ps_output = subprocess.check_output(['adb', 'shell', 'ps'])
|
|
for line in ps_output.split('\n'):
|
|
fields = line.split()
|
|
if fields and fields[-1] == package:
|
|
return fields[1]
|
|
return None
|
|
|
|
def _find_install_location_for_package(self, package):
|
|
pm_command = ['adb', 'shell', 'pm', 'path', package]
|
|
pm_output = subprocess.check_output(pm_command)
|
|
# e.g. package:/data/app/org.chromium.mojo.shell-1/base.apk
|
|
return pm_output.split(':')[-1]
|
|
|
|
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.
|
|
|
|
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(self.paths.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'] = self.paths.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(self.paths.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, is_android)
|
|
|
|
# On android we can't launch inside gdb, but rather have to attach.
|
|
if not is_android and args.gdb:
|
|
shell_command = ['gdbserver', ':%d' % GDB_PORT] + shell_command
|
|
|
|
print ' '.join(map(pipes.quote, shell_command))
|
|
# This pid is meaningless on android (it's the adb shell pid)
|
|
start_command_pid = subprocess.Popen(shell_command).pid
|
|
|
|
if is_android:
|
|
# TODO(eseidel): am start -W does not seem to work?
|
|
pid_tries = 0
|
|
while True:
|
|
pid = self._find_remote_pid_for_package(ANDROID_PACKAGE)
|
|
if pid or pid_tries > 3:
|
|
break
|
|
logging.debug('No pid for %s yet, waiting' % ANDROID_PACKAGE)
|
|
time.sleep(5)
|
|
pid_tries += 1
|
|
|
|
if not pid:
|
|
logging.error('Failed to find mojo_shell pid on device!')
|
|
return
|
|
self.pids['mojo_shell_pid'] = pid
|
|
else:
|
|
self.pids['mojo_shell_pid'] = start_command_pid
|
|
|
|
if args.gdb and is_android:
|
|
# We push our own copy of gdbserver with the package since
|
|
# the default gdbserver is a different version from our gdb.
|
|
package_path = \
|
|
self._find_install_location_for_package(ANDROID_PACKAGE)
|
|
gdb_server_path = os.path.join(
|
|
os.path.dirname(package_path), 'lib/arm/gdbserver')
|
|
gdbserver_cmd = [
|
|
'adb', 'shell',
|
|
gdb_server_path, '--attach',
|
|
':%d' % GDB_PORT,
|
|
str(self.pids['mojo_shell_pid'])
|
|
]
|
|
print ' '.join(map(pipes.quote, gdbserver_cmd))
|
|
self.pids['adb_shell_gdbserver_pid'] = \
|
|
subprocess.Popen(gdbserver_cmd).pid
|
|
|
|
port_string = 'tcp:%d' % 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 (%d).' % (name, pid))
|
|
try:
|
|
os.kill(pid, signal.SIGTERM)
|
|
except OSError:
|
|
logging.info('%s (%d) 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('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])
|
|
else:
|
|
# Only try to kill mojo_shell if it's running locally.
|
|
self._kill_if_exists('mojo_shell_pid', 'mojo_shell')
|
|
|
|
if 'remote_gdbserver_port' in self.pids:
|
|
self._kill_if_exists('adb_shell_gdbserver_pid',
|
|
'adb shell gdbserver')
|
|
|
|
port_string = 'tcp:%s' % self.pids['remote_gdbserver_port']
|
|
subprocess.call(['adb', 'forward', '--remove', port_string])
|
|
self.pids = {} # Clear out our pid file.
|
|
|
|
self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
|
|
|
|
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'])
|
|
|
|
self._kill_if_exists('mojo_cache_linker_pid', 'mojo cache linker')
|
|
|
|
links_path = '/tmp/mojo_cache_links'
|
|
if not os.path.exists(links_path):
|
|
os.makedirs(links_path)
|
|
shell_link_path = os.path.join(links_path, 'libmojo_shell.so')
|
|
if os.path.lexists(shell_link_path):
|
|
os.unlink(shell_link_path)
|
|
os.symlink(self.paths.mojo_shell_path, shell_link_path)
|
|
|
|
logcat_cmd = ['adb', 'logcat']
|
|
logcat = subprocess.Popen(logcat_cmd, stdout=subprocess.PIPE)
|
|
|
|
mojo_cache_linker_path = os.path.join(
|
|
self.paths.sky_tools_directory, 'mojo_cache_linker.py')
|
|
cache_linker_cmd = [
|
|
mojo_cache_linker_path,
|
|
links_path,
|
|
self.pids['build_dir'],
|
|
'http://localhost:%s' % self.pids['remote_sky_server_port']
|
|
]
|
|
self.pids['mojo_cache_linker_pid'] = \
|
|
subprocess.Popen(cache_linker_cmd, stdin=logcat.stdout).pid
|
|
|
|
# Write out our pid file before we exec ourselves.
|
|
self._write_pid_file(PID_FILE_PATH, self.pids)
|
|
|
|
# Pull down the system libraries this pid has already mapped in.
|
|
# TODO(eseidel): This does not handle dynamic loads well.
|
|
system_libs_root = '/tmp/device_libs'
|
|
library_cacher_path = os.path.join(
|
|
self.paths.sky_tools_directory, 'android_library_cacher.py')
|
|
subprocess.call([
|
|
library_cacher_path, system_libs_root, self.pids['mojo_shell_pid']
|
|
])
|
|
|
|
# TODO(eseidel): adb_gdb does, this, unclear why solib-absolute-prefix
|
|
# doesn't make this explicit listing not necessary?
|
|
system_lib_dirs = subprocess.check_output([
|
|
'find', system_libs_root,
|
|
'-mindepth', '1',
|
|
'-maxdepth', '4',
|
|
'-type', 'd',
|
|
]).strip().split('\n')
|
|
|
|
# TODO(eseidel): Need to sync down system libraries into a directory.
|
|
symbol_search_paths = system_lib_dirs + [
|
|
links_path,
|
|
self.pids['build_dir'],
|
|
]
|
|
# TODO(eseidel): We need to look up the toolchain somehow?
|
|
gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/'
|
|
'toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64/'
|
|
'bin/arm-linux-androideabi-gdb')
|
|
gdb_command = [
|
|
gdb_path,
|
|
'--eval-command', 'file %s' % self.paths.mojo_shell_path,
|
|
'--eval-command', 'directory %s' % self.paths.src_root,
|
|
'--eval-command', 'target remote localhost:%s' % GDB_PORT,
|
|
'--eval-command', 'set solib-search-path %s' %
|
|
':'.join(symbol_search_paths),
|
|
'--eval-command', 'set solib-absolute-prefix %s' % system_libs_root,
|
|
]
|
|
print " ".join(gdb_command)
|
|
# We don't want python listening 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.WARNING)
|
|
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')
|
|
self._add_basic_command(subparsers, 'start_profiling', '/start_profiling',
|
|
'starts profiling the running sky instance (Linux only)')
|
|
self._add_basic_command(subparsers, 'stop_profiling', '/stop_profiling',
|
|
'stios profiling the running sky instance (Linux only)')
|
|
|
|
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()
|