mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Also refactors ios_sim.py to be part of sky_tool. The current skyx file that ‘listen’ generates does not contain material design icons, so icons will be missing.
830 lines
29 KiB
Python
Executable File
830 lines
29 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.
|
|
|
|
import argparse
|
|
import atexit
|
|
import errno
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
import re
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urlparse
|
|
|
|
PACKAGES_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SKY_ENGINE_DIR = os.path.join(PACKAGES_DIR, 'sky_engine')
|
|
APK_DIR = os.path.join(os.path.realpath(SKY_ENGINE_DIR), os.pardir, 'apks')
|
|
|
|
SKY_SERVER_PORT = 9888
|
|
OBSERVATORY_PORT = 8181
|
|
ADB_PATH = 'adb'
|
|
APK_NAME = 'SkyShell.apk'
|
|
ANDROID_PACKAGE = "org.domokit.sky.shell"
|
|
ANDROID_COMPONENT = '%s/%s.SkyActivity' % (ANDROID_PACKAGE, ANDROID_PACKAGE)
|
|
|
|
SKY_SHELL_APP_ID = 'com.google.SkyShell'
|
|
IOS_APP_NAME = 'SkyShell.app'
|
|
|
|
# FIXME: Do we need to look in $DART_SDK?
|
|
DART_PATH = 'dart'
|
|
PUB_PATH = 'pub'
|
|
|
|
PID_FILE_PATH = "/tmp/sky_tool.pids"
|
|
PID_FILE_KEYS = frozenset([
|
|
'remote_sky_server_port',
|
|
'sky_server_pid',
|
|
'sky_server_port',
|
|
'sky_server_root',
|
|
])
|
|
|
|
IOS_SIM_PATH = [
|
|
'/Applications/iOS Simulator.app/Contents/MacOS/iOS Simulator'
|
|
]
|
|
|
|
SIMCTL_PATH = [
|
|
'/usr/bin/env',
|
|
'xcrun',
|
|
'simctl',
|
|
]
|
|
|
|
PLIST_BUDDY_PATH = [
|
|
'/usr/bin/env',
|
|
'xcrun',
|
|
'PlistBuddy',
|
|
]
|
|
|
|
|
|
def _port_in_use(port):
|
|
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
return sock.connect_ex(('localhost', port)) == 0
|
|
|
|
|
|
def _start_http_server(port, root):
|
|
server_command = [
|
|
PUB_PATH, 'run', 'sky_tools:sky_server', str(port),
|
|
]
|
|
return subprocess.Popen(server_command, cwd=root).pid
|
|
|
|
|
|
# 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 known_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):
|
|
# These keys are required to write a valid file.
|
|
if not self._dict.viewkeys() >= { 'sky_server_pid', 'sky_server_port' }:
|
|
return
|
|
|
|
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)
|
|
|
|
|
|
def _url_for_path(port, root, path):
|
|
relative_path = os.path.relpath(path, root)
|
|
return 'http://localhost:%s/%s' % (port, relative_path)
|
|
|
|
|
|
class StartSky(object):
|
|
def add_subparser(self, subparsers):
|
|
start_parser = subparsers.add_parser('start',
|
|
help='launch %s on the device' % APK_NAME)
|
|
start_parser.add_argument('--install', action='store_true')
|
|
start_parser.add_argument('--poke', action='store_true')
|
|
start_parser.add_argument('--checked', action='store_true')
|
|
start_parser.add_argument('project_or_path', nargs='?', type=str,
|
|
default='.')
|
|
start_parser.set_defaults(func=self.run)
|
|
|
|
def _is_package_installed(self, package_name):
|
|
pm_path_cmd = [ADB_PATH, 'shell', 'pm', 'path', package_name]
|
|
return subprocess.check_output(pm_path_cmd).strip() != ''
|
|
|
|
def _is_valid_script_path(self):
|
|
script_path = os.path.dirname(os.path.abspath(__file__))
|
|
script_dirs = script_path.split('/')
|
|
return len(script_dirs) > 1 and script_dirs[-2] == 'packages'
|
|
|
|
def run(self, args, pids):
|
|
if not args.poke:
|
|
StopSky().run(args, pids)
|
|
|
|
project_or_path = os.path.abspath(args.project_or_path)
|
|
|
|
if os.path.isdir(project_or_path):
|
|
sky_server_root = project_or_path
|
|
main_dart = os.path.join(project_or_path, 'lib', 'main.dart')
|
|
missing_msg = "Missing lib/main.dart in project: %s" % project_or_path
|
|
else:
|
|
# FIXME: This assumes the path is at the root of the project!
|
|
# Instead we should walk up looking for a pubspec.yaml
|
|
sky_server_root = os.path.dirname(project_or_path)
|
|
main_dart = project_or_path
|
|
missing_msg = "%s does not exist." % main_dart
|
|
|
|
if not os.path.isfile(main_dart):
|
|
logging.error(missing_msg)
|
|
return 2
|
|
|
|
package_root = os.path.join(sky_server_root, 'packages')
|
|
if not os.path.isdir(package_root):
|
|
logging.error("%s is not a valid packages path." % package_root)
|
|
return 2
|
|
|
|
if not self._is_package_installed(ANDROID_PACKAGE):
|
|
logging.info('%s is not on the device. Installing now...' % APK_NAME)
|
|
args.install = True
|
|
|
|
if args.install:
|
|
if not self._is_valid_script_path():
|
|
logging.error("'%s' must be located in packages/sky. " \
|
|
"The directory packages/sky_engine must also " \
|
|
"exist to locate %s." \
|
|
% (os.path.basename(__file__), APK_NAME))
|
|
return 2
|
|
if args.local_build:
|
|
apk_path = os.path.join(os.path.normpath(args.sky_src_path), args.android_debug_build_path, 'apks', APK_NAME)
|
|
else:
|
|
apk_path = os.path.join(APK_DIR, APK_NAME)
|
|
if not os.path.exists(apk_path):
|
|
logging.error("'%s' does not exist?" % apk_path)
|
|
return 2
|
|
|
|
cmd = [ADB_PATH, 'install', '-r', apk_path]
|
|
subprocess.check_call(cmd)
|
|
|
|
# Install on connected iOS device
|
|
if IOSDevice.is_connected() and args.local_build:
|
|
app_path = os.path.join(args.sky_src_path, args.ios_debug_build_path, IOS_APP_NAME)
|
|
IOSDevice.install_app(app_path)
|
|
|
|
# Install on iOS simulator if it's running
|
|
if IOSSimulator.is_booted() and args.local_build:
|
|
app_path = os.path.join(args.sky_src_path, args.ios_sim_debug_build_path, IOS_APP_NAME)
|
|
IOSSimulator.fork_install_app(app_path)
|
|
|
|
# Set up port forwarding for observatory
|
|
observatory_port_string = 'tcp:%s' % OBSERVATORY_PORT
|
|
subprocess.check_call([
|
|
ADB_PATH, 'forward', observatory_port_string, observatory_port_string
|
|
])
|
|
|
|
sky_server_port = SKY_SERVER_PORT
|
|
pids['sky_server_port'] = sky_server_port
|
|
if _port_in_use(sky_server_port):
|
|
logging.info(('Port %s already in use. '
|
|
' Not starting server for %s') % (sky_server_port, sky_server_root))
|
|
else:
|
|
sky_server_pid = _start_http_server(sky_server_port, sky_server_root)
|
|
pids['sky_server_pid'] = sky_server_pid
|
|
pids['sky_server_root'] = sky_server_root
|
|
|
|
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
|
|
|
|
# The load happens on the remote device, use the remote port.
|
|
url = _url_for_path(pids['remote_sky_server_port'], sky_server_root,
|
|
main_dart)
|
|
if args.poke:
|
|
url += '?rand=%s' % random.random()
|
|
|
|
cmd = [
|
|
ADB_PATH, 'shell',
|
|
'am', 'start',
|
|
'-a', 'android.intent.action.VIEW',
|
|
'-d', url,
|
|
]
|
|
|
|
if args.checked:
|
|
cmd += [ '--ez', 'enable-checked-mode', 'true' ]
|
|
|
|
cmd += [ ANDROID_COMPONENT ]
|
|
|
|
subprocess.check_output(cmd)
|
|
|
|
|
|
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 _run(self, args):
|
|
with open('/dev/null', 'w') as dev_null:
|
|
subprocess.call(args, stdout=dev_null, stderr=dev_null)
|
|
|
|
def run(self, args, pids):
|
|
self._run(['fuser', '-k', '%s/tcp' % SKY_SERVER_PORT])
|
|
|
|
if 'remote_sky_server_port' in pids:
|
|
port_string = 'tcp:%s' % pids['remote_sky_server_port']
|
|
self._run([ADB_PATH, 'reverse', '--remove', port_string])
|
|
|
|
self._run([ADB_PATH, 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
|
|
|
|
pids.clear()
|
|
|
|
|
|
class IOSDevice(object):
|
|
_has_ios_deploy = None
|
|
@classmethod
|
|
def has_ios_deploy(cls):
|
|
if cls._has_ios_deploy is not None:
|
|
return cls._has_ios_deploy
|
|
cmd = [
|
|
'which',
|
|
'ios-deploy'
|
|
]
|
|
out = subprocess.check_output(cmd)
|
|
match = re.search(r'ios-deploy', out)
|
|
cls._has_ios_deploy = match is not None
|
|
return cls._has_ios_deploy
|
|
|
|
_is_connected = False
|
|
@classmethod
|
|
def is_connected(cls):
|
|
if not cls.has_ios_deploy():
|
|
return False
|
|
if cls._is_connected:
|
|
return True
|
|
cmd = [
|
|
'ios-deploy',
|
|
'--detect',
|
|
'--timeout',
|
|
'1'
|
|
]
|
|
out = subprocess.check_output(cmd)
|
|
match = re.search(r'\[\.\.\.\.\] Found [^\)]*\) connected', out)
|
|
cls._is_connected = match is not None
|
|
return cls._is_connected
|
|
|
|
@classmethod
|
|
def install_app(cls, ios_app_path):
|
|
if not cls.has_ios_deploy():
|
|
return
|
|
cmd = [
|
|
'ios-deploy',
|
|
'--justlaunch',
|
|
'--timeout',
|
|
'10', # Smaller timeouts cause it to exit before having launched the app
|
|
'--bundle',
|
|
ios_app_path
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
@classmethod
|
|
def copy_file(cls, bundle_id, local_path, device_path):
|
|
if not cls.has_ios_deploy():
|
|
return
|
|
cmd = [
|
|
'ios-deploy',
|
|
'-t',
|
|
'1',
|
|
'--bundle_id',
|
|
bundle_id,
|
|
'--upload',
|
|
local_path,
|
|
'--to',
|
|
device_path
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
|
|
class IOSSimulator(object):
|
|
@classmethod
|
|
def is_booted(cls):
|
|
return cls.get_simulator_device_id() is not None
|
|
|
|
_device_id = None
|
|
@classmethod
|
|
def get_simulator_device_id(cls):
|
|
if cls._device_id is not None:
|
|
return cls._device_id
|
|
cmd = [
|
|
'xcrun',
|
|
'simctl',
|
|
'list',
|
|
'devices',
|
|
]
|
|
out = subprocess.check_output(cmd)
|
|
match = re.search(r'[^\(]+\(([^\)]+)\) \(Booted\)', out)
|
|
if match is not None and match.group(1) is not None:
|
|
cls._device_id = match.group(1)
|
|
return cls._device_id
|
|
else:
|
|
logging.warning('No running simulators found')
|
|
# TODO: Maybe start the simulator?
|
|
return None
|
|
if err is not None:
|
|
print(err)
|
|
exit(-1)
|
|
|
|
_simulator_path = None
|
|
@classmethod
|
|
def get_simulator_path(cls):
|
|
if cls._simulator_path is not None:
|
|
return cls._simulator_path
|
|
home_dir = os.path.expanduser("~")
|
|
device_id = cls.get_simulator_device_id()
|
|
if device_id is None:
|
|
# TODO: Maybe start the simulator?
|
|
return None
|
|
cls._simulator_path = os.path.join(home_dir, 'Library', 'Developer', 'CoreSimulator', 'Devices', device_id)
|
|
return cls._simulator_path
|
|
|
|
_simulator_app_id = None
|
|
@classmethod
|
|
def get_simulator_app_id(cls):
|
|
if cls._simulator_app_id is not None:
|
|
return cls._simulator_app_id
|
|
simulator_path = cls.get_simulator_path()
|
|
cmd = [
|
|
'find',
|
|
simulator_path + '/data/Containers/Data/Application',
|
|
'-name',
|
|
SKY_SHELL_APP_ID
|
|
]
|
|
out = subprocess.check_output(cmd)
|
|
match = re.search(r'Data\/Application\/([^\/]+)\/Documents\/' + SKY_SHELL_APP_ID, out)
|
|
if match is not None and match.group(1) is not None:
|
|
cls._simulator_app_id = match.group(1)
|
|
return cls._simulator_app_id
|
|
else:
|
|
logging.warning(SKY_SHELL_APP_ID + ' is not installed on the simulator')
|
|
# TODO: Maybe install the app?
|
|
return None
|
|
if err is not None:
|
|
print(err)
|
|
exit(-1)
|
|
|
|
_simulator_app_documents_dir = None
|
|
@classmethod
|
|
def get_simulator_app_documents_dir(cls):
|
|
if cls._simulator_app_documents_dir is not None:
|
|
return cls._simulator_app_documents_dir
|
|
if not cls.is_booted():
|
|
return None
|
|
simulator_path = cls.get_simulator_path()
|
|
simulator_app_id = cls.get_simulator_app_id()
|
|
if simulator_app_id is None:
|
|
return None
|
|
cls._simulator_app_documents_dir = os.path.join(simulator_path, 'data', 'Containers', 'Data', 'Application', simulator_app_id, 'Documents')
|
|
return cls._simulator_app_documents_dir
|
|
|
|
@classmethod
|
|
def fork_install_app(cls, ios_app_path):
|
|
cmd = [
|
|
os.path.abspath(__file__),
|
|
'ios_sim',
|
|
'-p',
|
|
# This path manipulation is to work around an issue where simctl fails to correctly parse
|
|
# paths that start with ../
|
|
ios_app_path,
|
|
'launch'
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
def get_application_identifier(self, path):
|
|
identifier = subprocess.check_output( PLIST_BUDDY_PATH + [
|
|
'-c',
|
|
'Print CFBundleIdentifier',
|
|
'%s/Info.plist' % path,
|
|
])
|
|
return identifier.strip()
|
|
|
|
def is_simulator_booted(self):
|
|
devices = subprocess.check_output( SIMCTL_PATH + [ 'list', 'devices' ]).strip().split('\n')
|
|
for device in devices:
|
|
if re.search(r'\(Booted\)', device):
|
|
return True
|
|
return False
|
|
|
|
# Launch whatever simulator the user last used, rather than try to guess which of their simulators they might want to use
|
|
def boot_simulator(self, args, pids):
|
|
if self.is_simulator_booted():
|
|
return
|
|
# Use Popen here because launching the simulator from the command line in this manner doesn't return, so we can't check the result.
|
|
if args.ios_sim_path:
|
|
subprocess.Popen(args.ios_sim_path)
|
|
else:
|
|
subprocess.Popen(IOS_SIM_PATH)
|
|
while not is_simulator_booted():
|
|
print('Waiting for iOS Simulator to boot...')
|
|
time.sleep(0.5)
|
|
|
|
def install_app(self, args, pids):
|
|
self.boot_simulator(args, pids)
|
|
cmd = SIMCTL_PATH + [
|
|
'install',
|
|
'booted',
|
|
args.path,
|
|
]
|
|
return subprocess.check_call(cmd)
|
|
|
|
def install_launch_and_wait(self, args, pids, wait):
|
|
res = self.install_app(args, pids)
|
|
if res != 0:
|
|
return res
|
|
identifier = self.get_application_identifier(args.path)
|
|
launch_args = [ 'launch' ]
|
|
if wait:
|
|
launch_args += [ '-w' ]
|
|
launch_args += [
|
|
'booted',
|
|
identifier,
|
|
'-target',
|
|
args.target,
|
|
'-server',
|
|
args.server
|
|
]
|
|
return subprocess.check_output( SIMCTL_PATH + launch_args ).strip()
|
|
|
|
def launch_app(self, args, pids):
|
|
self.install_launch_and_wait(args, pids, False)
|
|
|
|
def debug_app(self, args, pids):
|
|
launch_res = self.install_launch_and_wait(args, pids, True)
|
|
launch_pid = re.search('.*: (\d+)', launch_res).group(1)
|
|
return os.system(' '.join([
|
|
'/usr/bin/env',
|
|
'xcrun',
|
|
'lldb',
|
|
# TODO(iansf): get this working again
|
|
# '-s',
|
|
# os.path.join(os.path.dirname(__file__), 'lldb_start_commands.txt'),
|
|
'-p',
|
|
launch_pid,
|
|
]))
|
|
|
|
def add_subparser(self, subparsers):
|
|
simulator_parser = subparsers.add_parser('ios_sim',
|
|
help='A script that launches an'
|
|
' application in the simulator and attaches'
|
|
' the debugger to it.')
|
|
simulator_parser.add_argument('-p', dest='path', required=True,
|
|
help='Path to the simulator application.')
|
|
simulator_parser.add_argument('-t', dest='target', required=False,
|
|
default='examples/demo_launcher/lib/main.dart',
|
|
help='Sky server-relative path to the Sky app to run.')
|
|
simulator_parser.add_argument('-s', dest='server', required=False,
|
|
default='localhost:8080',
|
|
help='Sky server address.')
|
|
simulator_parser.add_argument('--ios_sim_path', dest='ios_sim_path',
|
|
help='Path to your iOS Simulator executable. '
|
|
'Not normally required.')
|
|
|
|
subparsers = simulator_parser.add_subparsers()
|
|
launch_parser = subparsers.add_parser('launch', help='Launch app')
|
|
launch_parser.set_defaults(func=self.launch_app)
|
|
install_parser = subparsers.add_parser('install', help='Install app')
|
|
install_parser.set_defaults(func=self.install_app)
|
|
debug_parser = subparsers.add_parser('debug', help='Debug app')
|
|
debug_parser.set_defaults(func=self.debug_app)
|
|
|
|
def run(self, args, pids):
|
|
return args.func(args)
|
|
|
|
|
|
class StartListening(object):
|
|
def add_subparser(self, subparsers):
|
|
listen_parser = subparsers.add_parser('listen',
|
|
help=('Listen for changes to files and reload the running app on all connected devices'))
|
|
listen_parser.set_defaults(func=self.run)
|
|
|
|
def run(self, args, pids):
|
|
cmd = [
|
|
'which',
|
|
'fswatch'
|
|
]
|
|
out = subprocess.check_output(cmd)
|
|
match = re.search(r'fswatch', out)
|
|
if match is None:
|
|
logging.error('"listen" command is only useful if you have installed fswatch. Run "brew install fswatch" to install it with homebrew.')
|
|
return
|
|
|
|
tempdir = None
|
|
currdir = None
|
|
while True:
|
|
# Watch filesystem for changes
|
|
cmd = [
|
|
'fswatch',
|
|
'-r',
|
|
'-v',
|
|
'-1',
|
|
'.'
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
logging.info('Updating running Sky apps...')
|
|
|
|
# Restart the app on Android. Android does not currently restart using skyx files.
|
|
cmd = [
|
|
sys.executable,
|
|
os.path.abspath(__file__),
|
|
'start',
|
|
'--poke'
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
if not args.local_build:
|
|
# Currently sending to iOS only works if you are building Sky locally
|
|
# since we aren't shipping the sky_snapshot binary yet.
|
|
continue
|
|
|
|
if tempdir is None:
|
|
tempdir = tempfile.mkdtemp()
|
|
currdir = os.getcwd()
|
|
|
|
# Build the snapshot
|
|
sky_snapshot_path = os.path.join(args.sky_src_path, args.ios_sim_debug_build_path, 'clang_x64', 'sky_snapshot')
|
|
cmd = [
|
|
sky_snapshot_path,
|
|
'--package-root=packages',
|
|
'--snapshot=' + os.path.join(tempdir, 'snapshot_blob.bin'),
|
|
os.path.join('lib', 'main.dart')
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
os.chdir(tempdir)
|
|
# Turn the snapshot into an app.skyx file
|
|
cmd = [
|
|
'zip',
|
|
'-r',
|
|
'app.skyx',
|
|
'snapshot_blob.bin',
|
|
'action',
|
|
'content',
|
|
'navigation'
|
|
]
|
|
subprocess.check_call(cmd)
|
|
os.chdir(currdir)
|
|
|
|
# Copy the app.skyx to the running simulator
|
|
simulator_app_documents_dir = IOSSimulator.get_simulator_app_documents_dir()
|
|
if simulator_app_documents_dir is not None:
|
|
cmd = [
|
|
'cp',
|
|
os.path.join(tempdir, 'app.skyx'),
|
|
simulator_app_documents_dir
|
|
]
|
|
subprocess.check_call(cmd)
|
|
|
|
# Copy the app.skyx to the attached iOS device
|
|
if IOSDevice.is_connected():
|
|
IOSDevice.copy_file(SKY_SHELL_APP_ID, os.path.join(tempdir, 'app.skyx'), 'Documents/app.skyx')
|
|
|
|
|
|
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
|
|
|
|
logging.info('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 _update_paths(self):
|
|
global ADB_PATH
|
|
if 'ANDROID_HOME' in os.environ:
|
|
android_home_dir = os.environ['ANDROID_HOME']
|
|
ADB_PATH = os.path.join(android_home_dir, 'sdk/platform-tools/adb')
|
|
|
|
def _is_valid_adb_version(self, adb_version):
|
|
# Sample output: "Android Debug Bridge version 1.0.31"
|
|
version_fields = re.search('(\d+)\.(\d+)\.(\d+)', adb_version)
|
|
if version_fields:
|
|
major_version = int(version_fields.group(1))
|
|
minor_version = int(version_fields.group(2))
|
|
patch_version = int(version_fields.group(3))
|
|
if major_version > 1:
|
|
return True
|
|
if major_version == 1 and minor_version > 0:
|
|
return True
|
|
if major_version == 1 and minor_version == 0 and patch_version >= 32:
|
|
return True
|
|
return False
|
|
else:
|
|
logging.warn('Unrecognized adb version string. Skipping version check.')
|
|
return True
|
|
|
|
def _check_for_adb(self):
|
|
try:
|
|
adb_version = subprocess.check_output([ADB_PATH, 'version'])
|
|
if self._is_valid_adb_version(adb_version):
|
|
return True
|
|
|
|
adb_path = subprocess.check_output( ['which', ADB_PATH]).rstrip()
|
|
logging.error("'%s' is too old. Need 1.0.32 or later. " \
|
|
"Try setting ANDROID_HOME." % adb_path)
|
|
return False
|
|
|
|
except OSError:
|
|
logging.error("'adb' (from the Android SDK) not in $PATH, can't continue.")
|
|
return False
|
|
return True
|
|
|
|
def _check_for_lollipop_or_later(self):
|
|
try:
|
|
# If the server is automatically restarted, then we get irrelevant
|
|
# output lines like this, which we want to ignore:
|
|
# adb server is out of date. killing..
|
|
# * daemon started successfully *
|
|
|
|
subprocess.call([ADB_PATH, 'start-server'])
|
|
sdk_version = subprocess.check_output(
|
|
[ADB_PATH, 'shell', 'getprop', 'ro.build.version.sdk']).rstrip()
|
|
# Sample output: "22"
|
|
if not sdk_version.isdigit():
|
|
logging.error("Unexpected response from getprop: '%s'." % sdk_version)
|
|
return False
|
|
|
|
if int(sdk_version) < 22:
|
|
logging.error("Version '%s' of the Android SDK is too old. " \
|
|
"Need Lollipop (22) or later. " % sdk_version)
|
|
return False
|
|
|
|
except subprocess.CalledProcessError as e:
|
|
# adb printed the error, so we print nothing.
|
|
return False
|
|
return True
|
|
|
|
def _check_for_dart(self):
|
|
try:
|
|
subprocess.check_output([DART_PATH, '--version'], stderr=subprocess.STDOUT)
|
|
except OSError:
|
|
logging.error("'dart' (from the Dart SDK) not in $PATH, can't continue.")
|
|
return False
|
|
return True
|
|
|
|
def main(self):
|
|
logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.INFO)
|
|
|
|
self._update_paths()
|
|
if not self._check_for_adb() or not self._check_for_lollipop_or_later():
|
|
sys.exit(2)
|
|
if not self._check_for_dart():
|
|
sys.exit(2)
|
|
|
|
parser = argparse.ArgumentParser(description='Sky App Runner')
|
|
parser.add_argument('--local-build', dest='local_build', action='store_true',
|
|
help='Set this if you are building Sky locally and want to use those build products. '
|
|
'When set, attempts to automaticaly determine sky-src-path if sky-src-path is '
|
|
'not set. Not normally required.')
|
|
parser.add_argument('--sky-src-path', dest='sky_src_path',
|
|
help='Path to your Sky src directory, if you are building Sky locally. '
|
|
'Ignored if local-build is not set. Not normally required.')
|
|
parser.add_argument('--android-debug-build-path', dest='android_debug_build_path',
|
|
help='Path to your Android Debug out directory, if you are building Sky locally. '
|
|
'This path is relative to sky-src-path. Not normally required.',
|
|
default='out/android_Debug/')
|
|
parser.add_argument('--ios-debug-build-path', dest='ios_debug_build_path',
|
|
help='Path to your iOS Debug out directory, if you are building Sky locally. '
|
|
'This path is relative to sky-src-path. Not normally required.',
|
|
default='out/ios_Debug/')
|
|
parser.add_argument('--ios-sim-debug-build-path', dest='ios_sim_debug_build_path',
|
|
help='Path to your iOS Simulator Debug out directory, if you are building Sky locally. '
|
|
'This path is relative to sky-src-path. Not normally required.',
|
|
default='out/ios_sim_Debug/')
|
|
|
|
subparsers = parser.add_subparsers(help='sub-command help')
|
|
|
|
for command in [StartSky(), StopSky(), StartListening(), StartTracing(), StopTracing(), IOSSimulator()]:
|
|
command.add_subparser(subparsers)
|
|
|
|
args = parser.parse_args()
|
|
if args.local_build and args.sky_src_path is None:
|
|
real_sky_path = os.path.realpath(os.path.join(PACKAGES_DIR, 'sky'))
|
|
match = re.match(r'pub.dartlang.org/sky', real_sky_path)
|
|
if match is not None:
|
|
args.local_build = False
|
|
else:
|
|
sky_src_path = os.path.dirname(
|
|
os.path.dirname(
|
|
os.path.dirname(
|
|
os.path.dirname(real_sky_path))))
|
|
if sky_src_path == '/' or sky_src_path == '':
|
|
args.local_build = False
|
|
else:
|
|
args.sky_src_path = sky_src_path
|
|
|
|
if not args.local_build:
|
|
logging.warning('Unable to detect a valid sky install. Disabling local-build flag.\n'
|
|
'The recommended way to use a local build of Sky is to add the following\n'
|
|
'to your pubspec.yaml file and then run pub get again:\n'
|
|
'dependency_overrides:\n'
|
|
' material_design_icons:\n'
|
|
' path: /path/to/sky_engine/src/sky/packages/material_design_icons\n'
|
|
' sky:\n'
|
|
' path: /path/to/sky_engine/src/sky/packages/sky\n')
|
|
|
|
pids = Pids.read_from(PID_FILE_PATH, PID_FILE_KEYS)
|
|
atexit.register(pids.write_to, PID_FILE_PATH)
|
|
exit_code = 0
|
|
try:
|
|
exit_code = args.func(args, pids)
|
|
except subprocess.CalledProcessError as e:
|
|
# Don't print a stack trace if the adb command fails.
|
|
logging.error(e)
|
|
exit_code = 2
|
|
sys.exit(exit_code)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(SkyShellRunner().main())
|