| #!/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 platform |
| 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 = [ |
| os.path.join('/Applications', 'iOS Simulator.app', 'Contents', 'MacOS', 'iOS Simulator') |
| ] |
| |
| XCRUN_PATH = [ |
| os.path.join('/usr', 'bin', 'env'), |
| 'xcrun', |
| ] |
| |
| SIMCTL_PATH = XCRUN_PATH + [ |
| 'simctl', |
| ] |
| |
| PLIST_BUDDY_PATH = XCRUN_PATH + [ |
| '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 |
| try: |
| cmd = [ |
| 'which', |
| 'ios-deploy' |
| ] |
| out = subprocess.check_output(cmd) |
| match = re.search(r'ios-deploy', out) |
| cls._has_ios_deploy = match is not None |
| except subprocess.CalledProcessError: |
| cls._has_ios_deploy = False |
| 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): |
| if platform.system() != 'Darwin': |
| return False |
| 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 = SIMCTL_PATH + [ |
| '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', |
| os.path.join(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', |
| os.path.join(path, 'Info.plist') |
| ]) |
| 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 self.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(XCRUN_PATH + [ |
| '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 __init__(self): |
| self.watch_cmd = None |
| |
| 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 watch_dir(self, directory): |
| if self.watch_cmd is None: |
| name = platform.system() |
| if name == 'Linux': |
| try: |
| cmd = [ |
| 'which', |
| 'inotifywait' |
| ] |
| out = subprocess.check_output(cmd) |
| except subprocess.CalledProcessError: |
| logging.error('"listen" command is only useful if you have installed inotifywait on Linux. Run "apt-get install inotify-tools" or equivalent to install it.') |
| return False |
| |
| self.watch_cmd = [ |
| 'inotifywait', |
| '-r', |
| '-e', |
| 'modify,close_write,move,create,delete', # Only listen for events that matter, to avoid triggering constantly from the editor watching files |
| directory |
| ] |
| elif name == 'Darwin': |
| try: |
| cmd = [ |
| 'which', |
| 'fswatch' |
| ] |
| out = subprocess.check_output(cmd) |
| except subprocess.CalledProcessError: |
| logging.error('"listen" command is only useful if you have installed fswatch on Mac. Run "brew install fswatch" to install it with homebrew.') |
| return False |
| |
| self.watch_cmd = [ |
| 'fswatch', |
| '-r', |
| '-v', |
| '-1', |
| directory |
| ] |
| else: |
| logging.error('"listen" command is only available on Mac and Linux.') |
| return False |
| |
| subprocess.check_call(self.watch_cmd) |
| return True |
| |
| def run(self, args, pids): |
| tempdir = tempfile.mkdtemp() |
| currdir = os.getcwd() |
| while True: |
| # Watch filesystem for changes |
| if not self.watch_dir(currdir): |
| return |
| |
| 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 |
| |
| # 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, cannot 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, cannot 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()) |