mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
502 lines
18 KiB
Python
Executable File
502 lines
18 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 Analyze(object):
|
|
def add_subparser(self, subparsers):
|
|
analyze_parser = subparsers.add_parser('analyze',
|
|
help=('run the dart analyzer with sky url mappings'))
|
|
analyze_parser.add_argument('app_path', type=str)
|
|
analyze_parser.add_argument('--congratulate', action="store_true")
|
|
analyze_parser.set_defaults(func=self.run)
|
|
|
|
def run(self, args, pids):
|
|
build_dir = pids.get('build_dir')
|
|
if not build_dir:
|
|
logging.fatal("pids file missing build_dir. Try 'start' first.")
|
|
return 2
|
|
analyzer_path = os.path.join(SRC_ROOT, 'sky/tools/skyanalyzer')
|
|
analyzer_args = [
|
|
analyzer_path,
|
|
args.app_path
|
|
]
|
|
if args.congratulate:
|
|
analyzer_args.append('--congratulate')
|
|
try:
|
|
output = subprocess.check_output(analyzer_args, stderr=subprocess.STDOUT)
|
|
result = 0
|
|
except subprocess.CalledProcessError as e:
|
|
output = e.output
|
|
result = e.returncode
|
|
lines = output.split('\n')
|
|
lines.pop()
|
|
for line in lines:
|
|
print >> sys.stderr, line
|
|
return result
|
|
|
|
|
|
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(),
|
|
Analyze(),
|
|
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()
|