diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index af710d68b07..dfa4f78807c 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -73,6 +73,7 @@ class ResidentWebRunner extends ResidentRunner { WebFs _webFs; DebugConnection _debugConnection; StreamSubscription _stdOutSub; + bool _exited = false; vmservice.VmService get _vmService => _debugConnection.vmService; @@ -101,9 +102,13 @@ class ResidentWebRunner extends ResidentRunner { } Future _cleanup() async { + if (_exited) { + return; + } await _debugConnection?.close(); await _stdOutSub?.cancel(); await _webFs?.stop(); + _exited = true; } @override diff --git a/packages/flutter_tools/lib/src/web/chrome.dart b/packages/flutter_tools/lib/src/web/chrome.dart index 34e5ecf64b2..f6be2e77369 100644 --- a/packages/flutter_tools/lib/src/web/chrome.dart +++ b/packages/flutter_tools/lib/src/web/chrome.dart @@ -155,26 +155,25 @@ class ChromeLauncher { static Future get connectedInstance => _currentCompleter.future; /// Returns the full URL of the Chrome remote debugger for the main page. -/// -/// This takes the [base] remote debugger URL (which points to a browser-wide -/// page) and uses its JSON API to find the resolved URL for debugging the host -/// page. -Future _getRemoteDebuggerUrl(Uri base) async { - try { - final HttpClient client = HttpClient(); - final HttpClientRequest request = await client.getUrl(base.resolve('/json/list')); - final HttpClientResponse response = await request.close(); - final List jsonObject = await json.fuse(utf8).decoder.bind(response).single; - return base.resolve(jsonObject.first['devtoolsFrontendUrl']); - } catch (_) { - // If we fail to talk to the remote debugger protocol, give up and return - // the raw URL rather than crashing. - return base; + /// + /// This takes the [base] remote debugger URL (which points to a browser-wide + /// page) and uses its JSON API to find the resolved URL for debugging the host + /// page. + Future _getRemoteDebuggerUrl(Uri base) async { + try { + final HttpClient client = HttpClient(); + final HttpClientRequest request = await client.getUrl(base.resolve('/json/list')); + final HttpClientResponse response = await request.close(); + final List jsonObject = await json.fuse(utf8).decoder.bind(response).single; + return base.resolve(jsonObject.first['devtoolsFrontendUrl']); + } catch (_) { + // If we fail to talk to the remote debugger protocol, give up and return + // the raw URL rather than crashing. + return base; + } } } -} - /// A class for managing an instance of Chrome. class Chrome { Chrome._( diff --git a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart index db867cf10ad..251650dc60e 100644 --- a/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_web_runner_test.dart @@ -338,6 +338,25 @@ void main() { verify(mockVmService.callServiceExtension('ext.flutter.profileWidgetBuilds', args: {'enabled': true})).called(1); })); + + test('cleanup of resources is safe to call multiple times', () => testbed.run(() async { + _setupMocks(); + bool debugClosed = false; + when(mockDebugConnection.close()).thenAnswer((Invocation invocation) async { + if (debugClosed) { + throw StateError('debug connection closed twice'); + } + debugClosed = true; + }); + final Completer connectionInfoCompleter = Completer(); + unawaited(residentWebRunner.run( + connectionInfoCompleter: connectionInfoCompleter, + )); + await connectionInfoCompleter.future; + + await residentWebRunner.exit(); + await residentWebRunner.exit(); + })); }