Visitar URL original
[flutter_tools] Pause web server requests until initial compilation f… · flutter/flutter@771cb39 · GitHub
Skip to content

Commit 771cb39

Browse files
committed
[flutter_tools] Pause web server requests until initial compilation finishes
When launching an application using `flutter run -d web-server`, the HTTP server begins listening as soon as `WebDevFS.create()` starts `WebAssetServer`. However, the application compilation in `_updateDevFS()` has not yet completed. If a client requests the page before compilation finishes, DWDS attempts to read module metadata while `_webMemoryFS.mergedMetadata` is still null. DWDS then injects an empty module list into the bootstrap script and permanently memoizes that 0 modules exist, causing the page to only load `dart_sdk.js` and hang on a blank white screen. Additionally, if the client connects to DWDS before `webDevFS.connect()` starts listening, the `connectedApps` broadcast event is dropped. This adds a readiness completer to `WebAssetServer` and a Shelf middleware to pause incoming requests until initial compilation completes and the DWDS connection listener is attached. Fixes #191957
1 parent 5a70a4c commit 771cb39

6 files changed

Lines changed: 188 additions & 2 deletions

File tree

‎packages/flutter_tools/lib/src/isolated/devfs_web.dart‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,11 @@ class WebDevFS implements DevFS {
243243
return baseUri;
244244
}
245245

246+
/// Signal that the underlying web asset server is ready to handle requests.
247+
void markReady() {
248+
webAssetServer.markReady();
249+
}
250+
246251
@override
247252
Future<void> destroy() async {
248253
await webAssetServer.dispose();

‎packages/flutter_tools/lib/src/isolated/resident_web_runner.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ class ResidentWebRunner extends ResidentRunner {
367367
final Future<ConnectionResult?>? connectDebug = supportsServiceProtocol
368368
? webDevFS.connect(useDebugExtension)
369369
: null;
370+
webDevFS.markReady();
370371
await flutterDevice!.device!.startApp(
371372
package,
372373
mainPath: target,

‎packages/flutter_tools/lib/src/isolated/web_asset_server.dart‎

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ class WebAssetServer implements AssetReader {
9999
final Map<String, String> _modules;
100100
final Map<String, String> _digests;
101101

102+
final Completer<void> _readyCompleter = Completer<void>();
103+
104+
/// A future that completes when the server is ready to handle requests.
105+
Future<void> get isReady => _readyCompleter.future;
106+
107+
/// Signal that the server is ready to handle incoming requests.
108+
void markReady() {
109+
if (!_readyCompleter.isCompleted) {
110+
_readyCompleter.complete();
111+
}
112+
}
113+
102114
int get selectedPort => _httpServer.port;
103115

104116
/// Given a list of [modules] that need to be loaded, compute module names and
@@ -298,6 +310,13 @@ class WebAssetServer implements AssetReader {
298310
return server;
299311
}
300312

313+
shelf.Handler waitMiddleware(shelf.Handler innerHandler) {
314+
return (shelf.Request request) async {
315+
await server.isReady;
316+
return innerHandler(request);
317+
};
318+
}
319+
301320
// In release builds (or wasm builds) deploy a simpler proxy server.
302321
if (buildInfo.mode != BuildMode.debug || isWasm) {
303322
final releaseAssetServer = ReleaseAssetServer(
@@ -309,9 +328,12 @@ class WebAssetServer implements AssetReader {
309328
basePath: server.basePath,
310329
needsCoopCoep: crossOriginIsolation,
311330
);
331+
final shelf.Handler releaseHandler = const shelf.Pipeline()
332+
.addMiddleware(waitMiddleware)
333+
.addHandler(releaseAssetServer.handle);
312334
runZonedGuarded(
313335
() {
314-
shelf.serveRequests(httpServer!, releaseAssetServer.handle);
336+
shelf.serveRequests(httpServer!, releaseHandler);
315337
},
316338
(Object e, StackTrace s) {
317339
logger.printTrace('Release asset server: error serving requests: $e:$s');
@@ -388,9 +410,12 @@ class WebAssetServer implements AssetReader {
388410
pipeline = pipeline.addMiddleware(proxyMiddleware(proxy, globals.logger));
389411
final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest);
390412
final shelf.Cascade cascade = shelf.Cascade().add(dwds.handler).add(dwdsHandler);
413+
final shelf.Handler serverHandler = const shelf.Pipeline()
414+
.addMiddleware(waitMiddleware)
415+
.addHandler(cascade.handler);
391416
runZonedGuarded(
392417
() {
393-
shelf.serveRequests(httpServer!, cascade.handler);
418+
shelf.serveRequests(httpServer!, serverHandler);
394419
},
395420
(Object e, StackTrace s) {
396421
logger.printTrace('Dwds server: error serving requests: $e:$s');
@@ -574,6 +599,9 @@ class WebAssetServer implements AssetReader {
574599

575600
/// Tear down the http server running.
576601
Future<void> dispose() async {
602+
if (!_readyCompleter.isCompleted) {
603+
_readyCompleter.complete();
604+
}
577605
if (_dwdsInit) {
578606
await dwds.stop();
579607
}

‎packages/flutter_tools/test/general.shard/resident_web_runner_cold_test.dart‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ class FakeWebDevFS extends Fake implements WebDevFS {
231231
Future<Uri> create() async {
232232
return Uri.base;
233233
}
234+
235+
@override
236+
void markReady() {}
234237
}
235238

236239
class FakeWebDevice extends Fake implements Device {

‎packages/flutter_tools/test/general.shard/resident_web_runner_test.dart‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,26 @@ name: my_app
271271
},
272272
);
273273

274+
testUsingContext(
275+
'ResidentWebRunner marks WebDevFS as ready before starting app',
276+
() async {
277+
final ResidentRunner residentWebRunner = setUpResidentRunner(flutterDevice);
278+
fakeVmServiceHost = FakeVmServiceHost(requests: kAttachExpectations.toList());
279+
setupMocks();
280+
281+
final connectionInfoCompleter = Completer<DebugConnectionInfo>();
282+
unawaited(residentWebRunner.run(connectionInfoCompleter: connectionInfoCompleter));
283+
await connectionInfoCompleter.future;
284+
285+
expect(webDevFS.wasMarkedReady, true);
286+
},
287+
overrides: <Type, Generator>{
288+
FileSystem: () => fileSystem,
289+
ProcessManager: () => processManager,
290+
Pub: ThrowingPub.new,
291+
},
292+
);
293+
274294
testUsingContext(
275295
'Does not crash if the application exits during DDS startup',
276296
() async {
@@ -2477,6 +2497,12 @@ class FakeWebDevFS extends Fake implements WebDevFS {
24772497
late UpdateFSReport report;
24782498

24792499
Uri? mainUri;
2500+
bool wasMarkedReady = false;
2501+
2502+
@override
2503+
void markReady() {
2504+
wasMarkedReady = true;
2505+
}
24802506

24812507
@override
24822508
List<Uri> sources = <Uri>[];

‎packages/flutter_tools/test/general.shard/web/web_asset_server_test.dart‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'dart:async';
6+
57
import 'package:dwds/dwds.dart';
68
import 'package:file/memory.dart';
79
import 'package:flutter_tools/src/artifacts.dart';
@@ -10,6 +12,7 @@ import 'package:flutter_tools/src/base/io.dart';
1012
import 'package:flutter_tools/src/base/logger.dart';
1113
import 'package:flutter_tools/src/base/platform.dart';
1214
import 'package:flutter_tools/src/build_info.dart';
15+
import 'package:flutter_tools/src/convert.dart';
1316
import 'package:flutter_tools/src/isolated/release_asset_server.dart';
1417
import 'package:flutter_tools/src/isolated/web_asset_server.dart';
1518
import 'package:flutter_tools/src/web/compile.dart';
@@ -512,6 +515,126 @@ void main() {
512515
expect(server.basePath, isEmpty);
513516
});
514517

518+
testWithoutContext('isReady is completed by markReady() or dispose()', () async {
519+
final WebAssetServer server = await WebAssetServer.start(
520+
null,
521+
null,
522+
false,
523+
false,
524+
false,
525+
BuildInfo.debug,
526+
false,
527+
const DartDevelopmentServiceConfiguration(enable: false),
528+
Uri.base,
529+
null,
530+
crossOriginIsolation: false,
531+
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
532+
webRenderer: WebRendererMode.canvaskit,
533+
isWasm: false,
534+
useLocalCanvasKit: false,
535+
testMode: true,
536+
fileSystem: fileSystem,
537+
logger: BufferLogger.test(),
538+
platform: platform,
539+
);
540+
541+
var serverReady = false;
542+
unawaited(server.isReady.then((_) => serverReady = true));
543+
expect(serverReady, false);
544+
545+
server.markReady();
546+
await pumpEventQueue();
547+
expect(serverReady, true);
548+
549+
final WebAssetServer server2 = await WebAssetServer.start(
550+
null,
551+
null,
552+
false,
553+
false,
554+
false,
555+
BuildInfo.debug,
556+
false,
557+
const DartDevelopmentServiceConfiguration(enable: false),
558+
Uri.base,
559+
null,
560+
crossOriginIsolation: false,
561+
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
562+
webRenderer: WebRendererMode.canvaskit,
563+
isWasm: false,
564+
useLocalCanvasKit: false,
565+
testMode: true,
566+
fileSystem: fileSystem,
567+
logger: BufferLogger.test(),
568+
platform: platform,
569+
);
570+
571+
var server2Ready = false;
572+
unawaited(server2.isReady.then((_) => server2Ready = true));
573+
expect(server2Ready, false);
574+
575+
await server2.dispose();
576+
await pumpEventQueue();
577+
expect(server2Ready, true);
578+
});
579+
580+
testUsingContext(
581+
'pauses requests until markReady() is called',
582+
() async {
583+
fileSystem.file('build/web/index.html')
584+
..createSync(recursive: true)
585+
..writeAsStringSync('hello');
586+
587+
final WebAssetServer server = await WebAssetServer.start(
588+
null,
589+
null,
590+
false,
591+
false,
592+
false,
593+
BuildInfo.release,
594+
false,
595+
const DartDevelopmentServiceConfiguration(enable: false),
596+
Uri.base,
597+
null,
598+
crossOriginIsolation: false,
599+
webDevServerConfig: const WebDevServerConfig(host: 'localhost'),
600+
webRenderer: WebRendererMode.canvaskit,
601+
isWasm: true,
602+
useLocalCanvasKit: false,
603+
fileSystem: fileSystem,
604+
logger: BufferLogger.test(),
605+
platform: platform,
606+
);
607+
608+
final client = HttpClient();
609+
try {
610+
final Future<HttpClientResponse> responseFuture = client
611+
.getUrl(Uri.parse('http://localhost:${server.selectedPort}/'))
612+
.then((HttpClientRequest request) => request.close());
613+
614+
var responseReceived = false;
615+
unawaited(responseFuture.then((_) => responseReceived = true));
616+
617+
// Pump events to ensure request is in flight.
618+
await pumpEventQueue();
619+
expect(responseReceived, false);
620+
621+
server.markReady();
622+
623+
final HttpClientResponse response = await responseFuture;
624+
expect(response.statusCode, HttpStatus.ok);
625+
expect(await utf8.decoder.bind(response).join(), 'hello');
626+
} finally {
627+
client.close();
628+
await server.dispose();
629+
}
630+
},
631+
overrides: <Type, Generator>{
632+
FileSystem: () => fileSystem,
633+
Platform: () => platform,
634+
ProcessManager: () => FakeProcessManager.any(),
635+
},
636+
);
637+
515638
testUsingContext(
516639
'serves assets with exact key and does not serve non-source project files as assets',
517640
() async {

0 commit comments

Comments
 (0)