fix: find seats by probing the seat port block, not by asking the service - #2
Merged
Merged
Conversation
…vice Closes #1. Discovery polled http://127.0.0.1:9550/api/seats and emitted every seat at 127.0.0.1. On any machine that is not the host, that address is the CLIENT, which runs no MultiSeat service -- so the request failed and was swallowed on purpose ("silent failure, will retry"). Seats never appeared, no error was shown and nothing was logged. It could only ever have worked with Moonlight running on the host itself. Seats are now found by probing the seat port block on hosts the user already has. A seat's Apollo answers /serverinfo on its own port -- exactly what adding it by hand does, and that path is known to work: verified end to end on 2026-09-10 by adding 192.168.1.46:48100, pairing and streaming. Ports come from MultiSeat's own constants (PortBase 48100, PortsPerSeat 30 -> 48100/48130/48160/48190) and a seat identifies itself by the hostname ApolloConfigBuilder gives it, MultiSeat-{Account}-{N}. Anything answering on those ports without that prefix is left alone. Why probing rather than fixing the address: - no API key on the client, and no secret to distribute - MultiSeat's ApiBindLoopbackOnly stays true; its dashboard API never has to be reachable from the LAN - no dependency on mDNS, which does NOT work for seats. A seat's Apollo logs "Registered Apollo mDNS service" but the registration never reaches the network: Apollo registers through Windows' responder instead of binding 5353 itself, and a registration made inside an RDP session does not escape it. Measured -- browsing _nvstream._tcp with a seat running returns the console Apollo and nothing else, with that same browse finding the console Apollo every time as the control. Addresses are pulled on an aboutToPoll signal rather than pushed from the host add/remove paths, which run on a thread pool under ComputerManager's write lock -- reaching back into it from there invites a deadlock. Local and manual addresses only, deduplicated: a seat and the console Apollo beside it share one address, and seat ports are never port-forwarded. Parsing reuses NvHTTP::getXmlString rather than opening a second QXmlStreamReader on the same shape of document. ⛔ NOT compiled or run: this host has no Qt toolchain (no qmake, no cmake), so CI is the only verification. The probe's server side is proven -- the seat answers /serverinfo with the expected hostname -- but the client code itself has not executed anywhere. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQvL62WkT8xDWXqyjFCGDw
Merged
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.
Closes #1.
The bug
Discovery polled
http://127.0.0.1:9550/api/seatsand emitted every seat at127.0.0.1. On any machine that is not the host, that address is the client, which runs no MultiSeat service — so the request failed and was swallowed on purpose:Seats never appeared, no error was shown, nothing was logged. It could only ever have worked with Moonlight running on the host itself.
The fix
Seats are found by probing the seat port block on hosts the user already has. A seat's Apollo answers
/serverinfoon its own port — exactly what adding it by hand does, and that path is known to work: verified end to end on 2026-09-10 by adding192.168.1.46:48100, pairing, and streaming.Ports come from MultiSeat's own constants (
PortBase48100,PortsPerSeat30 → 48100/48130/48160/48190). A seat identifies itself by the hostnameApolloConfigBuildergives it,MultiSeat-{Account}-{N}. Anything answering on those ports without that prefix is left alone.Why probing rather than fixing the address
ApiBindLoopbackOnlystaystrue— its dashboard API never has to be reachable from the LANThat last point was measured, not assumed. A seat's Apollo logs
Registered Apollo mDNS service, but the registration never reaches the network — Apollo registers through Windows' responder rather than binding 5353 itself, and a registration made inside an RDP session does not escape it. Browsing_nvstream._tcpwith a seat running returns the console Apollo and nothing else, with that same browse finding the console Apollo every time as the control.Implementation notes
Addresses are pulled, not pushed. ComputerManager's host add/remove paths run on a thread pool under its write lock; reaching back into it from there invites a deadlock. Discovery raises
aboutToPolland ComputerManager fills the list in, on the timer's own thread.Local and manual addresses only, deduplicated. A seat and the console Apollo beside it share one address, and seat ports are never port-forwarded — probing a remote address would be four guaranteed failures per tick against someone else's network.
Reuses
NvHTTP::getXmlStringrather than opening a secondQXmlStreamReaderon the same shape of document.In-flight tracking is per endpoint (
host:port), replacing a singlem_RequestPendingflag that cannot express parallel probes.⛔ Not compiled or run
The machine this was written on has no Qt toolchain — no
qmake, nocmake— so CI is the only verification. The probe's server side is proven (a seat answers/serverinfowith the expected hostname), but the client code itself has not executed anywhere.Realistic risks are compile errors and Qt-idiom mismatches.
QReadLockeragainstCopySafeReadWriteLockwas checked by inheritance (: public QReadWriteLock) and the XML comparison follows the existing pattern innvhttp.cpp, but that is reasoning rather than a compiler.Known limitation
PortBaseandMaxSeatsare configurable host-side and nothing advertises them, so a host that has changed them still needs its seats added manually. Probing the default block covers the normal case with no host-side cooperation at all.🤖 Generated with Claude Code
https://claude.ai/code/session_01SQvL62WkT8xDWXqyjFCGDw