Skip to content

[fix] Announce the VM ready only once its QMP monitor answers - #229

Merged
themartiano merged 4 commits into
omacom:mainfrom
NimbleAINinja:fix-qmp-ready-contract
Sep 21, 2026
Merged

themartiano merged 4 commits into
omacom:mainfrom
NimbleAINinja:fix-qmp-ready-contract

Conversation

@NimbleAINinja

@NimbleAINinja NimbleAINinja commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Launching intermittently fails with:

The virtual machine started, but Try Omarchy could not enable safe Mac sleep. Please close and reopen the app. (I/O failure: cannot connect to QMP socket: Connection refused)

Most often on the first launch after installing a build; the guest console log is empty afterwards; reopening usually works. Seen on an M1 Max / macOS 27.

Cause

run-qemu-gpu.sh prints [qemu-gpu] Ready. QMP: â€Ķ as soon as QEMU has created its socket files. QEMU creates those early in qemu_init, but only accepts monitor connections from its main loop, which starts when init finishes — and the QMP chardev listens with a backlog of one (chardev/char-socket.c, qio_net_listener_open_sync(â€Ķ, 1, â€Ķ)).

The helper connects on Ready and gives the greeting two seconds. On a slow start (8 GiB guest, VirGL/ANGLE setup, cold caches after an install) init outlasts that. The connection had already been queued by the kernel, and a queued connection stays queued when its client gives up, so the backlog is now full: every later connect returns ECONNREFUSED until the main loop runs. The helper reads that as a broken monitor and sends QEMU SIGTERM — hence the empty console log.

Reproduced deterministically against the shipped QEMU with a deliberately slow init: one probe that gives up, then every connect refused, then success the instant the main loop came up. Pacing the helper's retries (#228) cannot help — they retry into a full queue — which is why that PR is closed in favour of this one.

Fix

Make Ready mean that the monitor completed its handshake. The launcher invokes the bundled Swift helper's --wait-for-qmp QEMU_PID SOCKET command before printing Ready, so release launches do not acquire a host Python dependency.

The helper reuses QMPConnection to validate the socket and peer PID, parse complete JSON messages, and negotiate qmp_capabilities. It closes each probe and retries refused or silent connections against a monotonic 60-second deadline, stopping promptly if the target process exits. Greeting and capability negotiation share each attempt's timeout. The launcher tracks the probe as a child so cancelling startup terminates both the probe and QEMU promptly.

Validation

  • Merged current main and resolved the helper/launcher-test conflicts.
  • make test passes on the merged branch.
  • Retry timing uses a controlled clock, including a scheduler wakeup after the deadline, so CI load cannot change the expected attempt count.
  • All 306 Swift tests pass, including readiness timeout, process-exit, and failed-probe cleanup coverage.
  • Native-helper socket integration covers delayed acceptance with backlog one, immediate readiness, fragmented greetings, interleaved events, rejected capability negotiation followed by recovery, and target exit. The helper runs with an unusable Python on PATH.
  • Launcher contract tests cover the actual native probe during a default NAT launch without Python, probe failure without a Ready event, and cancellation that reaps both children.
  • Existing guest, build-cache, runtime compatibility/relocation, networking, memory, power, persistent-storage, and disk-resize checks pass.
  • A real VM cold boot has not been rerun for this native-helper revision.

Launching intermittently failed with "The virtual machine started, but
Try Omarchy could not enable safe Mac sleep ... cannot connect to QMP
socket: Connection refused", most often on the first launch after
installing a build, and the guest console log was empty afterwards.

run-qemu-gpu.sh printed "Ready. QMP:" as soon as QEMU had created its
socket files. QEMU creates those early in initialisation, but accepts
monitor connections only from its main loop, which starts when
initialisation finishes, and the socket listens with a backlog of one.
The helper connected on Ready and waited two seconds for a greeting. On
a slow start -- an 8 GiB guest, VirGL and ANGLE setup, cold caches after
an install -- initialisation outlasted that wait. The connection had been
queued by the kernel, and a queued connection stays queued when its
client gives up, so the backlog was now full and every later connect was
refused until the main loop ran. The helper reads that as a broken
monitor and sends QEMU SIGTERM, which is why the console log was empty.

The mechanism was reproduced against the shipped QEMU with a
deliberately slow initialisation: one probe that gave up, four refused
retries, then success the instant the main loop came up. Spacing the
helper's retries cannot help; they retry into a full queue.

Make Ready mean what its consumer assumes. A new sourced library waits
until a connection to the QMP socket receives a greeting, retrying a
refused connect or a silent monitor every 100 ms. A probe that gives up
may itself occupy the backlog, and the main loop drains it as its first
act, so the next probe gets through. It fails if QEMU exits or after 60
seconds, so a monitor that never answers still fails startup.

The test drives the function against a stand-in that listens
immediately but accepts only after a delay, the way QEMU does, and
checks that Ready waits for the answer, does not hold up a monitor that
answers at once, and reports a dead QEMU promptly; it also checks that
the launcher consults the monitor before printing the Ready line. The
two launcher contract tests stage the new library into their bundle and
their fake QEMU now answers the monitor with a greeting, as QEMU does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@NimbleAINinja NimbleAINinja changed the title Announce the VM ready only once its QMP monitor answers [fix] Announce the VM ready only once its QMP monitor answers Sep 18, 2026
@themartiano
themartiano merged commit de0f1dc into omacom:main Sep 21, 2026
1 check passed
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.

2 participants