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

This commit is contained in:
Jonah Williams 2020-02-18 11:27:14 -08:00 committed by GitHub
parent dfcf9beb6b
commit 57acc68740
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 180 additions and 48 deletions

View file

@ -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: <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(
package,
mainPath: target,
debuggingOptions: debuggingOptions,
platformArgs: <String, Object>{
'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', <String, Object>{
'ignoreCache': !debuggingOptions.buildInfo.isDebug,
});
} else {
await _wipConnection?.debugger
?.sendCommand('Runtime.evaluate', params: <String, Object>{
'expression': 'window.\$hotReloadHook([$reloadModules])',
'awaitPromise': true,
'returnByValue': true,
});
transferMarker = timer.elapsed;
await _wipConnection?.debugger?.sendCommand(
'Runtime.evaluate', params: <String, Object>{
'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;

View file

@ -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: <Type, Generator>{
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 {}