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

[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.
parent 44d0e52d
...@@ -136,6 +136,8 @@ class WebAssetServer implements AssetReader { ...@@ -136,6 +136,8 @@ class WebAssetServer implements AssetReader {
final Map<String, String> _modules; final Map<String, String> _modules;
final Map<String, String> _digests; final Map<String, String> _digests;
int get selectedPort => _httpServer.port;
void performRestart(List<String> modules) { void performRestart(List<String> modules) {
for (final String module in modules) { for (final String module in modules) {
// We skip computing the digest by using the hashCode of the underlying buffer. // We skip computing the digest by using the hashCode of the underlying buffer.
...@@ -171,113 +173,124 @@ class WebAssetServer implements AssetReader { ...@@ -171,113 +173,124 @@ class WebAssetServer implements AssetReader {
bool testMode = false, bool testMode = false,
DwdsLauncher dwdsLauncher = Dwds.start, DwdsLauncher dwdsLauncher = Dwds.start,
}) async { }) async {
try { InternetAddress address;
InternetAddress address; if (hostname == 'any') {
if (hostname == 'any') { address = InternetAddress.anyIPv4;
address = InternetAddress.anyIPv4; } else {
} else { address = (await InternetAddress.lookup(hostname)).first;
address = (await InternetAddress.lookup(hostname)).first; }
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));
} }
final HttpServer httpServer = await HttpServer.bind(address, port); }
// Allow rendering in a iframe. if (httpServer == null) {
httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN'); throwToolExit('Failed to bind web development server:\n$lastError');
}
final PackageConfig packageConfig = await loadPackageConfigWithLogging( // Allow rendering in a iframe.
globals.fs.file(buildInfo.packagesPath), httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
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. final PackageConfig packageConfig = await loadPackageConfigWithLogging(
if (buildInfo.mode != BuildMode.debug) { globals.fs.file(buildInfo.packagesPath),
final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer( logger: globals.logger,
entrypoint, );
fileSystem: globals.fs, final Map<String, String> digests = <String, String>{};
platform: globals.platform, final Map<String, String> modules = <String, String>{};
flutterRoot: Cache.flutterRoot, final WebAssetServer server = WebAssetServer(
webBuildDirectory: getWebBuildDirectory(), httpServer,
basePath: server.basePath, packageConfig,
); address,
shelf.serveRequests(httpServer, releaseAssetServer.handle); modules,
return server; 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 // Return a version string for all active modules. This is populated
// along with the `moduleProvider` update logic. // along with the `moduleProvider` update logic.
Future<Map<String, String>> _digestProvider() async => digests; Future<Map<String, String>> _digestProvider() async => digests;
// Ensure dwds is present and provide middleware to avoid trying to // Ensure dwds is present and provide middleware to avoid trying to
// load the through the isolate APIs. // load the through the isolate APIs.
final Directory directory = final Directory directory =
await _loadDwdsDirectory(globals.fs, globals.logger); await _loadDwdsDirectory(globals.fs, globals.logger);
final shelf.Middleware middleware = final shelf.Middleware middleware =
(FutureOr<shelf.Response> Function(shelf.Request) innerHandler) { (FutureOr<shelf.Response> Function(shelf.Request) innerHandler) {
return (shelf.Request request) async { return (shelf.Request request) async {
if (request.url.path.endsWith('dwds/src/injected/client.js')) { if (request.url.path.endsWith('dwds/src/injected/client.js')) {
final Uri uri = directory.uri.resolve('src/injected/client.js'); final Uri uri = directory.uri.resolve('src/injected/client.js');
final String result = final String result =
await globals.fs.file(uri.toFilePath()).readAsString(); await globals.fs.file(uri.toFilePath()).readAsString();
return shelf.Response.ok(result, headers: <String, String>{ return shelf.Response.ok(result, headers: <String, String>{
HttpHeaders.contentTypeHeader: 'application/javascript' HttpHeaders.contentTypeHeader: 'application/javascript'
}); });
} }
return innerHandler(request); return innerHandler(request);
};
}; };
};
logging.Logger.root.onRecord.listen((logging.LogRecord event) { logging.Logger.root.onRecord.listen((logging.LogRecord event) {
globals.printTrace('${event.loggerName}: ${event.message}'); globals.printTrace('${event.loggerName}: ${event.message}');
}); });
// In debug builds, spin up DWDS and the full asset server. // In debug builds, spin up DWDS and the full asset server.
final Dwds dwds = await dwdsLauncher( final Dwds dwds = await dwdsLauncher(
assetReader: server, assetReader: server,
enableDebugExtension: true, enableDebugExtension: true,
buildResults: const Stream<BuildResult>.empty(), buildResults: const Stream<BuildResult>.empty(),
chromeConnection: () async { chromeConnection: () async {
final Chromium chromium = await chromiumLauncher.connectedInstance; final Chromium chromium = await chromiumLauncher.connectedInstance;
return chromium.chromeConnection; return chromium.chromeConnection;
}, },
hostname: hostname, hostname: hostname,
urlEncoder: urlTunneller, urlEncoder: urlTunneller,
enableDebugging: true, enableDebugging: true,
useSseForDebugProxy: useSseForDebugProxy, useSseForDebugProxy: useSseForDebugProxy,
useSseForDebugBackend: useSseForDebugBackend, useSseForDebugBackend: useSseForDebugBackend,
serveDevTools: false, serveDevTools: false,
loadStrategy: FrontendServerRequireStrategyProvider( loadStrategy: FrontendServerRequireStrategyProvider(
ReloadConfiguration.none, server, _digestProvider) ReloadConfiguration.none,
.strategy, server,
expressionCompiler: expressionCompiler, _digestProvider,
spawnDds: true); ).strategy,
shelf.Pipeline pipeline = const shelf.Pipeline(); expressionCompiler: expressionCompiler,
if (enableDwds) { spawnDds: true,
pipeline = pipeline.addMiddleware(middleware); );
pipeline = pipeline.addMiddleware(dwds.middleware); shelf.Pipeline pipeline = const shelf.Pipeline();
} if (enableDwds) {
final shelf.Handler dwdsHandler = pipeline = pipeline.addMiddleware(middleware);
pipeline.addHandler(server.handleRequest); pipeline = pipeline.addMiddleware(dwds.middleware);
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');
} }
assert(false); final shelf.Handler dwdsHandler =
return null; 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; final BuildInfo _buildInfo;
...@@ -709,7 +722,7 @@ class WebDevFS implements DevFS { ...@@ -709,7 +722,7 @@ class WebDevFS implements DevFS {
/// server. /// server.
WebDevFS({ WebDevFS({
@required this.hostname, @required this.hostname,
@required this.port, @required int port,
@required this.packagesFilePath, @required this.packagesFilePath,
@required this.urlTunneller, @required this.urlTunneller,
@required this.useSseForDebugProxy, @required this.useSseForDebugProxy,
...@@ -721,11 +734,10 @@ class WebDevFS implements DevFS { ...@@ -721,11 +734,10 @@ class WebDevFS implements DevFS {
@required this.chromiumLauncher, @required this.chromiumLauncher,
@required this.nullAssertions, @required this.nullAssertions,
this.testMode = false, this.testMode = false,
}); }) : _port = port;
final Uri entrypoint; final Uri entrypoint;
final String hostname; final String hostname;
final int port;
final String packagesFilePath; final String packagesFilePath;
final UrlTunneller urlTunneller; final UrlTunneller urlTunneller;
final bool useSseForDebugProxy; final bool useSseForDebugProxy;
...@@ -736,6 +748,7 @@ class WebDevFS implements DevFS { ...@@ -736,6 +748,7 @@ class WebDevFS implements DevFS {
final ExpressionCompiler expressionCompiler; final ExpressionCompiler expressionCompiler;
final ChromiumLauncher chromiumLauncher; final ChromiumLauncher chromiumLauncher;
final bool nullAssertions; final bool nullAssertions;
final int _port;
WebAssetServer webAssetServer; WebAssetServer webAssetServer;
...@@ -800,7 +813,7 @@ class WebDevFS implements DevFS { ...@@ -800,7 +813,7 @@ class WebDevFS implements DevFS {
webAssetServer = await WebAssetServer.start( webAssetServer = await WebAssetServer.start(
chromiumLauncher, chromiumLauncher,
hostname, hostname,
port, _port,
urlTunneller, urlTunneller,
useSseForDebugProxy, useSseForDebugProxy,
useSseForDebugBackend, useSseForDebugBackend,
...@@ -810,15 +823,16 @@ class WebDevFS implements DevFS { ...@@ -810,15 +823,16 @@ class WebDevFS implements DevFS {
expressionCompiler, expressionCompiler,
testMode: testMode, testMode: testMode,
); );
final int selectedPort = webAssetServer.selectedPort;
if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) { if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) {
webAssetServer.webRenderer = WebRendererMode.autoDetect; webAssetServer.webRenderer = WebRendererMode.autoDetect;
} else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) { } else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) {
webAssetServer.webRenderer = WebRendererMode.canvaskit; webAssetServer.webRenderer = WebRendererMode.canvaskit;
} }
if (hostname == 'any') { if (hostname == 'any') {
_baseUri = Uri.http('localhost:$port', ''); _baseUri = Uri.http('localhost:$selectedPort', '');
} else { } else {
_baseUri = Uri.http('$hostname:$port', ''); _baseUri = Uri.http('$hostname:$selectedPort', '');
} }
return _baseUri; return _baseUri;
} }
......
...@@ -483,11 +483,6 @@ class _ResidentWebRunner extends ResidentWebRunner { ...@@ -483,11 +483,6 @@ class _ResidentWebRunner extends ResidentWebRunner {
'Launching ${globals.fsUtils.getDisplayPath(target)} ' 'Launching ${globals.fsUtils.getDisplayPath(target)} '
'on ${device.device.name} in $modeName mode...', '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) { if (device.device is ChromiumDevice) {
_chromiumLauncher = (device.device as ChromiumDevice).chromeLauncher; _chromiumLauncher = (device.device as ChromiumDevice).chromeLauncher;
} }
...@@ -498,10 +493,11 @@ class _ResidentWebRunner extends ResidentWebRunner { ...@@ -498,10 +493,11 @@ class _ResidentWebRunner extends ResidentWebRunner {
debuggingOptions.webEnableExpressionEvaluation debuggingOptions.webEnableExpressionEvaluation
? WebExpressionCompiler(device.generator) ? WebExpressionCompiler(device.generator)
: null; : null;
device.devFS = WebDevFS( device.devFS = WebDevFS(
hostname: effectiveHostname, hostname: debuggingOptions.hostname ?? 'localhost',
port: hostPort, port: debuggingOptions.port != null
? int.tryParse(debuggingOptions.port)
: null,
packagesFilePath: packagesFilePath, packagesFilePath: packagesFilePath,
urlTunneller: urlTunneller, urlTunneller: urlTunneller,
useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy, useSseForDebugProxy: debuggingOptions.webUseSseForDebugProxy,
......
...@@ -699,7 +699,7 @@ void main() { ...@@ -699,7 +699,7 @@ void main() {
contains('GENERATED')); contains('GENERATED'));
// served on localhost // served on localhost
expect(uri, Uri.http('localhost:0', '')); expect(uri.host, 'localhost');
await webDevFS.destroy(); await webDevFS.destroy();
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -813,7 +813,7 @@ void main() { ...@@ -813,7 +813,7 @@ void main() {
contains('GENERATED')); contains('GENERATED'));
// served on localhost // served on localhost
expect(uri, Uri.http('localhost:0', '')); expect(uri.host, 'localhost');
await webDevFS.destroy(); await webDevFS.destroy();
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -859,7 +859,7 @@ void main() { ...@@ -859,7 +859,7 @@ void main() {
final Uri uri = await webDevFS.create(); final Uri uri = await webDevFS.create();
expect(uri, Uri.http('localhost:0', '')); expect(uri.host, 'localhost');
await webDevFS.destroy(); await webDevFS.destroy();
})); }));
......
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