diff --git a/docs/release-notes/0.6.3.md b/docs/release-notes/0.6.3.md new file mode 100644 index 0000000..5c54f42 --- /dev/null +++ b/docs/release-notes/0.6.3.md @@ -0,0 +1,103 @@ +**Correctness fixes for seat lifecycle, and the first release whose changes were exercised on real +hardware rather than only in tests.** + +## A process is now identified by more than its PID + +Windows recycles process IDs. MultiSeat recorded a seat's Apollo by PID alone and then acted on +that number later — checking whether it was alive, and terminating it on teardown. Once the +original process exits, that PID can belong to something else entirely, and nothing checked. + +A seat's Apollo is now identified by its PID **and** the start time the operating system reports +for it. Termination happens only while both still match, and the check and the kill share a single +open handle, because Windows will not recycle a PID while a handle to that process is open. If the +identity does not match, nothing is killed and the mismatch is logged. + +Where no identity could be recorded — the start time was unreadable at launch — the process name is +checked before terminating. That is weaker, and it is the last resort rather than the default. + +**A restart used to break this**, and that is fixed here too. Restarting a seat's Apollo advanced +the PID while leaving the recorded identity pointing at the process that had just exited. The +record then contradicted itself: liveness checks reported a perfectly healthy Apollo as dead, and +teardown killed the old PID and left the running one behind. Both are corrected. + +## Teardown holds the lock before it changes anything + +Tearing a seat down removed it from the registry and *then* waited for that seat's lifecycle lock. +Acquiring the lock can time out when another operation holds it, and a timeout at that point left +the seat unregistered while its session, its Apollo and its ports were all still running — with no +way back, because everything that tears a seat down starts by looking it up. + +The lock is now acquired first. A timeout leaves the seat registered and untouched, so the request +can simply be retried. + +## Operations check the seat is still there + +Any operation that waits for a seat's lifecycle lock can be admitted after a teardown has already +removed that seat. Changing resolution was the worst case: it creates a **new** Windows session and +a new Apollo, both belonging to a seat that no longer exists and which nothing would ever clean up. + +Every such operation now re-checks the seat after acquiring the lock and stops if it is gone. +Resetting the display, the audio routing or the controller, and launching an app into a seat, take +the lock at all now — previously they did not. + +## The API answers with the right status codes + +Every failure used to answer `400 Bad Request`, including "the host is full", which tells a client +its request was malformed when the request was fine. + +| condition | was | now | +|---|---|---| +| Seat limit reached, or no free port block | 400 | **503** Service Unavailable | +| Account already has a seat, or already exists | 400 | **409** Conflict | +| Seat not found | 400 | **404** Not Found | +| A genuinely malformed request | 400 | **400** | + +503 and 409 are deliberately different: one is worth retrying later, the other cannot succeed until +something changes. + +## What was actually tested + +Unlike previous releases, these changes were run on real hardware before shipping, not only in CI: + +- a seat provisioned, streamed to a Moonlight client, and was torn down cleanly +- the client received the seat's own desktop, and input worked +- `hevc_nvenc` opened inside the seat's session without trouble +- a seat's Apollo was restarted mid-life, and the recorded identity followed the new process +- teardown after that restart terminated the running Apollo rather than leaking it +- a standalone Apollo streamed simultaneously throughout and was never touched + +## Install + +Download `multiseat-windows-x64.zip`, extract it anywhere, and run both scripts from inside the +extracted folder: + +```powershell +.\prerequisites\install-prerequisites.ps1 # drivers - needed either way +.\scripts\install-service.ps1 -FromZip . +``` + +Self-contained as before: no clone, no .NET SDK, no .NET runtime, no Node. + +## If your seats stopped working recently + +A Windows update can replace `termsrv.dll` with a build RDP Wrapper has no offsets for. RDP Wrapper +then silently stops patching, and because every seat is an RDP session, every seat dies with it. +That happened on our own machine while preparing this release, and it had been broken for two days +without anything reporting it. + +```powershell +.\scripts\check-rdpwrap-offsets.ps1 # is this build covered? +.\scripts\check-rdpwrap-offsets.ps1 -Apply # compute and apply offsets locally +``` + +0.6.2 added the check that makes this visible at install time; this release does not change it. + +## Known limitations + +- **Seats capture the RDP surface, not a dedicated virtual display.** A seat's display reports a + 1000 Hz refresh rate, which is the sign of this. It is a Windows constraint on where a virtual + display can be attached, not a regression, and it is unchanged here. +- **A streaming seat still reports its status as `Ready`.** The stream works; the status simply + does not reflect it. +- Seat auto-discovery in MoonlightVibe does not find seats from another machine. Add the seat by + address and port instead — a seat's port block starts at 48100 by default. diff --git a/version.txt b/version.txt index b616048..844f6a9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -0.6.2 +0.6.3