Repository navigation
Reland "[flutter_tools] Fix DTD "Stream already subscribed" error and wait for analysis over DTD in widget preview" - #193658
Merged
auto-submit[bot] merged 1 commit intoOct 2, 2026
Conversation
… wait for analysis over DTD in widget preview" This relands flutter#192930 (reverted in flutter#193637). What changed since the original PR: - In `AnalysisServer`, store and cancel the `stdout` and `stderr` stream subscriptions in `dispose()` before closing `_analyzingController` and `_errorsController`, and guard `_analyzingController.add` with `!_analyzingController.isClosed` in `_handleProgress`. Previously, `$/progress` notifications arriving from the language server during or after `AnalysisServer.dispose()` threw `Bad state: Cannot add new events after calling close`. - In `widget_preview_test_helpers.dart`, await `analysisServer.onExit` after `analysisServer.dispose()` in `addTearDown` so the analysis server process has fully exited before temporary directory cleanup on Windows. - Added a hermetic unit test in `analyze_continuously_test.dart` verifying that `AnalysisServer` does not throw when `$/progress` notifications arrive during or after `dispose()`. Original description: During widget preview startup or file updates: 1. Concurrent calls to retrieve previews or wait for analysis (e.g., initial command startup racing with file watcher events) could concurrently invoke `_waitForLspService()`, causing duplicate `streamListen` calls to the Dart Tooling Daemon. DTD throws JSON-RPC error 103 (`Stream already subscribed`) when a client attempts to subscribe to a stream it is already listening to. 2. `WidgetPreviewDtdServices` previously considered the `Lsp` service ready as soon as *any* `Lsp` method was registered on DTD (or upon receiving the first `ServiceRegistered` event for `Lsp`). Because the analysis server registers its LSP methods sequentially before posting an `initialized` event on the `Lsp` stream, calls to `Lsp.dart/workspace/analysis/complete` or `Lsp.dart/workspace/getFlutterWidgetPreviews` could race ahead of their registration and fail with `-32601 (method not found)`. 3. When connecting to a pre-existing analysis server over DTD (`--dtd-url`), `LspPreviewDetector.analysisServer` is `null`, causing `analysisServer?.waitForAnalysis()` calls during startup and file updates to be no-ops. This change: - Deduplicates in-flight calls to `_waitForLspService()` using a shared future (`_waitForLspServiceFuture`) so concurrent callers await the same LSP registration check instead of issuing duplicate DTD requests. - Separates `lspServiceAvailable` (whether an analysis server has begun registering `Lsp` services on DTD) from `_allLspServicesRegistered` (whether all required widget preview LSP methods in `kRequiredLspServices` are registered), and updates `_waitForLspServiceHelper()` to listen for the `initialized` event on the `Lsp` stream before checking `getRegisteredServices()`. - Consolidates LSP DTD calls into `_callLspService()` and removes the `kMethodNotFound` polling loop now that `_waitForLspService()` ensures all required LSP methods are registered. - Safely catches and ignores `RpcErrorCodes.kStreamAlreadySubscribed` on `streamListen` calls in `WidgetPreviewDtdServices` (`safeStreamListen`). - Adds `WidgetPreviewDtdServices.waitForAnalysis()` (invoking `Lsp.dart/workspace/analysis/complete` over DTD), unifies `LspPreviewDetector.waitForAnalysis()` so widget preview waits for analysis completion on both tool-managed and pre-existing analysis servers, and moves the startup `waitForAnalysis()` call inside the DTD error-handling `try / catch` block in `WidgetPreviewStartCommand`. - Adds hermetic regression tests covering concurrent/pre-subscribed DTD streams, partial vs. full LSP service registration (`Lsp.initialized`), and waiting for analysis over DTD. Fixes flutter#192424
Contributor
There was a problem hiding this comment.
Code Review
This pull request refactors the Dart Tooling Daemon (DTD) and LSP preview detection logic in flutter_tools. Key changes include introducing a waitForAnalysis method to wait for analysis completion directly over DTD, preventing concurrent stream subscription issues by caching the wait future, and adding a safeStreamListen helper to ignore already subscribed exceptions. Additionally, AnalysisServer is updated to cancel stdout and stderr stream subscriptions on disposal and to avoid adding events to a closed controller. Comprehensive unit tests have been added to verify these behaviors. There are no review comments to address, and I have no further feedback to provide.
mdebbar
approved these changes
Oct 2, 2026
This was referenced Oct 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Relands #192930 (which was reverted in #193637).
What changed since the original PR
AnalysisServer, store and cancel thestdoutandstderrstream subscriptions indispose()before closing_analyzingControllerand_errorsController, and guard_analyzingController.addwith!_analyzingController.isClosedin_handleProgress. Previously,$/progressnotifications arriving from the language server during or afterAnalysisServer.dispose()threwBad state: Cannot add new events after calling closeinwidget_preview_scaffold_test.dart.widget_preview_test_helpers.dart, awaitanalysisServer.onExitafteranalysisServer.dispose()inaddTearDownso the analysis server process has fully exited before temporary directory cleanup on Windows.analyze_continuously_test.dartverifying thatAnalysisServerdoes not throw when$/progressnotifications arrive during or afterdispose().Original description
During widget preview startup or file updates:
_waitForLspService(), causing duplicatestreamListencalls to the Dart Tooling Daemon. DTD throws JSON-RPC error 103 (Stream already subscribed) when a client attempts to subscribe to a stream it is already listening to.WidgetPreviewDtdServicespreviously considered theLspservice ready as soon as anyLspmethod was registered on DTD (or upon receiving the firstServiceRegisteredevent forLsp). Because the analysis server registers its LSP methods sequentially before posting aninitializedevent on theLspstream, calls toLsp.dart/workspace/analysis/completeorLsp.dart/workspace/getFlutterWidgetPreviewscould race ahead of their registration and fail with-32601 (method not found).--dtd-url),LspPreviewDetector.analysisServerisnull, causinganalysisServer?.waitForAnalysis()calls during startup and file updates to be no-ops.This change:
_waitForLspService()using a shared future (_waitForLspServiceFuture) so concurrent callers await the same LSP registration check instead of issuing duplicate DTD requests.lspServiceAvailable(whether an analysis server has begun registeringLspservices on DTD) from_allLspServicesRegistered(whether all required widget preview LSP methods inkRequiredLspServicesare registered), and updates_waitForLspServiceHelper()to listen for theinitializedevent on theLspstream before checkinggetRegisteredServices()._callLspService()and removes thekMethodNotFoundpolling loop now that_waitForLspService()ensures all required LSP methods are registered.RpcErrorCodes.kStreamAlreadySubscribedonstreamListencalls inWidgetPreviewDtdServices(safeStreamListen).WidgetPreviewDtdServices.waitForAnalysis()(invokingLsp.dart/workspace/analysis/completeover DTD), unifiesLspPreviewDetector.waitForAnalysis()so widget preview waits for analysis completion on both tool-managed and pre-existing analysis servers, and moves the startupwaitForAnalysis()call inside the DTD error-handlingtry / catchblock inWidgetPreviewStartCommand.Lsp.initialized), and waiting for analysis over DTD.Fixes #192424
Pre-launch Checklist
///).