diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index 6c5506b5021..de14319cf0c 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -4,14 +4,17 @@ import 'dart:async'; +import 'package:dwds/dwds.dart'; import 'package:meta/meta.dart'; import 'package:vm_service/vm_service.dart' as vmservice; import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart' hide StackTrace; import '../application_package.dart'; +import '../base/async_guard.dart'; import '../base/common.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/net.dart'; import '../base/terminal.dart'; @@ -61,6 +64,10 @@ class DwdsWebRunnerFactory extends WebRunnerFactory { } } +const String kExitMessage = 'Failed to establish connection with the application ' + 'instance in Chrome.\nThis can happen if the websocket connection used by the ' + 'web tooling is unable to correctly establish a connection, for example due to a firewall.'; + /// A hot-runner which handles browser specific delegation. abstract class ResidentWebRunner extends ResidentRunner { ResidentWebRunner( @@ -393,45 +400,59 @@ class _ResidentWebRunner extends ResidentWebRunner { final int hostPort = debuggingOptions.port == null ? await globals.os.findFreePort() : int.tryParse(debuggingOptions.port); - device.devFS = WebDevFS( - hostname: effectiveHostname, - port: hostPort, - packagesFilePath: packagesFilePath, - urlTunneller: urlTunneller, - buildMode: debuggingOptions.buildInfo.mode, - enableDwds: _enableDwds, - entrypoint: globals.fs.file(target).uri, - ); - final Uri url = await device.devFS.create(); - if (debuggingOptions.buildInfo.isDebug) { - final UpdateFSReport report = await _updateDevFS(fullRestart: true); - if (!report.success) { - globals.printError('Failed to compile application.'); - return 1; - } - device.generator.accept(); - } else { - await buildWeb( - flutterProject, - target, - debuggingOptions.buildInfo, - debuggingOptions.initializePlatform, - dartDefines, - false, - ); + + try { + return await asyncGuard(() async { + device.devFS = WebDevFS( + hostname: effectiveHostname, + port: hostPort, + packagesFilePath: packagesFilePath, + urlTunneller: urlTunneller, + buildMode: debuggingOptions.buildInfo.mode, + enableDwds: _enableDwds, + entrypoint: globals.fs.file(target).uri, + ); + final Uri url = await device.devFS.create(); + if (debuggingOptions.buildInfo.isDebug) { + final UpdateFSReport report = await _updateDevFS(fullRestart: true); + if (!report.success) { + globals.printError('Failed to compile application.'); + return 1; + } + device.generator.accept(); + } else { + await buildWeb( + flutterProject, + target, + debuggingOptions.buildInfo, + debuggingOptions.initializePlatform, + dartDefines, + false, + ); + } + await device.device.startApp( + package, + mainPath: target, + debuggingOptions: debuggingOptions, + platformArgs: { + 'uri': url.toString(), + }, + ); + return attach( + connectionInfoCompleter: connectionInfoCompleter, + appStartedCompleter: appStartedCompleter, + ); + }); + } on WebSocketException { + throwToolExit(kExitMessage); + } on ChromeDebugException { + throwToolExit(kExitMessage); + } on AppConnectionException { + throwToolExit(kExitMessage); + } on SocketException { + throwToolExit(kExitMessage); } - await device.device.startApp( - package, - mainPath: target, - debuggingOptions: debuggingOptions, - platformArgs: { - 'uri': url.toString(), - }, - ); - return attach( - connectionInfoCompleter: connectionInfoCompleter, - appStartedCompleter: appStartedCompleter, - ); + return 0; } @override @@ -479,22 +500,25 @@ class _ResidentWebRunner extends ResidentWebRunner { } } + Duration transferMarker; try { if (!deviceIsDebuggable) { globals.printStatus('Recompile complete. Page requires refresh.'); - } else if (fullRestart || !debuggingOptions.buildInfo.isDebug) { + } else if (!debuggingOptions.buildInfo.isDebug) { // On non-debug builds, a hard refresh is required to ensure the // up to date sources are loaded. await _wipConnection?.sendCommand('Page.reload', { 'ignoreCache': !debuggingOptions.buildInfo.isDebug, }); } else { - await _wipConnection?.debugger - ?.sendCommand('Runtime.evaluate', params: { - 'expression': 'window.\$hotReloadHook([$reloadModules])', - 'awaitPromise': true, - 'returnByValue': true, - }); + transferMarker = timer.elapsed; + await _wipConnection?.debugger?.sendCommand( + 'Runtime.evaluate', params: { + 'expression': 'window.\$hotReloadHook([$reloadModules])', + 'awaitPromise': true, + 'returnByValue': true, + }, + ); } } on WipError catch (err) { globals.printError(err.toString()); @@ -503,8 +527,8 @@ class _ResidentWebRunner extends ResidentWebRunner { status.stop(); } - final String verb = fullRestart ? 'Restarted' : 'Reloaded'; - globals.printStatus('$verb application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + final String elapsed = getElapsedAsMilliseconds(timer.elapsed); + globals.printStatus('Restarted application in $elapsed.'); // Don't track restart times for dart2js builds or web-server devices. if (debuggingOptions.buildInfo.isDebug && deviceIsDebuggable) { @@ -517,6 +541,7 @@ class _ResidentWebRunner extends ResidentWebRunner { fullRestart: true, reason: reason, overallTimeInMs: timer.elapsed.inMilliseconds, + transferTimeInMs: timer.elapsed.inMilliseconds - transferMarker.inMilliseconds ).send(); } return OperationResult.ok; diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index 4fb7fce0bd4..b153a3642d1 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -7,6 +7,7 @@ import 'dart:convert'; import 'package:dwds/dwds.dart'; import 'package:flutter_tools/src/base/common.dart'; +import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/terminal.dart'; import 'package:flutter_tools/src/build_info.dart'; @@ -316,7 +317,7 @@ void main() { final OperationResult result = await residentWebRunner.restart(fullRestart: false); - expect(testLogger.statusText, contains('Reloaded application in')); + expect(testLogger.statusText, contains('Restarted application in')); expect(result.code, 0); verify(mockResidentCompiler.accept()).called(2); // ensure that analytics are sent. @@ -836,6 +837,112 @@ void main() { }, overrides: { Logger: () => DelegateLogger(MockLogger()) })); + + test('Successfully turns WebSocketException into ToolExit', () => testbed.run(() async { + _setupMocks(); + final Completer connectionInfoCompleter = Completer(); + final Completer unhandledErrorCompleter = Completer(); + when(mockWebDevFS.connect(any)).thenAnswer((Invocation _) async { + unawaited(unhandledErrorCompleter.future.then((void value) { + throw const WebSocketException(); + })); + return ConnectionResult(mockAppConnection, mockDebugConnection); + }); + + final Future expectation = expectLater(() => residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + ), throwsToolExit()); + + unhandledErrorCompleter.complete(); + await expectation; + })); + + test('Successfully turns AppConnectionException into ToolExit', () => testbed.run(() async { + _setupMocks(); + final Completer connectionInfoCompleter = Completer(); + final Completer unhandledErrorCompleter = Completer(); + when(mockWebDevFS.connect(any)).thenAnswer((Invocation _) async { + unawaited(unhandledErrorCompleter.future.then((void value) { + throw AppConnectionException('Could not connect to application with appInstanceId: c0ae0750-ee91-11e9-cea6-35d95a968356'); + })); + return ConnectionResult(mockAppConnection, mockDebugConnection); + }); + + final Future expectation = expectLater(() => residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + ), throwsToolExit()); + + unhandledErrorCompleter.complete(); + await expectation; + })); + + test('Successfully turns ChromeDebugError into ToolExit', () => testbed.run(() async { + _setupMocks(); + final Completer connectionInfoCompleter = Completer(); + final Completer unhandledErrorCompleter = Completer(); + when(mockWebDevFS.connect(any)).thenAnswer((Invocation _) async { + unawaited(unhandledErrorCompleter.future.then((void value) { + throw ChromeDebugException({}); + })); + return ConnectionResult(mockAppConnection, mockDebugConnection); + }); + + final Future expectation = expectLater(() => residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + ), throwsToolExit()); + + unhandledErrorCompleter.complete(); + await expectation; + })); + + test('Rethrows Exception type', () => testbed.run(() async { + _setupMocks(); + final Completer connectionInfoCompleter = Completer(); + final Completer unhandledErrorCompleter = Completer(); + when(mockWebDevFS.connect(any)).thenAnswer((Invocation _) async { + unawaited(unhandledErrorCompleter.future.then((void value) { + throw Exception('Something went wrong'); + })); + return ConnectionResult(mockAppConnection, mockDebugConnection); + }); + + final Future expectation = expectLater(() => residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + ), throwsException); + + unhandledErrorCompleter.complete(); + await expectation; + })); + test('Rethrows unknown exception type from web tooling', () => testbed.run(() async { + _setupMocks(); + final DelegateLogger delegateLogger = globals.logger as DelegateLogger; + final MockStatus mockStatus = MockStatus(); + delegateLogger.status = mockStatus; + final Completer connectionInfoCompleter = Completer(); + final Completer unhandledErrorCompleter = Completer(); + when(mockWebDevFS.connect(any)).thenAnswer((Invocation _) async { + unawaited(unhandledErrorCompleter.future.then((void value) { + throw StateError('Something went wrong'); + })); + return ConnectionResult(mockAppConnection, mockDebugConnection); + }); + + final Future expectation = expectLater(() => residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + ), throwsStateError); + + unhandledErrorCompleter.complete(); + await expectation; + verify(mockStatus.stop()).called(1); + }, overrides: { + Logger: () => DelegateLogger(BufferLogger( + terminal: AnsiTerminal( + stdio: null, + platform: const LocalPlatform(), + ), + outputPreferences: OutputPreferences.test(), + )) + })); } class MockChromeLauncher extends Mock implements ChromeLauncher {}