Skip to content

fix(marionette_mcp): time out connect when the VM service stops answering - #143

Open
uveshm003 wants to merge 1 commit into
leancodepl:mainfrom
uveshm003:fix/131-connect-timeout
Open

uveshm003 wants to merge 1 commit into
leancodepl:mainfrom
uveshm003:fix/131-connect-timeout

Conversation

@uveshm003

Copy link
Copy Markdown

Fixes #131

Problem

VmServiceConnector.connect() awaited vmServiceConnectUri, getVM and getIsolate with no timeout. Only streamListen had 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, connect waits 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(), which connect calls first when it is already connected, also awaited VmService.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 a TimeoutException that names the URI and what to check, and leaves the connector disconnected so the next connect starts fresh. The connect tool already returns that as an isError result: 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.
  • A handshake that times out is abandoned. If its WebSocket connects later, that service is disposed instead of being published, and the error path tears down only the service this attempt opened.
  • 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.
  • The CLI already wraps connect in 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_to and hot_restart can 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 on HttpServer and WebSocketTransformer:

  • connects to a responsive VM service
  • times out when the VM service stops answering requests
  • times out when the WebSocket handshake never completes
  • can connect again after a timed-out attempt
  • still reports a missing marionette binding without waiting
  • reconnects when the previous VM service stopped answering

I also ran these tests against the unfixed vm_service_connector.dart from main. 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 on main too, because the fake closes its socket cleanly, so it guards the new disconnect behaviour rather than reproducing a hang.

Checks run locally (Dart 3.12.2, not the Flutter 3.47.6 pinned in .fvmrc):

  • dart test in packages/marionette_mcp: 235 passed
  • dart test in packages/marionette_cli: 103 passed
  • dart analyze --fatal-infos lib bin test in packages/marionette_mcp: no issues
  • dart analyze --fatal-infos lib bin in packages/marionette_cli: no issues
  • dart format --set-exit-if-changed: clean

I 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.

…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
Copilot AI balanced review requested due to automatic review settings October 23, 2026 18:34
@netlify

netlify Bot commented Oct 23, 2026

Copy link
Copy Markdown

👷 Deploy request for marionette-landing pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 347d0ff

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment on lines +225 to +227
final isolateId = await handshake().timeout(
timeout,
onTimeout: () {
Comment on lines +244 to +246
if (identical(_service, service)) {
await disconnect();
}
Future<void> connect(String uri) async {
Future<void> connect(
String uri, {
Duration timeout = defaultConnectTimeout,
@mateusz-pietras

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

connect sometimes hangs for minutes with no timeout — timeline attached

3 participants