Close http connections on dwds and webdev_server exit (#1129)
* Close http connections on dwds and webdev_server exit
proxyHandler is creating new http client if none is passed to it,
and this client is never closed, which causes the program to hang
in some scenarios - for example, in a coming expression evaluation
feature where the expression compiler worker is running in an
isolate, communicating with webdev isolate using send/receive
ports.
- pass an http client to all proxyHandlers
- close that client on exit
- cleanup running services on context.setup fail so to the
subsequent setup retries do not fail due to services stil
running.
* Update dwds/lib/src/readers/frontend_server_asset_reader.dart
Co-authored-by: Nate Bosch <nbosch1@gmail.com>
* Update dwds/lib/src/readers/proxy_server_asset_reader.dart
Co-authored-by: Nate Bosch <nbosch1@gmail.com>
* Update dwds/test/debugger_test.dart
Co-authored-by: Nate Bosch <nbosch1@gmail.com>
* Fix compilation errors
Co-authored-by: Nate Bosch <nbosch1@gmail.com>
diff --git a/dwds/CHANGELOG.md b/dwds/CHANGELOG.md
index be26dba..16ab920 100644
--- a/dwds/CHANGELOG.md
+++ b/dwds/CHANGELOG.md
@@ -10,6 +10,7 @@
module paths.
- Fix issue where upgrading `build_web_compilers` would cause missing
module assets (JavaScript code and source maps).
+- Fix issue where open http connections prevent the process for exiting.
**Breaking changes:**
- Change `Dwds.start` to require `MetadataProvider` as a parameter.
diff --git a/dwds/lib/dwds.dart b/dwds/lib/dwds.dart
index a4d51ad..f7ca763 100644
--- a/dwds/lib/dwds.dart
+++ b/dwds/lib/dwds.dart
@@ -51,12 +51,14 @@
final Handler handler;
final DevTools _devTools;
final DevHandler _devHandler;
+ final AssetReader _assetReader;
final bool _enableDebugging;
Dwds._(
this.middleware,
this._devTools,
this._devHandler,
+ this._assetReader,
this._enableDebugging,
) : handler = _devHandler.handler;
@@ -68,6 +70,7 @@
Future<void> stop() async {
await _devTools?.close();
await _devHandler.close();
+ await _assetReader.close();
}
Future<DebugConnection> debugConnection(AppConnection appConnection) async {
@@ -161,6 +164,7 @@
injected.middleware,
devTools,
devHandler,
+ assetReader,
enableDebugging,
);
}
diff --git a/dwds/lib/src/readers/asset_reader.dart b/dwds/lib/src/readers/asset_reader.dart
index 55448cc..3b18864 100644
--- a/dwds/lib/src/readers/asset_reader.dart
+++ b/dwds/lib/src/readers/asset_reader.dart
@@ -14,4 +14,7 @@
/// Returns the contents for the merged metadata output at the provided path.
Future<String> metadataContents(String serverPath);
+
+ /// Closes connections
+ Future<void> close();
}
diff --git a/dwds/lib/src/readers/frontend_server_asset_reader.dart b/dwds/lib/src/readers/frontend_server_asset_reader.dart
index f64286f..c39bcac 100644
--- a/dwds/lib/src/readers/frontend_server_asset_reader.dart
+++ b/dwds/lib/src/readers/frontend_server_asset_reader.dart
@@ -108,4 +108,7 @@
// TODO(grouma) - Implement the merged metadata reader.
throw UnimplementedError();
}
+
+ @override
+ Future<void> close() async {}
}
diff --git a/dwds/lib/src/readers/proxy_server_asset_reader.dart b/dwds/lib/src/readers/proxy_server_asset_reader.dart
index 5cfe853..97f30a4 100644
--- a/dwds/lib/src/readers/proxy_server_asset_reader.dart
+++ b/dwds/lib/src/readers/proxy_server_asset_reader.dart
@@ -2,9 +2,11 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
+import 'dart:async';
import 'dart:convert';
import 'dart:io';
+import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:logging/logging.dart';
import 'package:shelf/shelf.dart';
@@ -18,6 +20,7 @@
final LogWriter _logWriter;
Handler _handler;
+ http.Client _client;
ProxyServerAssetReader(int assetServerPort, this._logWriter,
{String root, String host, bool isHttps}) {
@@ -25,13 +28,13 @@
root ??= '';
isHttps ??= false;
var scheme = isHttps ? 'https://' : 'http://';
- var client = isHttps
+ _client = isHttps
? IOClient(
HttpClient()..badCertificateCallback = (cert, host, port) => true)
- : null;
+ : http.Client();
var url = '$scheme$host:$assetServerPort/';
if (root?.isNotEmpty ?? false) url += '$root/';
- _handler = proxyHandler(url, client: client);
+ _handler = proxyHandler(url, client: _client);
}
@override
@@ -66,4 +69,7 @@
@override
Future<String> metadataContents(String serverPath) =>
_readResource(serverPath);
+
+ @override
+ Future<void> close() async => _client.close();
}
diff --git a/dwds/test/debugger_test.dart b/dwds/test/debugger_test.dart
index bc4bb01..e2347cb 100644
--- a/dwds/test/debugger_test.dart
+++ b/dwds/test/debugger_test.dart
@@ -55,6 +55,9 @@
'yCAAC,WAAW;IAChE;AAC0D,IAA3D,AAAS,AAAK,0DAAO;AAAe,kBAAO;;;AAEvC,gBAAQ;'
'AAGV,IAFI,kCAAqC,QAAC;AACX,MAA/B,WAAM,AAAwB,0BAAP,QAAF,AAAE,KAAK,GAAP;'
';EAEzB","file":"main.ddc.js"}';
+
+ @override
+ Future<void> close() async {}
}
void main() async {
diff --git a/dwds/test/fixtures/context.dart b/dwds/test/fixtures/context.dart
index 7576f92..a40e4e4 100644
--- a/dwds/test/fixtures/context.dart
+++ b/dwds/test/fixtures/context.dart
@@ -6,6 +6,8 @@
import 'dart:convert';
import 'dart:io';
+import 'package:http/http.dart' as http;
+import 'package:dwds/src/readers/proxy_server_asset_reader.dart';
import 'package:build_daemon/client.dart';
import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart';
@@ -51,6 +53,7 @@
AppConnection appConnection;
DebugConnection debugConnection;
WebkitDebugger webkitDebugger;
+ http.Client client;
int port;
Directory _outputDir;
File _entryFile;
@@ -114,166 +117,175 @@
logWriter ??= (Level level, String message) => printOnFailure(message);
spawnDds ??= true;
- var systemTempDir = Directory.systemTemp;
- _outputDir = systemTempDir.createTempSync('foo bar');
-
- var chromeDriverPort = await findUnusedPort();
- var chromeDriverUrlBase = 'wd/hub';
try {
- chromeDriver = await Process.start('chromedriver$_exeExt',
- ['--port=$chromeDriverPort', '--url-base=$chromeDriverUrlBase']);
- // On windows this takes a while to boot up, wait for the first line
- // of stdout as a signal that it is ready.
- await chromeDriver.stdout
- .transform(utf8.decoder)
- .transform(const LineSplitter())
- .first;
+ client = http.Client();
+
+ var systemTempDir = Directory.systemTemp;
+ _outputDir = systemTempDir.createTempSync('foo bar');
+
+ var chromeDriverPort = await findUnusedPort();
+ var chromeDriverUrlBase = 'wd/hub';
+ try {
+ chromeDriver = await Process.start('chromedriver$_exeExt',
+ ['--port=$chromeDriverPort', '--url-base=$chromeDriverUrlBase']);
+ // On windows this takes a while to boot up, wait for the first line
+ // of stdout as a signal that it is ready.
+ await chromeDriver.stdout
+ .transform(utf8.decoder)
+ .transform(const LineSplitter())
+ .first;
+ } catch (e) {
+ throw StateError(
+ 'Could not start ChromeDriver. Is it installed?\nError: $e');
+ }
+
+ await Process.run('pub$_batExt', ['upgrade'],
+ workingDirectory: workingDirectory);
+
+ ExpressionCompiler expressionCompiler;
+ AssetReader assetReader;
+ Handler assetHandler;
+ Stream<BuildResults> buildResults;
+ RequireStrategy requireStrategy;
+ MetadataProvider metadataProvider;
+
+ switch (compilationMode) {
+ case CompilationMode.buildDaemon:
+ {
+ daemonClient = await connectClient(
+ workingDirectory,
+ [],
+ (log) => logWriter(
+ server_log.toLoggingLevel(log.level), log.message));
+ daemonClient.registerBuildTarget(
+ DefaultBuildTarget((b) => b..target = pathToServe));
+ daemonClient.startBuild();
+
+ await daemonClient.buildResults
+ .firstWhere((results) => results.results
+ .any((result) => result.status == BuildStatus.succeeded))
+ .timeout(const Duration(seconds: 60));
+
+ var assetServerPort = daemonPort(workingDirectory);
+ assetHandler = proxyHandler(
+ 'http://localhost:$assetServerPort/$pathToServe/',
+ client: client);
+ assetReader = ProxyServerAssetReader(assetServerPort, logWriter,
+ root: pathToServe);
+ metadataProvider = MetadataProvider(assetReader, logWriter);
+
+ requireStrategy = BuildRunnerRequireStrategyProvider(
+ assetHandler, reloadConfiguration, metadataProvider)
+ .strategy;
+
+ buildResults = daemonClient.buildResults;
+ }
+ break;
+ case CompilationMode.frontendServer:
+ {
+ var fileSystemRoot = p.dirname(_packagesFilePath);
+ var entryPath =
+ _entryFile.path.substring(fileSystemRoot.length + 1);
+ webRunner = ResidentWebRunner(
+ '${Uri.file(entryPath)}',
+ urlEncoder,
+ fileSystemRoot,
+ _packagesFilePath,
+ [fileSystemRoot],
+ 'org-dartlang-app',
+ _outputDir.path,
+ logWriter);
+
+ var assetServerPort = await findUnusedPort();
+ await webRunner.run(hostname, assetServerPort, pathToServe);
+
+ expressionCompiler = webRunner.expressionCompiler;
+ assetReader = webRunner.devFS.assetServer;
+ assetHandler = webRunner.devFS.assetServer.handleRequest;
+
+ metadataProvider = MetadataProvider(assetReader, logWriter);
+ requireStrategy = FrontendServerRequireStrategyProvider(
+ reloadConfiguration, metadataProvider)
+ .strategy;
+
+ buildResults = const Stream<BuildResults>.empty();
+ }
+ break;
+ default:
+ throw Exception('Unsupported compilation mode: $compilationMode');
+ }
+
+ expressionCompiler = useFakeExpressionCompiler
+ ? FakeExpressionCompiler()
+ : expressionCompiler;
+
+ var debugPort = await findUnusedPort();
+ // If the environment variable DWDS_DEBUG_CHROME is set to the string true
+ // then Chrome will be launched with a UI rather than headless.
+ // If the extension is enabled, then Chrome will be launched with a UI
+ // since headless Chrome does not support extensions.
+ var headless = Platform.environment['DWDS_DEBUG_CHROME'] != 'true' &&
+ !enableDebugExtension;
+
+ var capabilities = Capabilities.chrome
+ ..addAll({
+ Capabilities.chromeOptions: {
+ 'args': [
+ 'remote-debugging-port=$debugPort',
+ if (enableDebugExtension) '--load-extension=debug_extension/web',
+ if (headless) '--headless'
+ ]
+ }
+ });
+ webDriver = await createDriver(
+ spec: WebDriverSpec.JsonWire,
+ desired: capabilities,
+ uri: Uri.parse(
+ 'http://127.0.0.1:$chromeDriverPort/$chromeDriverUrlBase/'));
+ var connection = ChromeConnection('localhost', debugPort);
+
+ port = await findUnusedPort();
+ testServer = await TestServer.start(
+ hostname,
+ port,
+ assetHandler,
+ assetReader,
+ requireStrategy,
+ metadataProvider,
+ pathToServe,
+ buildResults,
+ () async => connection,
+ serveDevTools,
+ enableDebugExtension,
+ autoRun,
+ enableDebugging,
+ useSse,
+ urlEncoder,
+ restoreBreakpoints,
+ expressionCompiler,
+ logWriter,
+ spawnDds);
+
+ appUrl = 'http://localhost:$port/$path';
+ await webDriver.get(appUrl);
+ var tab = await connection.getTab((t) => t.url == appUrl);
+ tabConnection = await tab.connect();
+ await tabConnection.runtime.enable();
+ await tabConnection.debugger.enable();
+
+ if (enableDebugExtension) {
+ var extensionTab = await _fetchDartDebugExtensionTab(connection);
+ extensionConnection = await extensionTab.connect();
+ await extensionConnection.runtime.enable();
+ }
+
+ appConnection = await testServer.dwds.connectedApps.first;
+ if (enableDebugging && !waitToDebug) {
+ await startDebugging();
+ }
} catch (e) {
- throw StateError(
- 'Could not start ChromeDriver. Is it installed?\nError: $e');
- }
-
- await Process.run('pub$_batExt', ['upgrade'],
- workingDirectory: workingDirectory);
-
- ExpressionCompiler expressionCompiler;
- AssetReader assetReader;
- Handler assetHandler;
- Stream<BuildResults> buildResults;
- RequireStrategy requireStrategy;
- MetadataProvider metadataProvider;
-
- switch (compilationMode) {
- case CompilationMode.buildDaemon:
- {
- daemonClient = await connectClient(
- workingDirectory,
- [],
- (log) =>
- logWriter(server_log.toLoggingLevel(log.level), log.message));
- daemonClient.registerBuildTarget(
- DefaultBuildTarget((b) => b..target = pathToServe));
- daemonClient.startBuild();
-
- await daemonClient.buildResults
- .firstWhere((results) => results.results
- .any((result) => result.status == BuildStatus.succeeded))
- .timeout(const Duration(seconds: 60));
-
- var assetServerPort = daemonPort(workingDirectory);
- assetHandler =
- proxyHandler('http://localhost:$assetServerPort/$pathToServe/');
- assetReader = ProxyServerAssetReader(assetServerPort, logWriter,
- root: pathToServe);
- metadataProvider = MetadataProvider(assetReader, logWriter);
-
- requireStrategy = BuildRunnerRequireStrategyProvider(
- assetHandler, reloadConfiguration, metadataProvider)
- .strategy;
-
- buildResults = daemonClient.buildResults;
- }
- break;
- case CompilationMode.frontendServer:
- {
- var fileSystemRoot = p.dirname(_packagesFilePath);
- var entryPath = _entryFile.path.substring(fileSystemRoot.length + 1);
- webRunner = ResidentWebRunner(
- '${Uri.file(entryPath)}',
- urlEncoder,
- fileSystemRoot,
- _packagesFilePath,
- [fileSystemRoot],
- 'org-dartlang-app',
- _outputDir.path,
- logWriter);
-
- var assetServerPort = await findUnusedPort();
- await webRunner.run(hostname, assetServerPort, pathToServe);
-
- expressionCompiler = webRunner.expressionCompiler;
- assetReader = webRunner.devFS.assetServer;
- assetHandler = webRunner.devFS.assetServer.handleRequest;
-
- metadataProvider = MetadataProvider(assetReader, logWriter);
- requireStrategy = FrontendServerRequireStrategyProvider(
- reloadConfiguration, metadataProvider)
- .strategy;
-
- buildResults = const Stream<BuildResults>.empty();
- }
- break;
- default:
- throw Exception('Unsupported compilation mode: $compilationMode');
- }
-
- expressionCompiler = useFakeExpressionCompiler
- ? FakeExpressionCompiler()
- : expressionCompiler;
-
- var debugPort = await findUnusedPort();
- // If the environment variable DWDS_DEBUG_CHROME is set to the string true
- // then Chrome will be launched with a UI rather than headless.
- // If the extension is enabled, then Chrome will be launched with a UI
- // since headless Chrome does not support extensions.
- var headless = Platform.environment['DWDS_DEBUG_CHROME'] != 'true' &&
- !enableDebugExtension;
-
- var capabilities = Capabilities.chrome
- ..addAll({
- Capabilities.chromeOptions: {
- 'args': [
- 'remote-debugging-port=$debugPort',
- if (enableDebugExtension) '--load-extension=debug_extension/web',
- if (headless) '--headless'
- ]
- }
- });
- webDriver = await createDriver(
- spec: WebDriverSpec.JsonWire,
- desired: capabilities,
- uri: Uri.parse(
- 'http://127.0.0.1:$chromeDriverPort/$chromeDriverUrlBase/'));
- var connection = ChromeConnection('localhost', debugPort);
-
- port = await findUnusedPort();
- testServer = await TestServer.start(
- hostname,
- port,
- assetHandler,
- assetReader,
- requireStrategy,
- metadataProvider,
- pathToServe,
- buildResults,
- () async => connection,
- serveDevTools,
- enableDebugExtension,
- autoRun,
- enableDebugging,
- useSse,
- urlEncoder,
- restoreBreakpoints,
- expressionCompiler,
- logWriter,
- spawnDds);
-
- appUrl = 'http://localhost:$port/$path';
- await webDriver.get(appUrl);
- var tab = await connection.getTab((t) => t.url == appUrl);
- tabConnection = await tab.connect();
- await tabConnection.runtime.enable();
- await tabConnection.debugger.enable();
-
- if (enableDebugExtension) {
- var extensionTab = await _fetchDartDebugExtensionTab(connection);
- extensionConnection = await extensionTab.connect();
- await extensionConnection.runtime.enable();
- }
-
- appConnection = await testServer.dwds.connectedApps.first;
- if (enableDebugging && !waitToDebug) {
- await startDebugging();
+ await tearDown();
+ rethrow;
}
}
@@ -290,7 +302,17 @@
await daemonClient?.close();
await webRunner?.stop();
await testServer?.stop();
+ client?.close();
await _outputDir?.delete(recursive: true);
+
+ // clear the state for next setup
+ webDriver = null;
+ chromeDriver = null;
+ daemonClient = null;
+ webRunner = null;
+ testServer = null;
+ client = null;
+ _outputDir = null;
}
Future<void> changeInput() async {
diff --git a/dwds/test/metadata_test.dart b/dwds/test/metadata_test.dart
index 913e8b9..76a99cb 100644
--- a/dwds/test/metadata_test.dart
+++ b/dwds/test/metadata_test.dart
@@ -23,6 +23,9 @@
@override
Future<String> sourceMapContents(String serverPath) =>
throw UnimplementedError();
+
+ @override
+ Future<void> close() async {}
}
void main() {
diff --git a/frontend_server_common/lib/src/asset_server.dart b/frontend_server_common/lib/src/asset_server.dart
index d8cbd1a..834afe2 100644
--- a/frontend_server_common/lib/src/asset_server.dart
+++ b/frontend_server_common/lib/src/asset_server.dart
@@ -133,7 +133,8 @@
}
/// Tear down the http server running.
- Future<void> dispose() {
+ @override
+ Future<void> close() {
return _httpServer.close();
}
diff --git a/frontend_server_common/lib/src/devfs.dart b/frontend_server_common/lib/src/devfs.dart
index 9858800..ac11d5a 100644
--- a/frontend_server_common/lib/src/devfs.dart
+++ b/frontend_server_common/lib/src/devfs.dart
@@ -53,7 +53,7 @@
Future<void> dispose() {
fileSystem.currentDirectory = _savedCurrentDirectory;
- return assetServer.dispose();
+ return assetServer.close();
}
Future<UpdateFSReport> update({
diff --git a/webdev/CHANGELOG.md b/webdev/CHANGELOG.md
index ae786a0..c237bba 100644
--- a/webdev/CHANGELOG.md
+++ b/webdev/CHANGELOG.md
@@ -4,6 +4,7 @@
- Expand `package:vm_service` to version `>=3.0.0 <6.0.0`.
- Require at least `build_web_compilers` version `2.12.0`.
- Depend on the latest `package:dwds`.
+- Fix issue where open http connections prevent the process for exiting.
## 2.6.2
diff --git a/webdev/lib/src/serve/webdev_server.dart b/webdev/lib/src/serve/webdev_server.dart
index 169e5fa..afaba86 100644
--- a/webdev/lib/src/serve/webdev_server.dart
+++ b/webdev/lib/src/serve/webdev_server.dart
@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io';
+import 'package:http/http.dart' as http;
import 'package:build_daemon/data/build_status.dart' as daemon;
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
@@ -34,7 +35,7 @@
class WebDevServer {
final HttpServer _server;
-
+ final http.Client _client;
final String _protocol;
final Stream<BuildResult> buildResults;
@@ -47,6 +48,7 @@
WebDevServer._(
this.target,
this._server,
+ this._client,
this._protocol,
this.buildResults,
bool autoRun, {
@@ -66,6 +68,7 @@
Future<void> stop() async {
await dwds?.stop();
await _server.close(force: true);
+ _client?.close();
}
static Future<WebDevServer> start(
@@ -94,8 +97,10 @@
});
var cascade = Cascade();
+ var client = http.Client();
var assetHandler = proxyHandler(
- 'http://localhost:${options.daemonPort}/${options.target}/');
+ 'http://localhost:${options.daemonPort}/${options.target}/',
+ client: client);
Dwds dwds;
if (options.configuration.enableInjectedClient) {
var assetReader = ProxyServerAssetReader(
@@ -149,6 +154,7 @@
return WebDevServer._(
options.target,
server,
+ client,
protocol,
filteredBuildResults,
options.configuration.autoRun,