mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge pull request #944 from iansf/no_android
Don't crash when no Android device is attached
This commit is contained in:
commit
cdbb26c627
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user