diff options
| author | historia <[not public]> | 2026-09-15 20:35:23 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-15 20:35:23 -0400 |
| commit | fcb806c3445ce56f2e57e208de773b4dac957c8d (patch) | |
| tree | 3b08c3d51bfe5456dcd215449cf07509aca2353a /internal/game/owner_disconnect_test.go | |
| parent | 69235cac5b3da5ff0dc20310e7474cabd8e63ba3 (diff) | |
| download | thehouseoficarus-fcb806c3445ce56f2e57e208de773b4dac957c8d.tar.gz | |
fix: owner re-entrancy deadlock, telnet write queue, recovery/refund money safety, admin path traversal
Diffstat (limited to 'internal/game/owner_disconnect_test.go')
| -rw-r--r-- | internal/game/owner_disconnect_test.go | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/internal/game/owner_disconnect_test.go b/internal/game/owner_disconnect_test.go new file mode 100644 index 0000000..e43a341 --- /dev/null +++ b/internal/game/owner_disconnect_test.go @@ -0,0 +1,71 @@ +package game + +// Disconnect-cleanup regressions: DisconnectTick runs on the owner +// goroutine, so the Hub removal callback it triggers must execute inline +// rather than re-submitting through run() — which would deadlock waiting +// for the running tick to finish (freezing every tick afterward). + +import ( + "testing" + "time" + + "thehouseoficarus/internal/net" +) + +// TestDisconnectTickHardRemoveCompletesOnOwner guards the single-owner +// re-entrancy fix: DisconnectTick (owner) -> Hub.HardRemove -> onRemove +// callback must not block on the owner from the owner. +func TestDisconnectTickHardRemoveCompletesOnOwner(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice", "pw", false) + h.seedCharacter("alice", "alice") + sess, _ := h.connect("alice") + + done := make(chan struct{}) + h.g.RunOnOwner(func() { + sess.Disconnecting = true + sess.DisconnectTicks = 1 + }) + h.barrier() + + go func() { + h.g.RunOnOwner(func() { h.g.DisconnectTick() }) + close(done) + }() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("DisconnectTick deadlocked: Hub.HardRemove's onRemove callback re-submitted to the owner from the owner") + } + + // The session was hard-removed and its character persisted. + if sessions := h.g.Hub.AllSessions(); len(sessions) != 0 { + t.Fatalf("session still in hub after disconnect tick: %d", len(sessions)) + } + if _, err := h.g.AccountStore.LoadCharacter("alice"); err != nil { + t.Fatalf("character file unreadable after disconnect save: %v", err) + } +} + +// TestHubRemoveNilPlayerOnOwner: DisconnectTick removes sessions without a +// loaded player through Hub.Remove, which runs the same onRemove callback. +func TestHubRemoveNilPlayerOnOwner(t *testing.T) { + h := newHarness(t) + sess := &net.Session{Conn: newChanConn(), State: net.StateAccountName} + sess.StartWriter() + h.hub.Add(sess) + + done := make(chan struct{}) + go func() { + h.g.RunOnOwner(func() { h.g.Hub.Remove(sess) }) + close(done) + }() + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("Hub.Remove with a nil player deadlocked on the owner") + } + if sessions := h.g.Hub.AllSessions(); len(sessions) != 0 { + t.Fatalf("player-less session still in hub: %d", len(sessions)) + } +} |
