Commit 576f51d4 authored by Ian Fischer's avatar Ian Fischer

Make it so that sky_tool doesn’t crash if an Android device isn’t attached.

Refactors a bunch of Android-related things into an AndroidDevice singleton class.
parent 4f1b4e3e
......@@ -157,26 +157,12 @@ class StartSky(object):
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]
logging.info(' '.join(pm_path_cmd))
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 _get_device_apk_sha1(self, apk_path):
# We might need to install a new APK, so check SHA1
cmd = [ADB_PATH, 'shell', 'cat', SHA1_PATH]
logging.info(' '.join(cmd))
return subprocess.check_output(cmd)
def run(self, args, pids):
if not args.poke:
StopSky().run(args, pids)
android = AndroidDevice()
project_or_path = os.path.abspath(args.project_or_path)
if args.android_build_available and args.use_release:
......@@ -185,7 +171,6 @@ class StartSky(object):
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)
source_sha1 = hashlib.sha1(open(apk_path, 'rb').read()).hexdigest()
if os.path.isdir(project_or_path):
sky_server_root = project_or_path
......@@ -207,33 +192,21 @@ class StartSky(object):
logging.error('%s is not a valid packages path.' % package_root)
return 2
if not self._is_package_installed(ANDROID_PACKAGE):
if not android.is_package_installed(ANDROID_PACKAGE):
logging.info('%s is not on the device. Installing now...' % APK_NAME)
args.install = True
elif self._get_device_apk_sha1(apk_path) != source_sha1:
elif android.get_device_apk_sha1(apk_path) != android.get_source_sha1(apk_path):
logging.info('%s on the device is out of date. 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 not os.path.exists(apk_path):
logging.error('"%s" does not exist.' % apk_path)
return 2
cmd = [ADB_PATH, 'install', '-r', apk_path]
logging.info(' '.join(cmd))
subprocess.check_call(cmd)
# record the SHA1 of the APK we just pushed
with tempfile.NamedTemporaryFile() as fp:
fp.write(source_sha1)
fp.seek(0)
cmd = [ADB_PATH, 'push', fp.name, SHA1_PATH]
logging.info(' '.join(cmd))
subprocess.check_call(cmd)
# Install on connected Android device
if android.is_connected() and args.android_build_available:
if args.use_release:
apk_path = os.path.join(args.sky_src_path, args.android_release_build_path, 'apks', APK_NAME)
else:
apk_path = os.path.join(args.sky_src_path, args.android_debug_build_path, 'apks', APK_NAME)
android.install_apk(apk_path)
# Install on connected iOS device
if IOSDevice.is_connected() and args.ios_build_available:
......@@ -251,10 +224,171 @@ class StartSky(object):
app_path = os.path.join(args.sky_src_path, args.ios_sim_debug_build_path, IOS_APP_NAME)
IOSSimulator.fork_install_app(app_path)
# TODO(iansf): fix this so that we don't have to pass sky_server_root, main_dart, pid, and args.
android.setup_servers(sky_server_root, main_dart, pids, args)
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:
logging.info(' '.join(args))
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([AndroidDevice().adb_path, 'reverse', '--remove', port_string])
self._run([AndroidDevice().adb_path, 'shell', 'am', 'force-stop', ANDROID_PACKAGE])
pids.clear()
class AndroidDevice(object):
# _state used in this manner gives a simple way to treat AndroidDevice
# as a singleton while easily allowing subclassing for mocks. All
# AndroidDevices created in a given session will share the same state.
_state = {}
def __init__(self):
self.__dict__ = AndroidDevice._state
self._update_paths()
# Checking for lollipop only needs to be done if we are starting an
# app, but it has an important side effect, which is to discard any
# progress messages if the adb server is restarted.
self._check_for_adb()
self._check_for_lollipop_or_later()
def _update_paths(self):
if 'adb_path' in self.__dict__:
return
if 'ANDROID_HOME' in os.environ:
android_home_dir = os.environ['ANDROID_HOME']
self.adb_path = os.path.join(android_home_dir, 'sdk', 'platform-tools', 'adb')
else:
self.adb_path = ADB_PATH
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):
if 'has_valid_adb' in self.__dict__:
return
try:
cmd = [self.adb_path, 'version']
logging.info(' '.join(cmd))
adb_version = subprocess.check_output(cmd)
if self._is_valid_adb_version(adb_version):
self.has_valid_adb = True
return
cmd = ['which', ADB_PATH]
logging.info(' '.join(cmd))
adb_path = subprocess.check_output(cmd).rstrip()
logging.error('"%s" is too old. Need 1.0.32 or later. '
'Try setting ANDROID_HOME to use Android builds. Android builds are unavailable.' % adb_path)
self.has_valid_adb = False
except OSError:
logging.warning('"adb" (from the Android SDK) not in $PATH, Android builds are unavailable.')
self.has_valid_adb = False
def _check_for_lollipop_or_later(self):
if 'has_valid_android' in self.__dict__:
return
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 *
cmd = [self.adb_path, 'start-server']
logging.info(' '.join(cmd))
subprocess.call(cmd)
cmd = [self.adb_path, 'shell', 'getprop', 'ro.build.version.sdk']
logging.info(' '.join(cmd))
sdk_version = subprocess.check_output(cmd).rstrip()
# Sample output: '22'
if not sdk_version.isdigit():
logging.error('Unexpected response from getprop: "%s".' % sdk_version)
self.has_valid_android = False
return
if int(sdk_version) < 22:
logging.error('Version "%s" of the Android SDK is too old. '
'Need Lollipop (22) or later. ' % sdk_version)
self.has_valid_android = False
return
except subprocess.CalledProcessError as e:
# adb printed the error, so we print nothing.
self.has_valid_android = False
return
self.has_valid_android = True
def is_package_installed(self, package_name):
if not self.is_connected():
return False
pm_path_cmd = [self.adb_path, 'shell', 'pm', 'path', package_name]
logging.info(' '.join(pm_path_cmd))
return subprocess.check_output(pm_path_cmd).strip() != ''
def get_device_apk_sha1(self, apk_path):
# We might need to install a new APK, so check SHA1
cmd = [self.adb_path, 'shell', 'cat', SHA1_PATH]
logging.info(' '.join(cmd))
return subprocess.check_output(cmd)
def get_source_sha1(self, apk_path):
return hashlib.sha1(open(apk_path, 'rb').read()).hexdigest()
def is_connected(self):
return self.has_valid_android
def install_apk(self, apk_path):
if not os.path.exists(apk_path):
logging.error('"%s" does not exist.' % apk_path)
return
cmd = [self.adb_path, 'install', '-r', apk_path]
logging.info(' '.join(cmd))
subprocess.check_call(cmd)
# record the SHA1 of the APK we just pushed
with tempfile.NamedTemporaryFile() as fp:
fp.write(self.get_source_sha1(apk_path))
fp.seek(0)
cmd = [self.adb_path, 'push', fp.name, SHA1_PATH]
logging.info(' '.join(cmd))
subprocess.check_call(cmd)
# TODO(iansf): refactor setup_servers
def setup_servers(self, sky_server_root, main_dart, pids, args):
if not self.is_connected():
return
# Set up port forwarding for observatory
observatory_port_string = 'tcp:%s' % OBSERVATORY_PORT
cmd = [
ADB_PATH,
self.adb_path,
'forward',
observatory_port_string,
observatory_port_string
......@@ -274,7 +408,7 @@ class StartSky(object):
port_string = 'tcp:%s' % sky_server_port
cmd = [
ADB_PATH,
self.adb_path,
'reverse',
port_string,
port_string
......@@ -290,42 +424,20 @@ class StartSky(object):
url += '?rand=%s' % random.random()
cmd = [
ADB_PATH, 'shell',
self.adb_path, 'shell',
'am', 'start',
'-a', 'android.intent.action.VIEW',
'-d', url,
]
if args.checked:
cmd += [ '--ez', 'enable-checked-mode', 'true' ]
cmd += ['--ez', 'enable-checked-mode', 'true']
cmd += [ ANDROID_COMPONENT ]
cmd += [ANDROID_COMPONENT]
logging.info(' '.join(cmd))
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:
logging.info(' '.join(args))
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
......@@ -810,78 +922,6 @@ class StopTracing(object):
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:
cmd = [ADB_PATH, 'version']
logging.info(' '.join(cmd))
adb_version = subprocess.check_output(cmd)
if self._is_valid_adb_version(adb_version):
return True
cmd = ['which', ADB_PATH]
logging.info(' '.join(cmd))
adb_path = subprocess.check_output(cmd).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 *
cmd = [ADB_PATH, 'start-server']
logging.info(' '.join(cmd))
subprocess.call(cmd)
cmd = [ADB_PATH, 'shell', 'getprop', 'ro.build.version.sdk']
logging.info(' '.join(cmd))
sdk_version = subprocess.check_output(cmd).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:
cmd = [DART_PATH, '--version']
......@@ -895,16 +935,6 @@ class SkyShellRunner(object):
def main(self):
logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.WARNING)
self._update_paths()
# Checking for lollipop only needs to be done if we are starting an
# app, but it has an important side effect, which is to discard any
# progress messages if the adb server is restarted.
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('--verbose', dest='verbose', action='store_true',
help='Noisy logging, including all shell commands executed')
......@@ -1008,6 +1038,9 @@ class SkyShellRunner(object):
if os.path.isdir(os.path.join(args.sky_src_path, args.ios_sim_debug_build_path)):
args.ios_sim_build_available = True
if not self._check_for_dart():
sys.exit(2)
pids = Pids.read_from(PID_FILE_PATH, PID_FILE_KEYS)
atexit.register(pids.write_to, PID_FILE_PATH)
exit_code = 0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment