Print message when HttpException is thrown after running `flutter run` (#37440)
diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 88ab5dc..cd618a3 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart
@@ -512,6 +512,23 @@ } } +// Issue: https://github.com/flutter/flutter/issues/33050 +// Matches the following patterns: +// HttpException: Connection closed before full header was received, uri = * +// HttpException: , uri = * +final RegExp kAndroidQHttpConnectionClosedExp = RegExp(r'^HttpException\:.+\, uri \=.+$'); + +/// Returns `true` if any of the devices is running Android Q. +Future<bool> hasDeviceRunningAndroidQ(List<FlutterDevice> flutterDevices) async { + for (FlutterDevice flutterDevice in flutterDevices) { + final String sdkNameAndVersion = await flutterDevice.device.sdkNameAndVersion; + if (sdkNameAndVersion != null && sdkNameAndVersion.startsWith('Android 10')) { + return true; + } + } + return false; +} + // Shared code between different resident application runners. abstract class ResidentRunner { ResidentRunner(
diff --git a/packages/flutter_tools/lib/src/run_cold.dart b/packages/flutter_tools/lib/src/run_cold.dart index 6a79afc..12b494c 100644 --- a/packages/flutter_tools/lib/src/run_cold.dart +++ b/packages/flutter_tools/lib/src/run_cold.dart
@@ -131,6 +131,13 @@ await connectToServiceProtocol(); } catch (error) { printError('Error connecting to the service protocol: $error'); + // https://github.com/flutter/flutter/issues/33050 + // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed. + if (await hasDeviceRunningAndroidQ(flutterDevices) && + error.toString().contains(kAndroidQHttpConnectionClosedExp)) { + printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.'); + printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318'); + } return 2; } for (FlutterDevice device in flutterDevices) {
diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 7a8105d..4b09b2b 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart
@@ -155,6 +155,13 @@ ); } catch (error) { printError('Error connecting to the service protocol: $error'); + // https://github.com/flutter/flutter/issues/33050 + // TODO(blasten): Remove this check once https://issuetracker.google.com/issues/132325318 has been fixed. + if (await hasDeviceRunningAndroidQ(flutterDevices) && + error.toString().contains(kAndroidQHttpConnectionClosedExp)) { + printStatus('🔨 If you are using an emulator running Android Q Beta, consider using an emulator running API level 29 or lower.'); + printStatus('Learn more about the status of this issue on https://issuetracker.google.com/issues/132325318.'); + } return 2; }
diff --git a/packages/flutter_tools/test/general.shard/cold_test.dart b/packages/flutter_tools/test/general.shard/cold_test.dart new file mode 100644 index 0000000..93f8b2f --- /dev/null +++ b/packages/flutter_tools/test/general.shard/cold_test.dart
@@ -0,0 +1,114 @@ +// Copyright 2019 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 'dart:async'; + +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/compile.dart'; +import 'package:flutter_tools/src/device.dart'; +import 'package:flutter_tools/src/resident_runner.dart'; +import 'package:flutter_tools/src/run_cold.dart'; +import 'package:flutter_tools/src/vmservice.dart'; +import 'package:meta/meta.dart'; +import 'package:mockito/mockito.dart'; + +import '../src/common.dart'; +import '../src/context.dart'; +import '../src/mocks.dart'; + +void main() { + group('cold attach', () { + MockResidentCompiler residentCompiler; + BufferLogger mockLogger; + + setUp(() { + mockLogger = BufferLogger(); + residentCompiler = MockResidentCompiler(); + }); + + testUsingContext('Prints message when HttpException is thrown - 1', () async { + final MockDevice mockDevice = MockDevice(); + when(mockDevice.supportsHotReload).thenReturn(true); + when(mockDevice.supportsHotRestart).thenReturn(false); + when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10'); + + final List<FlutterDevice> devices = <FlutterDevice>[ + TestFlutterDevice( + device: mockDevice, + generator: residentCompiler, + exception: const HttpException('Connection closed before full header was received, ' + 'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws') + ), + ]; + + final int exitCode = await ColdRunner(devices, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ).attach(); + expect(exitCode, 2); + expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, ' + 'consider using an emulator running API level 29 or lower.')); + expect(mockLogger.statusText, contains('Learn more about the status of this issue on ' + 'https://issuetracker.google.com/issues/132325318')); + }, overrides: <Type, Generator>{ + Logger: () => mockLogger, + }); + + testUsingContext('Prints message when HttpException is thrown - 2', () async { + final MockDevice mockDevice = MockDevice(); + when(mockDevice.supportsHotReload).thenReturn(true); + when(mockDevice.supportsHotRestart).thenReturn(false); + when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10'); + + final List<FlutterDevice> devices = <FlutterDevice>[ + TestFlutterDevice( + device: mockDevice, + generator: residentCompiler, + exception: const HttpException(', uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws') + ), + ]; + + final int exitCode = await ColdRunner(devices, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ).attach(); + expect(exitCode, 2); + expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, ' + 'consider using an emulator running API level 29 or lower.')); + expect(mockLogger.statusText, contains('Learn more about the status of this issue on ' + 'https://issuetracker.google.com/issues/132325318')); + }, overrides: <Type, Generator>{ + Logger: () => mockLogger, + }); + }); +} + +class MockDevice extends Mock implements Device { + MockDevice() { + when(isSupported()).thenReturn(true); + } +} + +class TestFlutterDevice extends FlutterDevice { + TestFlutterDevice({ + @required Device device, + @required this.exception, + @required ResidentCompiler generator + }) : assert(exception != null), + super(device, buildMode: BuildMode.debug, generator: generator, trackWidgetCreation: false); + + /// The exception to throw when the connect method is called. + final Exception exception; + + @override + Future<void> connect({ + ReloadSources reloadSources, + Restart restart, + CompileExpression compileExpression, + }) async { + throw exception; + } +}
diff --git a/packages/flutter_tools/test/general.shard/hot_test.dart b/packages/flutter_tools/test/general.shard/hot_test.dart index 304fcf7..2853c19 100644 --- a/packages/flutter_tools/test/general.shard/hot_test.dart +++ b/packages/flutter_tools/test/general.shard/hot_test.dart
@@ -5,11 +5,15 @@ import 'dart:async'; import 'package:flutter_tools/src/artifacts.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/build_info.dart'; +import 'package:flutter_tools/src/compile.dart'; import 'package:flutter_tools/src/devfs.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/resident_runner.dart'; import 'package:flutter_tools/src/run_hot.dart'; +import 'package:flutter_tools/src/vmservice.dart'; import 'package:meta/meta.dart'; import 'package:mockito/mockito.dart'; @@ -262,6 +266,77 @@ }); }); }); + + group('hot attach', () { + MockResidentCompiler residentCompiler = MockResidentCompiler(); + BufferLogger mockLogger; + MockLocalEngineArtifacts mockArtifacts; + + setUp(() { + residentCompiler = MockResidentCompiler(); + mockLogger = BufferLogger(); + mockArtifacts = MockLocalEngineArtifacts(); + }); + + testUsingContext('Prints message when HttpException is thrown - 1', () async { + final MockDevice mockDevice = MockDevice(); + when(mockDevice.supportsHotReload).thenReturn(true); + when(mockDevice.supportsHotRestart).thenReturn(false); + when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10'); + + final List<FlutterDevice> devices = <FlutterDevice>[ + TestFlutterDevice( + device: mockDevice, + generator: residentCompiler, + exception: const HttpException('Connection closed before full header was received, ' + 'uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws') + ), + ]; + + final int exitCode = await HotRunner(devices, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ).attach(); + expect(exitCode, 2); + expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, ' + 'consider using an emulator running API level 29 or lower.')); + expect(mockLogger.statusText, contains('Learn more about the status of this issue on ' + 'https://issuetracker.google.com/issues/132325318')); + }, overrides: <Type, Generator>{ + Artifacts: () => mockArtifacts, + Logger: () => mockLogger, + HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true), + }); + + testUsingContext('Prints message when HttpException is thrown - 2', () async { + final MockDevice mockDevice = MockDevice(); + when(mockDevice.supportsHotReload).thenReturn(true); + when(mockDevice.supportsHotRestart).thenReturn(false); + when(mockDevice.targetPlatform).thenAnswer((Invocation _) async => TargetPlatform.tester); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation _) async => 'Android 10'); + + final List<FlutterDevice> devices = <FlutterDevice>[ + TestFlutterDevice( + device: mockDevice, + generator: residentCompiler, + exception: const HttpException(', uri = http://127.0.0.1:63394/5ZmLv8A59xY=/ws') + ), + ]; + + final int exitCode = await HotRunner(devices, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + ).attach(); + expect(exitCode, 2); + expect(mockLogger.statusText, contains('If you are using an emulator running Android Q Beta, ' + 'consider using an emulator running API level 29 or lower.')); + expect(mockLogger.statusText, contains('Learn more about the status of this issue on ' + 'https://issuetracker.google.com/issues/132325318')); + }, overrides: <Type, Generator>{ + Artifacts: () => mockArtifacts, + Logger: () => mockLogger, + HotRunnerConfig: () => TestHotRunnerConfig(successfulSetup: true), + }); + }); } class MockDevFs extends Mock implements DevFS {} @@ -274,6 +349,27 @@ } } +class TestFlutterDevice extends FlutterDevice { + TestFlutterDevice({ + @required Device device, + @required this.exception, + @required ResidentCompiler generator + }) : assert(exception != null), + super(device, buildMode: BuildMode.debug, generator: generator, trackWidgetCreation: false); + + /// The exception to throw when the connect method is called. + final Exception exception; + + @override + Future<void> connect({ + ReloadSources reloadSources, + Restart restart, + CompileExpression compileExpression, + }) async { + throw exception; + } +} + class TestHotRunnerConfig extends HotRunnerConfig { TestHotRunnerConfig({@required this.successfulSetup}); bool successfulSetup;