mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
- Remove shelldb analyze, since we want to remove shelldb. - Make skyanalyzer pass the absolute path to dartanalyzer. I'm not sure why this is sometimes needed, but without it, I sometimes get file not found errors. - Remove some of the patterns we were ignoring, since either the errors have been fixed or the analyzer has been fixed. There's still hundreds of warnings in the mojo and sky packages (the ones in the sky packages are all bogus, I believe).
468 lines
16 KiB
Python
Executable File
468 lines
16 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# Copyright 2015 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 hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import pipes
|
|
import platform
|
|
import re
|
|
import signal
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urlparse
|
|
|
|
SKY_TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
SKY_ROOT = os.path.dirname(SKY_TOOLS_DIR)
|
|
SRC_ROOT = os.path.dirname(SKY_ROOT)
|
|
|
|
GDB_PORT = 8888
|
|
SKY_SERVER_PORT = 9888
|
|
OBSERVATORY_PORT = 8181
|
|
DEFAULT_URL = "https://domokit.github.io/home.dart"
|
|
APK_NAME = 'SkyShell.apk'
|
|
ADB_PATH = os.path.join(SRC_ROOT,
|
|
'third_party/android_tools/sdk/platform-tools/adb')
|
|
ANDROID_PACKAGE = "org.domokit.sky.shell"
|
|
ANDROID_COMPONENT = '%s/%s.SkyActivity' % (ANDROID_PACKAGE, ANDROID_PACKAGE)
|
|
SHA1_PATH = '/sdcard/%s/%s.sha1' % (ANDROID_PACKAGE, APK_NAME)
|
|
|
|
PID_FILE_PATH = "/tmp/shelldb.pids"
|
|
PID_FILE_KEYS = frozenset([
|
|
'remote_sky_server_port',
|
|
'sky_server_pid',
|
|
'sky_server_port',
|
|
'sky_server_root',
|
|
'build_dir',
|
|
'sky_shell_pid',
|
|
'remote_gdbserver_port',
|
|
])
|
|
|
|
SYSTEM_LIBS_ROOT_PATH = '/tmp/device_libs'
|
|
|
|
# This 'strict dictionary' approach is useful for catching typos.
|
|
class Pids(object):
|
|
def __init__(self, known_keys, contents=None):
|
|
self._known_keys = known_keys
|
|
self._dict = contents if contents is not None else {}
|
|
|
|
def __len__(self):
|
|
return len(self._dict)
|
|
|
|
def get(self, key, default=None):
|
|
assert key in self._known_keys, '%s not in known_keys' % key
|
|
return self._dict.get(key, default)
|
|
|
|
def __getitem__(self, key):
|
|
assert key in self._known_keys, '%s not in known_keys' % key
|
|
return self._dict[key]
|
|
|
|
def __setitem__(self, key, value):
|
|
assert key in self._known_keys, '%s not in known_keys' % key
|
|
self._dict[key] = value
|
|
|
|
def __delitem__(self, key):
|
|
assert key in self._known_keys, '%s not in known_keys' % key
|
|
del self._dict[key]
|
|
|
|
def __iter__(self):
|
|
return iter(self._dict)
|
|
|
|
def __contains__(self, key):
|
|
assert key in self._known_keys, '%s not in allowed_keys' % key
|
|
return key in self._dict
|
|
|
|
def clear(self):
|
|
self._dict = {}
|
|
|
|
def pop(self, key, default=None):
|
|
assert key in self._known_keys, '%s not in allowed_keys' % key
|
|
return self._dict.pop(key, default)
|
|
|
|
@classmethod
|
|
def read_from(cls, path, known_keys):
|
|
contents = {}
|
|
try:
|
|
with open(path, 'r') as pid_file:
|
|
contents = json.load(pid_file)
|
|
except:
|
|
if os.path.exists(path):
|
|
logging.warn('Failed to read pid file: %s' % path)
|
|
return cls(known_keys, contents)
|
|
|
|
def write_to(self, path):
|
|
try:
|
|
with open(path, 'w') as pid_file:
|
|
json.dump(self._dict, pid_file, indent=2, sort_keys=True)
|
|
except:
|
|
logging.warn('Failed to write pid file: %s' % path)
|
|
|
|
|
|
# A free function for possible future sharing with a 'load' command.
|
|
def _url_from_args(args, pids):
|
|
if urlparse.urlparse(args.url_or_path).scheme:
|
|
return args.url_or_path
|
|
# The load happens on the remote device, use the remote port.
|
|
remote_sky_server_port = pids.get('remote_sky_server_port',
|
|
pids['sky_server_port'])
|
|
return SkyServer.url_for_path(remote_sky_server_port,
|
|
pids['sky_server_root'], args.url_or_path)
|
|
|
|
|
|
def dev_packages_root(build_dir):
|
|
return os.path.join(build_dir, 'gen', 'dart-pkg', 'packages')
|
|
|
|
|
|
class SetBuildDir(object):
|
|
def add_subparser(self, subparsers):
|
|
start_parser = subparsers.add_parser('set_build_dir',
|
|
help='force the build_dir to a particular value without starting Sky')
|
|
start_parser.add_argument('build_dir', type=str)
|
|
start_parser.set_defaults(func=self.run)
|
|
|
|
def run(self, args, pids):
|
|
pids['build_dir'] = os.path.abspath(args.build_dir)
|
|
|
|
|
|
class StartSky(object):
|
|
def add_subparser(self, subparsers):
|
|
start_parser = subparsers.add_parser('start',
|
|
help='launch SkyShell.apk on the device')
|
|
start_parser.add_argument('build_dir', type=str)
|
|
start_parser.add_argument('--gdb', action="store_true")
|
|
start_parser.add_argument('url_or_path', nargs='?', type=str,
|
|
default=DEFAULT_URL)
|
|
start_parser.add_argument('--no_install', action="store_false",
|
|
default=True, dest="install",
|
|
help="Don't install SkyShell.apk before starting")
|
|
start_parser.set_defaults(func=self.run)
|
|
|
|
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 _sky_server_for_args(self, args, packages_root):
|
|
server_root = self._server_root_for_url(args.url_or_path)
|
|
sky_server = SkyServer(SKY_SERVER_PORT, server_root, packages_root)
|
|
return sky_server
|
|
|
|
def _find_remote_pid_for_package(self, package):
|
|
ps_output = subprocess.check_output([ADB_PATH, '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_PATH, '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 run(self, args, pids):
|
|
apk_path = os.path.join(args.build_dir, 'apks', APK_NAME)
|
|
if not os.path.exists(apk_path):
|
|
print "'%s' does not exist?" % apk_path
|
|
return 2
|
|
|
|
StopSky().run(args, pids)
|
|
|
|
packages_root = dev_packages_root(args.build_dir)
|
|
sky_server = self._sky_server_for_args(args, packages_root)
|
|
pids['sky_server_pid'] = sky_server.start()
|
|
pids['sky_server_port'] = sky_server.port
|
|
pids['sky_server_root'] = sky_server.root
|
|
|
|
pids['build_dir'] = os.path.abspath(args.build_dir)
|
|
|
|
if args.install:
|
|
# We might need to install a new APK, so check SHA1
|
|
source_sha1 = hashlib.sha1(open(apk_path, 'rb').read()).hexdigest()
|
|
dest_sha1 = subprocess.check_output([ADB_PATH, 'shell', 'cat', SHA1_PATH])
|
|
use_existing_apk = False
|
|
if source_sha1 == dest_sha1:
|
|
# Make sure that the APK didn't get uninstalled somehow
|
|
use_existing_apk = subprocess.check_output([
|
|
ADB_PATH, 'shell', 'pm', 'list', 'packages', ANDROID_PACKAGE
|
|
])
|
|
else:
|
|
# User is telling us not to bother installing an APK
|
|
use_existing_apk = True
|
|
|
|
if use_existing_apk:
|
|
# APK is already on the device, we only need to stop it
|
|
subprocess.check_call([
|
|
ADB_PATH, 'shell', 'am', 'force-stop', ANDROID_PACKAGE
|
|
])
|
|
else:
|
|
# Slow path, need to upload a new APK to the device
|
|
# -r to replace an existing apk, -d to allow version downgrade.
|
|
subprocess.check_call([ADB_PATH, 'install', '-r', '-d', apk_path])
|
|
# record the SHA1 of the APK we just pushed
|
|
with tempfile.NamedTemporaryFile() as fp:
|
|
fp.write(source_sha1)
|
|
fp.seek(0)
|
|
subprocess.check_call([ADB_PATH, 'push', fp.name, SHA1_PATH])
|
|
|
|
# Set up port forwarding for observatory
|
|
port_string = 'tcp:%s' % OBSERVATORY_PORT
|
|
subprocess.check_call([
|
|
ADB_PATH, 'forward', port_string, port_string
|
|
])
|
|
|
|
port_string = 'tcp:%s' % sky_server.port
|
|
subprocess.check_call([
|
|
ADB_PATH, 'reverse', port_string, port_string
|
|
])
|
|
pids['remote_sky_server_port'] = sky_server.port
|
|
|
|
subprocess.check_call([ADB_PATH, 'shell',
|
|
'am', 'start',
|
|
'-a', 'android.intent.action.VIEW',
|
|
'-d', _url_from_args(args, pids),
|
|
ANDROID_COMPONENT])
|
|
|
|
if not args.gdb:
|
|
return
|
|
|
|
# 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 pid on device!')
|
|
return
|
|
|
|
pids['sky_shell_pid'] = pid
|
|
|
|
# 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_PATH, 'shell',
|
|
gdb_server_path, '--attach',
|
|
':%d' % GDB_PORT,
|
|
str(pid)
|
|
]
|
|
print ' '.join(map(pipes.quote, gdbserver_cmd))
|
|
subprocess.Popen(gdbserver_cmd)
|
|
|
|
port_string = 'tcp:%d' % GDB_PORT
|
|
subprocess.check_call([
|
|
ADB_PATH, 'forward', port_string, port_string
|
|
])
|
|
pids['remote_gdbserver_port'] = GDB_PORT
|
|
|
|
|
|
class GDBAttach(object):
|
|
def add_subparser(self, subparsers):
|
|
start_parser = subparsers.add_parser('gdb_attach',
|
|
help='attach to gdbserver running on device')
|
|
start_parser.set_defaults(func=self.run)
|
|
|
|
def _pull_system_libraries(self, pids, system_libs_root):
|
|
# Pull down the system libraries this pid has already mapped in.
|
|
# TODO(eseidel): This does not handle dynamic loads.
|
|
library_cacher_path = os.path.join(
|
|
SKY_TOOLS_DIR, 'android_library_cacher.py')
|
|
subprocess.call([
|
|
library_cacher_path, system_libs_root, pids['sky_shell_pid']
|
|
])
|
|
|
|
# TODO(eseidel): adb_gdb does, this, unclear why solib-absolute-prefix
|
|
# doesn't make this explicit listing not necessary?
|
|
return subprocess.check_output([
|
|
'find', system_libs_root,
|
|
'-mindepth', '1',
|
|
'-maxdepth', '4',
|
|
'-type', 'd',
|
|
]).strip().split('\n')
|
|
|
|
def run(self, args, pids):
|
|
symbol_search_paths = [
|
|
pids['build_dir'],
|
|
]
|
|
gdb_path = '/usr/bin/gdb'
|
|
|
|
eval_commands = [
|
|
'directory %s' % SRC_ROOT,
|
|
# TODO(eseidel): What file do I point it at? The apk?
|
|
#'file %s' % self.paths.mojo_shell_path,
|
|
'target remote localhost:%s' % GDB_PORT,
|
|
]
|
|
|
|
# TODO(iansf): Fix undefined behavior when you have more than one device attached.
|
|
device_id = subprocess.check_output([ADB_PATH, 'get-serialno']).strip()
|
|
device_libs_path = os.path.join(SYSTEM_LIBS_ROOT_PATH, device_id)
|
|
|
|
system_lib_dirs = self._pull_system_libraries(pids, device_libs_path)
|
|
eval_commands.append('set solib-absolute-prefix %s' % device_libs_path)
|
|
|
|
symbol_search_paths = system_lib_dirs + symbol_search_paths
|
|
|
|
# TODO(eseidel): We need to look up the toolchain somehow?
|
|
if platform.system() == 'Darwin':
|
|
gdb_path = os.path.join(SRC_ROOT, 'third_party/android_tools/ndk/'
|
|
'toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/'
|
|
'bin/arm-linux-androideabi-gdb')
|
|
else:
|
|
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')
|
|
|
|
# Set solib-search-path after letting android modify symbol_search_paths
|
|
eval_commands.append(
|
|
'set solib-search-path %s' % ':'.join(symbol_search_paths))
|
|
|
|
exec_command = [gdb_path]
|
|
for command in eval_commands:
|
|
exec_command += ['--eval-command', command]
|
|
|
|
print " ".join(exec_command)
|
|
|
|
# Write out our pid file before we exec ourselves.
|
|
pids.write_to(PID_FILE_PATH)
|
|
|
|
# Exec gdb directly to avoid python intercepting symbols, etc.
|
|
os.execv(exec_command[0], exec_command)
|
|
|
|
|
|
|
|
class StopSky(object):
|
|
def add_subparser(self, subparsers):
|
|
stop_parser = subparsers.add_parser('stop',
|
|
help=('kill all running SkyShell.apk processes'))
|
|
stop_parser.set_defaults(func=self.run)
|
|
|
|
def _kill_if_exists(self, pids, key, name):
|
|
pid = 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 _adb_reverse_remove(self, port):
|
|
port_string = 'tcp:%s' % port
|
|
subprocess.call([ADB_PATH, 'reverse', '--remove', port_string])
|
|
|
|
def _adb_forward_remove(self, port):
|
|
port_string = 'tcp:%s' % port
|
|
subprocess.call([ADB_PATH, 'forward', '--remove', port_string])
|
|
|
|
def run(self, args, pids):
|
|
self._kill_if_exists(pids, 'sky_server_pid', 'sky_server')
|
|
|
|
if 'remote_sky_server_port' in pids:
|
|
self._adb_reverse_remove(pids['remote_sky_server_port'])
|
|
|
|
if 'remote_gdbserver_port' in pids:
|
|
self._adb_forward_remove(pids['remote_gdbserver_port'])
|
|
|
|
subprocess.call([
|
|
ADB_PATH, 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
|
|
|
|
pids.clear()
|
|
|
|
|
|
class StartTracing(object):
|
|
def add_subparser(self, subparsers):
|
|
start_tracing_parser = subparsers.add_parser('start_tracing',
|
|
help=('start tracing a running sky instance'))
|
|
start_tracing_parser.set_defaults(func=self.run)
|
|
|
|
def run(self, args, pids):
|
|
subprocess.check_output([ADB_PATH, 'shell',
|
|
'am', 'broadcast',
|
|
'-a', 'org.domokit.sky.shell.TRACING_START'])
|
|
|
|
|
|
TRACE_COMPLETE_REGEXP = re.compile('Trace complete')
|
|
TRACE_FILE_REGEXP = re.compile(r'Saving trace to (?P<path>\S+)')
|
|
|
|
class StopTracing(object):
|
|
def add_subparser(self, subparsers):
|
|
stop_tracing_parser = subparsers.add_parser('stop_tracing',
|
|
help=('stop tracing a running sky instance'))
|
|
stop_tracing_parser.set_defaults(func=self.run)
|
|
|
|
def run(self, args, pids):
|
|
subprocess.check_output([ADB_PATH, 'logcat', '-c'])
|
|
subprocess.check_output([ADB_PATH, 'shell',
|
|
'am', 'broadcast',
|
|
'-a', 'org.domokit.sky.shell.TRACING_STOP'])
|
|
device_path = None
|
|
is_complete = False
|
|
while not is_complete:
|
|
time.sleep(0.2)
|
|
log = subprocess.check_output([ADB_PATH, 'logcat', '-d'])
|
|
if device_path is None:
|
|
result = TRACE_FILE_REGEXP.search(log)
|
|
if result:
|
|
device_path = result.group('path')
|
|
is_complete = TRACE_COMPLETE_REGEXP.search(log) is not None
|
|
|
|
print 'Downloading trace %s ...' % os.path.basename(device_path)
|
|
|
|
if device_path:
|
|
subprocess.check_output([ADB_PATH, 'pull', device_path])
|
|
subprocess.check_output([ADB_PATH, 'shell', 'rm', device_path])
|
|
|
|
|
|
class SkyShellRunner(object):
|
|
def main(self):
|
|
logging.basicConfig(level=logging.WARNING)
|
|
|
|
parser = argparse.ArgumentParser(description='Sky Shell Runner')
|
|
subparsers = parser.add_subparsers(help='sub-command help')
|
|
|
|
commands = [
|
|
SetBuildDir(),
|
|
StartSky(),
|
|
StopSky(),
|
|
GDBAttach(),
|
|
StartTracing(),
|
|
StopTracing(),
|
|
]
|
|
|
|
for command in commands:
|
|
command.add_subparser(subparsers)
|
|
|
|
args = parser.parse_args()
|
|
pids = Pids.read_from(PID_FILE_PATH, PID_FILE_KEYS)
|
|
exit_code = args.func(args, pids)
|
|
# We could do this with an at-exit handler instead?
|
|
pids.write_to(PID_FILE_PATH)
|
|
sys.exit(exit_code)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
SkyShellRunner().main()
|