mirror of
https://github.com/games-on-whales/wolf.git
synced 2026-10-15 13:02:03 +00:00
[GH-ISSUE #490] Joypad is unplugged from the lobby container right after being plugged in (race on JoinLobbyEvent) #298
Labels
No labels
🧠 1 - 🤖 999
AMD
bug
bug
cannot reproduce
documentation
duplicate
easy
enhancement
enhancement
enhancement
fixed?
fixed?
good first issue
hard
help wanted
Intel
invalid
LXC
medium
moonlight-protocol
Multi GPU
nixos
not a bug
nvidia
pull-request
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
games-on-whales/wolf#298
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Originally created by @piotrminkina on GitHub (Aug 28, 2026).
Original GitHub issue: https://github.com/games-on-whales/wolf/issues/490
Summary
When a Moonlight session joins a lobby, the joypads it already owns are moved from the session's own container into the lobby container. The move fires an
UnplugDeviceEventthat is intended for the session's runner, but the lobby-sideUnplugDeviceEventhandler picks it up as well and re-fires it at the lobby — deleting the device node that the accompanying plug is concurrently creating.Because the plug goes through
lobby->plugged_devices_queue(drained asynchronously) while the unplug is executed inline, which of the two wins is timing-dependent. With N pads it is N independent coin flips, which matches the field reports of "3-4 pads worked, after a reconnect only one did".leave_lobby()performs the mirror-image operation and gets it right;JoinLobbyEventdoes not. The difference is the ordering againstconnected_sessions.Where (
stable,7f416ec— also present in thed6d41deimage build)src/moonlight-server/sessions/lobbies.cpp:209adds the session tolobby->connected_sessionsfirst, so from that point on both lobby-side handlers match thissession_id. Then, for every joypad (lobbies.cpp:220-238):PlugDeviceEventhandler (lobbies.cpp:294), which logs[LOBBY] adding device to session ... in lobby ...and pushes a copy with.session_id = lobby->idontolobby->plugged_devices_queue. This is the path that actually works.UnplugDeviceEventhandler (lobbies.cpp:311) has no way to distinguish "this pad is being moved into the lobby" from "the user physically unplugged this pad", so it re-fires the event with.session_id = lobby->id. The lobby's docker runner (runners/docker.cpp:225-227) matches and synchronously execsfake-udev -m <remove> && rm <DEVNAME>(docker.cpp:244) plusstd::filesystem::remove(hw_db_path / filename)— in the container the pad is being moved into.session_idonto the lobby's queue. The runner filters withdevice_ev->get().session_id == session_idwheresession_id == lobby->id(docker.cpp:255), so this push is always discarded. It looks like a leftover duplicate of (A).The asymmetry that turns this into a race: the plug is queued and drained by the runner loop (
docker.cpp:254pop(50ms),docker.cpp:280sleep_for(500ms)), while the unplug is executed inline in the handler.For comparison,
leave_lobby()(lobbies.cpp:17-53) removes the session fromconnected_sessionsbefore touching the joypads, then firesPlug{session_id = session}(no longer matched by the lobby handler → correctly reaches the session's own queue) andUnplug{.session_id = lobby.id}directly, which the lobby-side handler cannot re-fire becauseconnected_sessionsholds moonlight session ids, never lobby ids. No race.Evidence
Steam lobby, 3 h session, single client. Tail of the Wolf log:
State of the lobby container immediately after, and for the following two hours:
The node was created and removed within that same moment. Note that this join produced no
Docker exec failedwarning, i.e. thermreturned 0, i.e. the node did exist when the unplug ran — the plug had already landed and was wiped.The other side of the same problem is visible as noise: the log contains 43 ×
Docker exec failed (1), and every single one of them iswith zero
mknodfailures — removes broadcast at a container that does not hold the device.All plug/unplug logging in
docker.cppislogs::debug, so at the default log level only the[LOBBY]lines and theseDocker exec failedwarnings are visible.Reproduce
stable, one Moonlight client, an app running in a lobby, at least one controller connected to the client.Workaround that avoids it deterministically: disconnect every controller from the client before rejoining, join the lobby, and only then connect the controllers. With
session->joypadsempty the loop atlobbies.cpp:221does nothing, and each pad then arrives viacreate_new_joypad(), which fires only aPlugDeviceEventwith no accompanying unplug. Verified working.Suggested fix
Mirror what
leave_lobby()already does — the ordering againstconnected_sessionsis what makes it safe:lobby->connected_sessions, so the lobby-side handler does not match it and cannot re-fire it. (Or give the move its own event/flag so the lobby-sideUnplugDeviceEventhandler skips the re-broadcast.).session_id = lobby->idand drop (A), or drop (C) and keep (A). Right now (C) is dead code and (A) is load-bearing, which is almost certainly the opposite of what was intended.Worth noting that simply deleting (B) is not sufficient: the pad would then stay plugged into the Wolf UI container, and every join/leave cycle would leave another stale set of nodes behind there — the leak just moves rather than disappearing.
I am happy to prepare a PR if you agree with the direction.
Update — this is very likely already fixed on an existing branch
fix/joypad-input-leak(single commit920fea3, "fix(lobbies): stop gamepad input leaking into Wolf-UI across lobby switches", targeting #435) routes every join/leave hand-off through a newmigrate_joypad()helper that does unplug-from-source →inputtino::Joypad::recreate_device()→ plug-into-target. That ordering fixes this race by construction, and the "Suggested fix" section above should be read as superseded by it.Worth flagging that the author almost certainly did not know they were fixing this. The branch is based on
2f790f6, 21 commits behindstable, and predatesa9d7c60(#452) — on its base the lobbyUnplugDeviceEventrelay was still dead code, so this race could not occur there and the ordering was never validated against a live relay.Two things that do not survive a rebase onto current
stable:1. A spurious unplug still reaches the lobby on every join.
connected_sessions->updateremains before the joypad loop, so the live relay catchesmigrate_joypad's step-1Unplug{session_id}and re-broadcasts it asUnplug{lobby.id}— a synchronousdocker execinto a container that does not have the node yet, one per pad per join, inside the join's 2 s deadline. It is harmless only by accident: nothing states or tests the invariant "the lobby has no node at join time". It also reproduces exactly theDocker exec failed (1) … rm: cannot removenoise that made this bug hard to diagnose in the first place.A targeted way to make the intent explicit rather than positional — the hand-off knows it is a hand-off, so let the event say so:
set it in
migrate_joypad's step 1, and return early in the relay handler when it is set.UnplugDeviceEventhas noReflectorand does not appear inspec.json, so the field is purely additive.2. Defect 3 above survives verbatim. In the join call
source == target == session_id, andtarget_queueislobby->plugged_devices_queue— so the direct push still carries the moonlight session id into the lobby's queue and the runner still discards it (docker.cpp:255). The commit's own comment explains that the working plug goes through the relay's re-stamp, which makes the push redundant rather than merely mis-stamped. Removing it leavestarget_queuealwaysnullptr(leave already passes that), so the parameter can go too.Dependency the branch does not declare:
920fea3does not bump the inputtino pin, andrecreate_device()exists in neitherfd136cf(the pinned commit) nor inputtino's currentstable— only in the open games-on-whales/inputtino#39. As it stands the branch cannot compile against either.Happy to prepare a PR on top of that branch with the two items above plus a regression test — there is currently no test touching
migrate_joypad, andtests/testWolfAPI.cppalready has the seam for one (it builds aLobbystraight into state, no compositor, no Docker).