Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions docs/release-notes/0.6.3.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.6.2
0.6.3
Loading