Unverified Commit 57acc687 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

[flutter_tools] ensure zoned errors are caught in new web runner (#50895)

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