From 987d0d5f7d015179fdf7f54a0e877c1424b0e79f Mon Sep 17 00:00:00 2001 From: Ian Fischer Date: Fri, 28 Aug 2015 17:13:39 -0700 Subject: [PATCH] =?UTF-8?q?Make=20it=20so=20that=20sky=5Ftool=20doesn?= =?UTF-8?q?=E2=80=99t=20crash=20if=20an=20Android=20device=20isn=E2=80=99t?= =?UTF-8?q?=20attached.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactors a bunch of Android-related things into an AndroidDevice singleton class. --- sky/packages/sky/lib/sky_tool | 327 +++++++++++++++++++--------------- 1 file changed, 180 insertions(+), 147 deletions(-) diff --git a/sky/packages/sky/lib/sky_tool b/sky/packages/sky/lib/sky_tool index e474b4a246b..e8bb3df2c76 100755 --- a/sky/packages/sky/lib/sky_tool +++ b/sky/packages/sky/lib/sky_tool @@ -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