[flutter_tools] Fix race condition with completer in devfs_web (#109059)

This commit is contained in:
Christopher Fujino 2022-08-09 16:57:06 -07:00 committed by GitHub
parent d823c88349
commit 017dd3e7d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 120 additions and 5 deletions

View file

@ -625,6 +625,12 @@ class ConnectionResult {
final vm_service.VmService vmService;
}
typedef VmServiceFactory = Future<vm_service.VmService> Function(
Uri, {
CompressionOptions compression,
required Logger logger,
});
/// The web specific DevFS implementation.
class WebDevFS implements DevFS {
/// Create a new [WebDevFS] instance.
@ -686,9 +692,17 @@ class WebDevFS implements DevFS {
/// Connect and retrieve the [DebugConnection] for the current application.
///
/// Only calls [AppConnection.runMain] on the subsequent connections.
Future<ConnectionResult?> connect(bool useDebugExtension) {
Future<ConnectionResult?> connect(
bool useDebugExtension, {
@visibleForTesting
VmServiceFactory vmServiceFactory = createVmServiceDelegate,
}) {
final Completer<ConnectionResult> firstConnection =
Completer<ConnectionResult>();
// Note there is an asynchronous gap between this being set to true and
// [firstConnection] completing; thus test the boolean to determine if
// the current connection is the first.
bool foundFirstConnection = false;
_connectedApps =
dwds.connectedApps.listen((AppConnection appConnection) async {
try {
@ -696,10 +710,11 @@ class WebDevFS implements DevFS {
? await (_cachedExtensionFuture ??=
dwds.extensionDebugConnections.stream.first)
: await dwds.debugConnection(appConnection);
if (firstConnection.isCompleted) {
if (foundFirstConnection) {
appConnection.runMain();
} else {
final vm_service.VmService vmService = await createVmServiceDelegate(
foundFirstConnection = true;
final vm_service.VmService vmService = await vmServiceFactory(
Uri.parse(debugConnection.uri),
logger: globals.logger,
);
@ -713,7 +728,8 @@ class WebDevFS implements DevFS {
}
}, onError: (Object error, StackTrace stackTrace) {
globals.printError(
'Unknown error while waiting for debug connection:$error\n$stackTrace');
'Unknown error while waiting for debug connection:$error\n$stackTrace',
);
if (!firstConnection.isCompleted) {
firstConnection.completeError(error, stackTrace);
}
@ -756,6 +772,7 @@ class WebDevFS implements DevFS {
nullSafetyMode,
testMode: testMode,
);
final int selectedPort = webAssetServer.selectedPort;
if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) {
webAssetServer.webRenderer = WebRendererMode.autoDetect;

View file

@ -1352,7 +1352,7 @@ class FakeWebDevFS extends Fake implements WebDevFS {
}
@override
Future<ConnectionResult> connect(bool useDebugExtension) async {
Future<ConnectionResult> connect(bool useDebugExtension, {VmServiceFactory vmServiceFactory = createVmServiceDelegate}) async {
if (exception != null) {
assert(exception is Exception || exception is Error);
// ignore: only_throw_errors, exception is either Error or Exception here.

View file

@ -4,8 +4,11 @@
// @dart = 2.8
import 'dart:async';
import 'dart:io' hide Directory, File;
import 'package:dwds/dwds.dart';
import 'package:fake_async/fake_async.dart';
import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
@ -24,6 +27,7 @@ import 'package:meta/meta.dart';
import 'package:package_config/package_config.dart';
import 'package:shelf/shelf.dart';
import 'package:test/fake.dart';
import 'package:vm_service/vm_service.dart' as vm_service;
import '../../src/common.dart';
import '../../src/testbed.dart';
@ -870,6 +874,71 @@ void main() {
Artifacts: () => Artifacts.test(),
}));
test('.connect() will never call vmServiceFactory twice', () => testbed.run(() async {
await FakeAsync().run<Future<void>>((FakeAsync time) {
final File outputFile = globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
..createSync(recursive: true);
outputFile.parent.childFile('a.sources').writeAsStringSync('');
outputFile.parent.childFile('a.json').writeAsStringSync('{}');
outputFile.parent.childFile('a.map').writeAsStringSync('{}');
outputFile.parent.childFile('a.metadata').writeAsStringSync('{}');
final WebDevFS webDevFS = WebDevFS(
// if this is any other value, we will do a real ip lookup
hostname: 'any',
port: 0,
packagesFilePath: '.packages',
urlTunneller: null,
useSseForDebugProxy: true,
useSseForDebugBackend: true,
useSseForInjectedClient: true,
nullAssertions: true,
nativeNullAssertions: true,
buildInfo: const BuildInfo(
BuildMode.debug,
'',
treeShakeIcons: false,
),
enableDwds: true,
enableDds: false,
entrypoint: Uri.base,
testMode: true,
expressionCompiler: null,
chromiumLauncher: null,
nullSafetyMode: NullSafetyMode.sound,
);
webDevFS.requireJS.createSync(recursive: true);
webDevFS.stackTraceMapper.createSync(recursive: true);
final FakeAppConnection firstConnection = FakeAppConnection();
final FakeAppConnection secondConnection = FakeAppConnection();
final Future<void> done = webDevFS.create().then<void>((Uri _) {
// In non-test mode, webDevFS.create() would have initialized DWDS
webDevFS.webAssetServer.dwds = FakeDwds(<AppConnection>[firstConnection, secondConnection]);
int vmServiceFactoryInvocationCount = 0;
Future<vm_service.VmService> vmServiceFactory(Uri uri, {CompressionOptions compression, @required Logger logger}) {
if (vmServiceFactoryInvocationCount > 0) {
fail('Called vmServiceFactory twice!');
}
vmServiceFactoryInvocationCount += 1;
return Future<vm_service.VmService>.delayed(
const Duration(seconds: 2),
() => FakeVmService(),
);
}
return webDevFS.connect(false, vmServiceFactory: vmServiceFactory).then<void>((ConnectionResult firstConnectionResult) {
return webDevFS.destroy();
});
});
time.elapse(const Duration(seconds: 1));
time.elapse(const Duration(seconds: 2));
return done;
});
}, overrides: <Type, Generator>{
Artifacts: () => Artifacts.test(),
}));
test('Can start web server with hostname any', () => testbed.run(() async {
final File outputFile = globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
..createSync(recursive: true);
@ -1140,3 +1209,32 @@ class FakeShaderCompiler implements DevelopmentShaderCompiler {
throw UnimplementedError();
}
}
class FakeDwds extends Fake implements Dwds {
FakeDwds(this.connectedAppsIterable) :
connectedApps = Stream<AppConnection>.fromIterable(connectedAppsIterable);
final Iterable<AppConnection> connectedAppsIterable;
@override
final Stream<AppConnection> connectedApps;
@override
Future<DebugConnection> debugConnection(AppConnection appConnection) => Future<DebugConnection>.value(FakeDebugConnection());
}
class FakeAppConnection extends Fake implements AppConnection {
@override
void runMain() {}
}
class FakeDebugConnection extends Fake implements DebugConnection {
FakeDebugConnection({
this.uri = 'http://foo',
});
@override
final String uri;
}
class FakeVmService extends Fake implements vm_service.VmService {}