Unverified Commit c9383913 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Fix timing issues in initialization of web resident runner (#41347)

parent 56d68a90
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
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;
...@@ -71,11 +70,11 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -71,11 +70,11 @@ class ResidentWebRunner extends ResidentRunner {
bool get debuggingEnabled => isRunningDebug && device is! WebServerDevice; bool get debuggingEnabled => isRunningDebug && device is! WebServerDevice;
WebFs _webFs; WebFs _webFs;
DebugConnection _debugConnection; ConnectionResult _connectionResult;
StreamSubscription<vmservice.Event> _stdOutSub; StreamSubscription<vmservice.Event> _stdOutSub;
bool _exited = false; bool _exited = false;
vmservice.VmService get _vmService => _debugConnection.vmService; vmservice.VmService get _vmService => _connectionResult?.debugConnection?.vmService;
@override @override
bool get canHotRestart { bool get canHotRestart {
...@@ -105,7 +104,7 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -105,7 +104,7 @@ class ResidentWebRunner extends ResidentRunner {
if (_exited) { if (_exited) {
return; return;
} }
await _debugConnection?.close(); await _connectionResult?.debugConnection?.close();
await _stdOutSub?.cancel(); await _stdOutSub?.cancel();
await _webFs?.stop(); await _webFs?.stop();
await device.stopApp(null); await device.stopApp(null);
...@@ -173,12 +172,25 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -173,12 +172,25 @@ class ResidentWebRunner extends ResidentRunner {
port: debuggingOptions.port, port: debuggingOptions.port,
skipDwds: device is WebServerDevice, skipDwds: device is WebServerDevice,
); );
await device.startApp(package, mainPath: target, debuggingOptions: debuggingOptions, platformArgs: <String, Object>{ // When connecting to a browser, update the message with a seemsSlow notification
// to handle the case where we fail to connect.
if (debuggingOptions.browserLaunch) {
buildStatus.stop();
buildStatus = logger.startProgress(
'Attempting to connect to browser instance..',
timeout: const Duration(seconds: 30),
);
}
await device.startApp(package,
mainPath: target,
debuggingOptions: debuggingOptions,
platformArgs: <String, Object>{
'uri': _webFs.uri 'uri': _webFs.uri
}); },
);
if (supportsServiceProtocol) { if (supportsServiceProtocol) {
_debugConnection = await _webFs.runAndDebug(debuggingOptions); _connectionResult = await _webFs.connect(debuggingOptions);
unawaited(_debugConnection.onDone.whenComplete(exit)); unawaited(_connectionResult.debugConnection.onDone.whenComplete(exit));
} }
} catch (err, stackTrace) { } catch (err, stackTrace) {
printError(err.toString()); printError(err.toString());
...@@ -199,26 +211,28 @@ class ResidentWebRunner extends ResidentRunner { ...@@ -199,26 +211,28 @@ class ResidentWebRunner extends ResidentRunner {
Completer<DebugConnectionInfo> connectionInfoCompleter, Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter, Completer<void> appStartedCompleter,
}) async { }) async {
Uri websocketUri;
if (supportsServiceProtocol) {
// Cleanup old subscriptions. These will throw if there isn't anything // Cleanup old subscriptions. These will throw if there isn't anything
// listening, which is fine because that is what we want to ensure. // listening, which is fine because that is what we want to ensure.
try { try {
await _debugConnection?.vmService?.streamCancel('Stdout'); await _vmService.streamCancel('Stdout');
} on vmservice.RPCError { } on vmservice.RPCError {
// Ignore this specific error. // Ignore this specific error.
} }
try { try {
await _debugConnection?.vmService?.streamListen('Stdout'); await _vmService.streamListen('Stdout');
} on vmservice.RPCError { } on vmservice.RPCError {
// Ignore this specific error. // Ignore this specific error.
} }
Uri websocketUri; _stdOutSub = _vmService.onStdoutEvent.listen((vmservice.Event log) {
if (supportsServiceProtocol) {
_stdOutSub = _debugConnection.vmService.onStdoutEvent.listen((vmservice.Event log) {
final String message = utf8.decode(base64.decode(log.bytes)).trim(); final String message = utf8.decode(base64.decode(log.bytes)).trim();
printStatus(message); printStatus(message);
}); });
unawaited(_debugConnection.vmService.registerService('reloadSources', 'FlutterTools')); unawaited(_vmService.registerService('reloadSources', 'FlutterTools'));
websocketUri = Uri.parse(_debugConnection.uri); websocketUri = Uri.parse(_connectionResult.debugConnection.uri);
// Always run main after connecting because start paused doesn't work yet.
_connectionResult.appConnection.runMain();
} }
if (websocketUri != null) { if (websocketUri != null) {
printStatus('Debug service listening on $websocketUri'); printStatus('Debug service listening on $websocketUri');
......
...@@ -112,17 +112,20 @@ class WebFs { ...@@ -112,17 +112,20 @@ class WebFs {
Future<DebugConnection> _cachedExtensionFuture; Future<DebugConnection> _cachedExtensionFuture;
/// Retrieve the [DebugConnection] for the current application /// Connect and retrieve the [DebugConnection] for the current application.
Future<DebugConnection> runAndDebug(DebuggingOptions debuggingOptions) { ///
final Completer<DebugConnection> firstConnection = Completer<DebugConnection>(); /// Only calls [AppConnection.runMain] on the subsequent connections.
Future<ConnectionResult> connect(DebuggingOptions debuggingOptions) {
final Completer<ConnectionResult> firstConnection = Completer<ConnectionResult>();
_connectedApps = _dwds.connectedApps.listen((AppConnection appConnection) async { _connectedApps = _dwds.connectedApps.listen((AppConnection appConnection) async {
final DebugConnection debugConnection = debuggingOptions.browserLaunch final DebugConnection debugConnection = debuggingOptions.browserLaunch
? await _dwds.debugConnection(appConnection) ? await _dwds.debugConnection(appConnection)
: await (_cachedExtensionFuture ??= _dwds.extensionDebugConnections.stream.first); : await (_cachedExtensionFuture ??= _dwds.extensionDebugConnections.stream.first);
if (!firstConnection.isCompleted) { if (!firstConnection.isCompleted) {
firstConnection.complete(debugConnection); firstConnection.complete(ConnectionResult(appConnection, debugConnection));
} } else {
appConnection.runMain(); appConnection.runMain();
}
}); });
return firstConnection.future; return firstConnection.future;
} }
...@@ -393,6 +396,13 @@ class AssetServer { ...@@ -393,6 +396,13 @@ class AssetServer {
} }
} }
class ConnectionResult {
ConnectionResult(this.appConnection, this.debugConnection);
final AppConnection appConnection;
final DebugConnection debugConnection;
}
/// A testable interface for starting a build daemon. /// A testable interface for starting a build daemon.
class BuildDaemonCreator { class BuildDaemonCreator {
const BuildDaemonCreator(); const BuildDaemonCreator();
......
...@@ -56,7 +56,7 @@ void main() { ...@@ -56,7 +56,7 @@ void main() {
fs.file('pubspec.yaml').createSync(); fs.file('pubspec.yaml').createSync();
fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true); fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true);
when(mockWebFs.runAndDebug(any)).thenThrow(StateError('debugging not supported')); when(mockWebFs.connect(any)).thenThrow(StateError('debugging not supported'));
} }
test('Can successfully run and connect without vmservice', () => testbed.run(() async { test('Can successfully run and connect without vmservice', () => testbed.run(() async {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:async'; import 'dart:async';
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';
...@@ -31,12 +32,14 @@ void main() { ...@@ -31,12 +32,14 @@ void main() {
MockDebugConnection mockDebugConnection; MockDebugConnection mockDebugConnection;
MockVmService mockVmService; MockVmService mockVmService;
MockWebDevice mockWebDevice; MockWebDevice mockWebDevice;
MockAppConnection mockAppConnection;
setUp(() { setUp(() {
mockWebFs = MockFlutterWebFs(); mockWebFs = MockFlutterWebFs();
mockDebugConnection = MockDebugConnection(); mockDebugConnection = MockDebugConnection();
mockVmService = MockVmService(); mockVmService = MockVmService();
mockWebDevice = MockWebDevice(); mockWebDevice = MockWebDevice();
mockAppConnection = MockAppConnection();
testbed = Testbed( testbed = Testbed(
setup: () { setup: () {
residentWebRunner = ResidentWebRunner( residentWebRunner = ResidentWebRunner(
...@@ -65,8 +68,8 @@ void main() { ...@@ -65,8 +68,8 @@ void main() {
fs.file('pubspec.yaml').createSync(); fs.file('pubspec.yaml').createSync();
fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true);
fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true); fs.file(fs.path.join('web', 'index.html')).createSync(recursive: true);
when(mockWebFs.runAndDebug(any)).thenAnswer((Invocation _) async { when(mockWebFs.connect(any)).thenAnswer((Invocation _) async {
return mockDebugConnection; return ConnectionResult(mockAppConnection, mockDebugConnection);
}); });
when(mockWebFs.recompile()).thenAnswer((Invocation _) { when(mockWebFs.recompile()).thenAnswer((Invocation _) {
return Future<bool>.value(false); return Future<bool>.value(false);
...@@ -132,11 +135,36 @@ void main() { ...@@ -132,11 +135,36 @@ void main() {
)); ));
final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future; final DebugConnectionInfo debugConnectionInfo = await connectionInfoCompleter.future;
verify(mockAppConnection.runMain()).called(1);
verify(mockVmService.registerService('reloadSources', 'FlutterTools')).called(1); verify(mockVmService.registerService('reloadSources', 'FlutterTools')).called(1);
expect(bufferLogger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/')); expect(bufferLogger.statusText, contains('Debug service listening on ws://127.0.0.1/abcd/'));
expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/'); expect(debugConnectionInfo.wsUri.toString(), 'ws://127.0.0.1/abcd/');
})); }));
test('Listens to stdout streams before running main', () => testbed.run(() async {
_setupMocks();
final BufferLogger bufferLogger = logger;
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
final StreamController<Event> controller = StreamController<Event>.broadcast();
when(mockVmService.onStdoutEvent).thenAnswer((Invocation _) {
return controller.stream;
});
when(mockAppConnection.runMain()).thenAnswer((Invocation invocation) {
controller.add(Event.parse(<String, Object>{
'type': 'Event',
'kind': 'WriteEvent',
'timestamp': 1569473488296,
'bytes': base64.encode('THIS MESSAGE IS IMPORTANT'.codeUnits)
}));
});
unawaited(residentWebRunner.run(
connectionInfoCompleter: connectionInfoCompleter,
));
await connectionInfoCompleter.future;
expect(bufferLogger.statusText, contains('THIS MESSAGE IS IMPORTANT'));
}));
test('Can hot reload after attaching', () => testbed.run(() async { test('Can hot reload after attaching', () => testbed.run(() async {
_setupMocks(); _setupMocks();
final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>(); final Completer<DebugConnectionInfo> connectionInfoCompleter = Completer<DebugConnectionInfo>();
...@@ -421,4 +449,5 @@ class MockWebDevice extends Mock implements ChromeDevice {} ...@@ -421,4 +449,5 @@ class MockWebDevice extends Mock implements ChromeDevice {}
class MockBuildDaemonCreator extends Mock implements BuildDaemonCreator {} class MockBuildDaemonCreator extends Mock implements BuildDaemonCreator {}
class MockFlutterWebFs extends Mock implements WebFs {} class MockFlutterWebFs extends Mock implements WebFs {}
class MockDebugConnection extends Mock implements DebugConnection {} class MockDebugConnection extends Mock implements DebugConnection {}
class MockAppConnection extends Mock implements AppConnection {}
class MockVmService extends Mock implements VmService {} class MockVmService extends Mock implements VmService {}
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