[flutter_tools] retry sever socket setup (and port selection if port is unspecified) (#69351)
Fixes #69348
If the web development server fails to bind, then retry up to 5 times. If a port was not provided, select a new free port each time.
diff --git a/packages/flutter_tools/lib/src/isolated/devfs_web.dart b/packages/flutter_tools/lib/src/isolated/devfs_web.dart
index 2ac0cb3..d19d8e2 100644
--- a/packages/flutter_tools/lib/src/isolated/devfs_web.dart
+++ b/packages/flutter_tools/lib/src/isolated/devfs_web.dart
@@ -136,6 +136,8 @@
final Map<String, String> _modules;
final Map<String, String> _digests;
+ int get selectedPort => _httpServer.port;
+
void performRestart(List<String> modules) {
for (final String module in modules) {
// We skip computing the digest by using the hashCode of the underlying buffer.
@@ -171,113 +173,124 @@
bool testMode = false,
DwdsLauncher dwdsLauncher = Dwds.start,
}) async {
- try {
- InternetAddress address;
- if (hostname == 'any') {
- address = InternetAddress.anyIPv4;
- } else {
- address = (await InternetAddress.lookup(hostname)).first;
- }
- final HttpServer httpServer = await HttpServer.bind(address, port);
- // Allow rendering in a iframe.
- httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
-
- final PackageConfig packageConfig = await loadPackageConfigWithLogging(
- globals.fs.file(buildInfo.packagesPath),
- logger: globals.logger,
- );
- final Map<String, String> digests = <String, String>{};
- final Map<String, String> modules = <String, String>{};
- final WebAssetServer server = WebAssetServer(
- httpServer,
- packageConfig,
- address,
- modules,
- digests,
- buildInfo,
- );
- if (testMode) {
- return server;
- }
-
- // In release builds deploy a simpler proxy server.
- if (buildInfo.mode != BuildMode.debug) {
- final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer(
- entrypoint,
- fileSystem: globals.fs,
- platform: globals.platform,
- flutterRoot: Cache.flutterRoot,
- webBuildDirectory: getWebBuildDirectory(),
- basePath: server.basePath,
- );
- shelf.serveRequests(httpServer, releaseAssetServer.handle);
- return server;
- }
-
- // Return a version string for all active modules. This is populated
- // along with the `moduleProvider` update logic.
- Future<Map<String, String>> _digestProvider() async => digests;
-
- // Ensure dwds is present and provide middleware to avoid trying to
- // load the through the isolate APIs.
- final Directory directory =
- await _loadDwdsDirectory(globals.fs, globals.logger);
- final shelf.Middleware middleware =
- (FutureOr<shelf.Response> Function(shelf.Request) innerHandler) {
- return (shelf.Request request) async {
- if (request.url.path.endsWith('dwds/src/injected/client.js')) {
- final Uri uri = directory.uri.resolve('src/injected/client.js');
- final String result =
- await globals.fs.file(uri.toFilePath()).readAsString();
- return shelf.Response.ok(result, headers: <String, String>{
- HttpHeaders.contentTypeHeader: 'application/javascript'
- });
- }
- return innerHandler(request);
- };
- };
-
- logging.Logger.root.onRecord.listen((logging.LogRecord event) {
- globals.printTrace('${event.loggerName}: ${event.message}');
- });
-
- // In debug builds, spin up DWDS and the full asset server.
- final Dwds dwds = await dwdsLauncher(
- assetReader: server,
- enableDebugExtension: true,
- buildResults: const Stream<BuildResult>.empty(),
- chromeConnection: () async {
- final Chromium chromium = await chromiumLauncher.connectedInstance;
- return chromium.chromeConnection;
- },
- hostname: hostname,
- urlEncoder: urlTunneller,
- enableDebugging: true,
- useSseForDebugProxy: useSseForDebugProxy,
- useSseForDebugBackend: useSseForDebugBackend,
- serveDevTools: false,
- loadStrategy: FrontendServerRequireStrategyProvider(
- ReloadConfiguration.none, server, _digestProvider)
- .strategy,
- expressionCompiler: expressionCompiler,
- spawnDds: true);
- shelf.Pipeline pipeline = const shelf.Pipeline();
- if (enableDwds) {
- pipeline = pipeline.addMiddleware(middleware);
- pipeline = pipeline.addMiddleware(dwds.middleware);
- }
- final shelf.Handler dwdsHandler =
- pipeline.addHandler(server.handleRequest);
- final shelf.Cascade cascade =
- shelf.Cascade().add(dwds.handler).add(dwdsHandler);
- shelf.serveRequests(httpServer, cascade.handler);
- server.dwds = dwds;
- return server;
- } on SocketException catch (err) {
- throwToolExit('Failed to bind web development server:\n$err');
+ InternetAddress address;
+ if (hostname == 'any') {
+ address = InternetAddress.anyIPv4;
+ } else {
+ address = (await InternetAddress.lookup(hostname)).first;
}
- assert(false);
- return null;
+ HttpServer httpServer;
+ dynamic lastError;
+ for (int i = 0; i < 5; i += 1) {
+ try {
+ httpServer = await HttpServer.bind(address, port ?? await globals.os.findFreePort());
+ break;
+ } on SocketException catch (error) {
+ lastError = error;
+ await Future<void>.delayed(const Duration(milliseconds: 100));
+ }
+ }
+ if (httpServer == null) {
+ throwToolExit('Failed to bind web development server:\n$lastError');
+ }
+
+ // Allow rendering in a iframe.
+ httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
+
+ final PackageConfig packageConfig = await loadPackageConfigWithLogging(
+ globals.fs.file(buildInfo.packagesPath),
+ logger: globals.logger,
+ );
+ final Map<String, String> digests = <String, String>{};
+ final Map<String, String> modules = <String, String>{};
+ final WebAssetServer server = WebAssetServer(
+ httpServer,
+ packageConfig,
+ address,
+ modules,
+ digests,
+ buildInfo,
+ );
+ if (testMode) {
+ return server;
+ }
+
+ // In release builds deploy a simpler proxy server.
+ if (buildInfo.mode != BuildMode.debug) {
+ final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer(
+ entrypoint,
+ fileSystem: globals.fs,
+ platform: globals.platform,
+ flutterRoot: Cache.flutterRoot,
+ webBuildDirectory: getWebBuildDirectory(),
+ basePath: server.basePath,
+ );
+ shelf.serveRequests(httpServer, releaseAssetServer.handle);
+ return server;
+ }
+
+ // Return a version string for all active modules. This is populated
+ // along with the `moduleProvider` update logic.
+ Future<Map<String, String>> _digestProvider() async => digests;
+
+ // Ensure dwds is present and provide middleware to avoid trying to
+ // load the through the isolate APIs.
+ final Directory directory =
+ await _loadDwdsDirectory(globals.fs, globals.logger);
+ final shelf.Middleware middleware =
+ (FutureOr<shelf.Response> Function(shelf.Request) innerHandler) {
+ return (shelf.Request request) async {
+ if (request.url.path.endsWith('dwds/src/injected/client.js')) {
+ final Uri uri = directory.uri.resolve('src/injected/client.js');
+ final String result =
+ await globals.fs.file(uri.toFilePath()).readAsString();
+ return shelf.Response.ok(result, headers: <String, String>{
+ HttpHeaders.contentTypeHeader: 'application/javascript'
+ });
+ }
+ return innerHandler(request);
+ };
+ };
+
+ logging.Logger.root.onRecord.listen((logging.LogRecord event) {
+ globals.printTrace('${event.loggerName}: ${event.message}');
+ });
+
+ // In debug builds, spin up DWDS and the full asset server.
+ final Dwds dwds = await dwdsLauncher(
+ assetReader: server,
+ enableDebugExtension: true,
+ buildResults: const Stream<BuildResult>.empty(),
+ chromeConnection: () async {
+ final Chromium chromium = await chromiumLauncher.connectedInstance;
+ return chromium.chromeConnection;
+ },
+ hostname: hostname,
+ urlEncoder: urlTunneller,
+ enableDebugging: true,
+ useSseForDebugProxy: useSseForDebugProxy,
+ useSseForDebugBackend: useSseForDebugBackend,
+ serveDevTools: false,
+ loadStrategy: FrontendServerRequireStrategyProvider(
+ ReloadConfiguration.none,
+ server,
+ _digestProvider,
+ ).strategy,
+ expressionCompiler: expressionCompiler,
+ spawnDds: true,
+ );
+ shelf.Pipeline pipeline = const shelf.Pipeline();
+ if (enableDwds) {
+ pipeline = pipeline.addMiddleware(middleware);
+ pipeline = pipeline.addMiddleware(dwds.middleware);
+ }
+ final shelf.Handler dwdsHandler =
+ pipeline.addHandler(server.handleRequest);
+ final shelf.Cascade cascade =
+ shelf.Cascade().add(dwds.handler).add(dwdsHandler);
+ shelf.serveRequests(httpServer, cascade.handler);
+ server.dwds = dwds;
+ return server;
}
final BuildInfo _buildInfo;
@@ -709,7 +722,7 @@
/// server.
WebDevFS({
@required this.hostname,
- @required this.port,
+ @required int port,
@required this.packagesFilePath,
@required this.urlTunneller,
@required this.useSseForDebugProxy,
@@ -721,11 +734,10 @@
@required this.chromiumLauncher,
@required this.nullAssertions,
this.testMode = false,
- });
+ }) : _port = port;
final Uri entrypoint;
final String hostname;
- final int port;
final String packagesFilePath;
final UrlTunneller urlTunneller;
final bool useSseForDebugProxy;
@@ -736,6 +748,7 @@
final ExpressionCompiler expressionCompiler;
final ChromiumLauncher chromiumLauncher;
final bool nullAssertions;
+ final int _port;
WebAssetServer webAssetServer;
@@ -800,7 +813,7 @@
webAssetServer = await WebAssetServer.start(
chromiumLauncher,
hostname,
- port,
+ _port,
urlTunneller,
useSseForDebugProxy,
useSseForDebugBackend,
@@ -810,15 +823,16 @@
expressionCompiler,
testMode: testMode,
);
+ final int selectedPort = webAssetServer.selectedPort;
if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) {
webAssetServer.webRenderer = WebRendererMode.autoDetect;
} else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) {
- webAssetServer.webRenderer = WebRendererMode.canvaskit;
+ webAssetServer.webRenderer = WebRendererMode.canvaskit;
}
if (hostname == 'any') {
- _baseUri = Uri.http('localhost:$port', '');
+ _baseUri = Uri.http('localhost:$selectedPort', '');
} else {
- _baseUri = Uri.http('$hostname:$port', '');
+ _baseUri = Uri.http('$hostname:$selectedPort', '');
}
return _baseUri;
}
diff --git a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
index 116b28f..32f60a4 100644
--- a/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
+++ b/packages/flutter_tools/lib/src/isolated/resident_web_runner.dart
@@ -483,11 +483,6 @@
'Launching ${globals.fsUtils.getDisplayPath(target)} '
'on ${device.device.name} in $modeName mode...',
);
- final String effectiveHostname = debuggingOptions.hostname ?? 'localhost';
- final int hostPort = debuggingOptions.port == null
- ? await globals.os.findFreePort()
- : int.tryParse(debuggingOptions.port);
-
if (device.device is ChromiumDevice) {
_chromiumLauncher = (device.device as ChromiumDevice).chromeLauncher;
}
@@ -498,10 +493,11 @@
debuggingOptions.webEnableExpressionEvaluation
? WebExpressionCompiler(device.generator)
: null;
-
device.devFS = WebDevFS(
- hostname: effectiveHostname,
- port: hostPort,
+ hostname: debuggingOptions.hostname ?? 'localhost',
+ port: debuggingOptions.port != null
+ ? int.tryParse(debuggingOptions.port)
+ : null,
packagesFilePath: packagesFilePath,
urlTunneller: urlTunneller,
useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy,
diff --git a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
index 06e6421..ed77f83 100644
--- a/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
+++ b/packages/flutter_tools/test/general.shard/web/devfs_web_test.dart
@@ -699,7 +699,7 @@
contains('GENERATED'));
// served on localhost
- expect(uri, Uri.http('localhost:0', ''));
+ expect(uri.host, 'localhost');
await webDevFS.destroy();
}, overrides: <Type, Generator>{
@@ -813,7 +813,7 @@
contains('GENERATED'));
// served on localhost
- expect(uri, Uri.http('localhost:0', ''));
+ expect(uri.host, 'localhost');
await webDevFS.destroy();
}, overrides: <Type, Generator>{
@@ -859,7 +859,7 @@
final Uri uri = await webDevFS.create();
- expect(uri, Uri.http('localhost:0', ''));
+ expect(uri.host, 'localhost');
await webDevFS.destroy();
}));