A starting desktop says so, instead of looking absent - #46
Merged
Conversation
The control socket was bound inside tauri's `setup`, which runs only
after the webview is initialized. On Windows that is routinely 15s and
has been measured past 30. For that whole window `connect` was refused,
so `ControlChannel` returned NoDesktop and `gx status` said "no desktop
is running" about a desktop the user could watch starting.
Refused is indistinguishable from absent. The state a user most wants
named was the one state the protocol could not express.
The socket is now bound at the top of `main`, before tauri exists, and
the accept loop starts immediately with it. `connect` therefore succeeds
from the first millisecond.
What the socket can DO is a separate question, answered per method rather
than by blocking:
- `status` needs no window and works at once. It is the method that
distinguishes "starting" from "not there", so it is exactly the one
that must not wait.
- anything needing a window -- watch/show, put-document, push-text, the
session tier -- gets a typed STARTING refusal.
The rejected alternative matters: binding early but deferring the accept
loop would also make `connect` succeed, and would then leave the client
BLOCKED on a read for the whole webview startup, because
ControlChannel.call has no read timeout. A fast, true refusal beats a
hang that looks like a fast success.
Mechanics:
- ConnectionContext.app_handle becomes Arc<OnceLock<AppHandle>>, filled
by `setup`. Handlers ask through ConnectionContext::app.
- spawn_control_server splits into bind_control_socket (called in main)
and the accept loop. check_socket_path_length still runs before any
bind, and set_owner_only_permissions still runs immediately after it
-- both verified, the former by tripping it during testing.
- the runtime file is written AFTER the bind, not before. Writing it
first published a path that did not answer yet, which is the state
that made a starting desktop look like a dead one. The file existing
and the socket existing are now the same fact.
- StatusBody gains `state`: "starting" | "running". `running` stops
being a hardcoded `true` -- it could only ever be read by a client
that had already connected, so it said nothing; it now means "there
is a window".
gx reads `state`, and treats its ABSENCE as running: a desktop from
before this only ever answered once fully up, so silence means running.
Read the other way, every older desktop would report itself as starting
forever. The pre-existing status tests stub a body with no `state`, so
they pin that compatibility without being touched.
Verified end to end against a sandboxed instance (HOME redirected, so the
running desktop was untouched), polling the socket every 5ms from launch:
t=0.001s state=starting
t=0.264s state=running
First answer 1ms after launch, where the old build refused until setup.
macOS closes the gap in 264ms; Windows is where the 15-30s version of that
gap lives, and where the runtime smoke's new socket-timing diagnostics
will show it.
Unit tests cover both halves, and the "starting" one was confirmed to fail
when the state mapping is broken. The Rust test is only possible because
the handle became a OnceLock -- an AppHandle cannot be constructed in a
unit test, so this state was previously untestable.
✅ Deploy Preview for graph-explorer-net ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
… answers
Caught by the new PR gate on this very branch, which is what it is for:
the macOS runtime smoke waited for the control channel, got an answer,
and then failed one line later on
assertion failed for release status running: expected 'true', got 'false'
Binding the socket before the webview is the point of this branch, so a
bound socket stopped being proof of a usable desktop — it now answers
`running: false` while starting. `control_ready` only checked that the
call returned "ok", which was indistinguishable from "there is a window"
back when `running` was hardcoded `true`, and became wrong the moment the
field started meaning something.
So every caller of `control_wait_ready` was racing the webview. It did not
show up before because the race was previously unwinnable in the other
direction: nothing answered at all until the window existed.
Worth noting which implementation was right. The Windows gate has always
checked `$status.result.running` in its readiness loop; the shell helper
never did. Two implementations of the same wait, disagreeing, and the
stricter one was correct — the macOS leg is simply the one that reached
the assertion first.
Verified against a real starting desktop in a sandboxed HOME: immediately
after launch `control_ready` returns false, and `control_wait_ready` then
succeeds reporting running=true, state=running.
The Linux leg of the PR gate caught the previous commit:
FAIL a live socket is ready: expected [0], got [1]
`control_ready` now requires `running: true`, and the self-test's stub
answered `status` by echoing the request's params — so it replied `{}`,
with no `running` at all. The helper was right and its stub was a desktop
that could not exist.
The stub now answers `status` the way a desktop does, and can be told to
report the state one is in while its window comes up. That turns the
distinction this branch introduces into something assertable with no
build and no desktop, in about a second:
ok a live socket is ready
ok a starting desktop is not ready
ok and it says so
ok and it is ready once the window is up
Confirmed to discriminate: with the old, weaker `control_ready` restored,
"a starting desktop is not ready" fails. It pins the behaviour rather
than merely passing alongside it.
The pre-existing "a defaulted params object is valid JSON" check keeps its
point — it exists for the bash 3.2 `${2:-{\}}` bug, and asserts the frame's
CONTENT rather than the call's success, which is what made it catch a bug
that looked like "no desktop". Status now carries a real body, so the
echoed params moved to `.result.echo` and the check reads one level
deeper. Same assertion, same reason.
Three gates have now each caught a different consequence of binding the
socket early, none of which a unit test would have reached: macOS found
the racing wait, Linux found the stub that made the wait look fine, and
the earlier Windows timing found how long the window really takes.
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.
The control socket was bound inside tauri's
setup, which runs only after the webview is initialized. On Windows that's routinely 15s and has been measured past 30. For that whole windowconnectwas refused, soControlChannelreturnedNoDesktopandgx statussaid "no desktop is running" about a desktop the user could watch starting.Refused is indistinguishable from absent. The state a user most wants named was the one state the protocol couldn't express.
The change
The socket is bound at the top of
main, before tauri exists, with the accept loop started immediately.connectsucceeds from the first millisecond. What the socket can do is then answered per method rather than by blocking:statusSTARTINGrefusalThe rejected alternative matters
Binding early but deferring the accept loop would also make
connectsucceed — and would then leave the client blocked on a read for the entire webview startup, becauseControlChannel.callhas no read timeout. A fast, true refusal beats a hang that looks like a fast success.Mechanics
ConnectionContext.app_handlebecomesArc<OnceLock<AppHandle>>, filled bysetup; handlers ask throughConnectionContext::app.spawn_control_serversplits intobind_control_socket(called inmain) and the accept loop.check_socket_path_lengthstill runs before any bind andset_owner_only_permissionsimmediately after — both verified, the former by tripping it during testing.StatusBodygainsstate.runningstops being a hardcodedtrue(it could only ever be read by a client that had already connected, so it said nothing) and now means "there is a window".Wire compatibility
gxtreats an absentstateas running: a desktop from before this only ever answered once fully up, so silence means running. Read the other way, every older desktop would report itself as starting forever. The pre-existing status tests stub a body with nostate, so they pin that compatibility without being touched.Verification
End to end against a sandboxed instance (
HOMEredirected, so the running desktop was untouched), polling the socket every 5ms from launch:First answer 1ms after launch, where the old build refused until
setup. macOS closes the gap in 264ms; Windows is where the 15–30s version lives, and where the runtime smoke's new socket-timing diagnostics will show it.Unit tests cover both halves, and the "starting" one was confirmed to fail when the state mapping is deliberately broken. The Rust test is only possible because the handle became a
OnceLock— anAppHandlecan't be constructed in a unit test, so this state was previously untestable.Full local suite green: 2178 tests, plus 36 Rust tests.