Repository navigation
fix(marionette_mcp): time out connect when the VM service stops answering - #143
Merged
mateusz-pietras merged 7 commits intoOct 28, 2026
Merged
Conversation
…ring connect awaited the WebSocket handshake, getVM and getIsolate with no timeout, so an app that accepted the connection but stopped responding (a sleeping device, an app paused in the debugger) made the tool wait until the MCP client gave up, which can take up to 30 minutes. The handshake now fails after 30 seconds with a TimeoutException that says what to check, and leaves the connector disconnected so the next connect starts fresh. A handshake that times out is abandoned: if its service connects later it is disposed instead of being published. disconnect also stops waiting for an unresponsive VM service to close after 5 seconds, so the previous app can't block a reconnect. Fixes leancodepl#131
uveshm003
requested review from
jakubfijalkowski and
mateusz-pietras
as code owners
October 23, 2026 18:34
👷 Deploy request for marionette-landing pending review.Visit the deploys page to approve it
|
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unbounded MCP setup requests, overlapping-attempt cleanup, and CLI timeout compatibility remain unresolved.
3 open issues
What changed in this PR
Addresses #131 by bounding VM service connection waits, extending the earlier stream-subscription timeout from #127.
Changes:
- Adds a configurable handshake timeout, defaulting to 30 seconds, with actionable errors.
- Bounds disconnect cleanup and handles WebSockets that connect after an attempt times out.
- Adds fake-server regression tests and an Unreleased changelog entry.
| File | Description |
|---|---|
packages/marionette_mcp/test/vm_service/connect_timeout_test.dart |
Tests responsive connections, timeouts, retries, and reconnects. |
packages/marionette_mcp/lib/src/vm_service/vm_service_connector.dart |
Adds handshake deadlines and bounded connection cleanup. |
CHANGELOG.md |
Documents the connection timeout behavior. |
Review effort: Balanced
Member
|
Hello, thank you for the contribution. We will look into the provided solution as well as review issues raised by copilot and follow up shortly. |
An attempt published its VM service, event subscription and registered services as soon as the WebSocket opened, and looked up the isolate through the shared service. When a second connect started while the first was still in flight, the first could finish on the second's service, and on timeout it skipped its cleanup because the shared service was no longer its own: its socket stayed open and its ServiceRegistered events kept landing in the next connection's registry. Each attempt now keeps its service, subscription, registered services and isolate lookup local, publishes them only when it succeeds, and closes its own connection when it fails. An attempt overtaken by a newer connect or a disconnect closes itself instead of publishing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
connect awaited disconnect() for the previous app before its deadline started and again for its own connection after a failure. Each close waits up to 5 s for a VM service that stopped reading, so a timed-out connect could take about 10 s longer than its timeout. connect now drops both connections without waiting for them to close; they still close in the background, with the same 5 s limit. The test freezes a proxied VM service in the middle of a large request, which makes closing that connection hang. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"reconnects when the previous VM service stopped answering" passed without the limit: a VM service that only stops answering still reads, so closing its connection finishes at once. Closing hangs when the app stops reading while a request is still queued. Replace it with a test that freezes a proxied VM service in the middle of a large request and expects disconnect to return after the limit. Without the limit, disconnect does not return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndshake The deadline covered only the VM service handshake. An app paused in the debugger still answers getVM and getIsolate, so the handshake finished, and the tool then waited indefinitely for getVersion: service extensions don't run while the isolate is paused. listExtensions and getConfiguration had the same problem. The connect tool now runs the handshake, the version check, the dynamic tool registration and the session setup under one 30 s deadline. When it fires, the tool disables the dynamic tools, disconnects and says what to check. The setup keeps running in the background, so it changes the connection, the tools and the session only while its call has not timed out or been replaced by a newer one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The instance, doctor and unregister commands wrapped connect in their own --timeout, but connect now gives up after its own 30 s default first. With --timeout above 30 s, a slow but working app failed after 30 s, and `unregister --stale` removed it. The commands now pass --timeout to connect. The instance commands keep their message, and doctor prints connect's message instead of "Future not completed". connect's message no longer tells the reader to "call connect again" or to check for an app paused in the debugger: the CLI has no connect call to repeat, and a paused app still finishes the handshake. The MCP connect tool says both in its own message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
withStepLogging logged a connect call's result into whatever session was active when the call returned. A call that timed out after a newer connect had opened a session therefore wrote its error line into the newer session's steps.md. The connect tool now logs its own step, and only while the call is still the current one. Add tests for the race guards that had none: a timed-out call does not log into a newer session, a replaced call does not disconnect the newer connection, an attempt overtaken by a newer connect closes itself, and a stale attempt's ServiceRegistered event does not answer a pending waitForServiceRegistration. Give the connect tool test and the overlap test more room before the deadline, so a slow runner can't make them fail or pass without proving anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Member
|
@uveshm003 Thank you once again for the contribution, I added small clean ups and connection hardening. This will land in the next release |
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.


Fixes #131
Problem
VmServiceConnector.connect()awaitedvmServiceConnectUri,getVMandgetIsolatewith no timeout. OnlystreamListenhad one (500 ms, from #127). When an app accepts the connection but then stops answering, for example on a sleeping device or with the app paused in the debugger,connectwaits until the MCP client gives up. That can take up to 30 minutes, which matches the 1 m 43 s and 9 min hangs in the issue's timeline.disconnect(), whichconnectcalls first when it is already connected, also awaitedVmService.dispose()with no limit.Change
connect(uri, {timeout})puts a timeout on the whole handshake, 30 s by default (VmServiceConnector.defaultConnectTimeout). When the timeout fires, it throws aTimeoutExceptionthat names the URI and what to check, and leaves the connector disconnected so the nextconnectstarts fresh. Theconnecttool already returns that as anisErrorresult:Failed to connect to app: … Timed out connecting to the VM service at ws://… after 30s. Make sure the app is still running, is not paused in the debugger, and its device is awake, then call connect again.disconnect()clears its state first, then waits at most 5 s for the old service to close, so an unresponsive app that was connected before can't block a reconnect.connectin its own--timeout(5 s by default), so its behaviour is unchanged.I left the per-call timeouts on the other tools (the second half of the issue's suggestion) out of this PR on purpose.
take_screenshots,scroll_toandhot_restartcan legitimately take a while, so choosing those limits seemed like a maintainer decision. I'm happy to follow up if you want them.Tests
New file
packages/marionette_mcp/test/vm_service/connect_timeout_test.dart. The tests use a local fake VM service built onHttpServerandWebSocketTransformer:I also ran these tests against the unfixed
vm_service_connector.dartfrommain. The three timeout tests hung until the test runner's 8 s timeout, which reproduces the bug; the other three passed. The last test passes onmaintoo, because the fake closes its socket cleanly, so it guards the newdisconnectbehaviour rather than reproducing a hang.Checks run locally (Dart 3.12.2, not the Flutter 3.47.6 pinned in
.fvmrc):dart testinpackages/marionette_mcp: 235 passeddart testinpackages/marionette_cli: 103 passeddart analyze --fatal-infos lib bin testinpackages/marionette_mcp: no issuesdart analyze --fatal-infos lib bininpackages/marionette_cli: no issuesdart format --set-exit-if-changed: cleanI haven't tested this against a real sleeping device. The hang is reproduced with the fake VM service above.
I added a CHANGELOG entry under Unreleased.