Fix: prevent mobile web remote desktop freeze on Safari app-switch

by forcing clean WS reconnect on foreground return;

add 30-second grace period on server to avoid cold-start on quick reconnects
This commit is contained in:
yuanyuanxiang
2026-07-10 15:56:32 +02:00
parent 72dcdc5a6f
commit f146af121a
5 changed files with 111 additions and 9 deletions

View File

@@ -95,6 +95,12 @@ type wsHub struct {
mu sync.RWMutex
clients map[*wsClient]struct{}
// screenCloseTimers delays CloseScreen by screenCloseGrace after the last
// viewer disconnects. A reconnect within that window cancels the timer and
// reuses the live screen sub-conn (hot path), avoiding a full cold-start.
// Guarded by mu.
screenCloseTimers map[string]*time.Timer
unsub func()
// Hardening knobs wired from server.Config. Nil/empty values mean
@@ -111,7 +117,8 @@ func newWSHub(auth *wsauth.Authenticator, devices *hub.Hub, log *logger.Logger)
auth: auth,
devices: devices,
log: log,
clients: make(map[*wsClient]struct{}),
clients: make(map[*wsClient]struct{}),
screenCloseTimers: make(map[string]*time.Timer),
}
h.unsub = devices.Subscribe(h)
return h
@@ -405,7 +412,7 @@ func (h *wsHub) unregister(c *wsClient) {
// session so the device stops encoding. Done OUTSIDE the lock so the
// hub's mutators can take their own locks without risk of recursion.
if c.watching != "" && h.countWatchers(c.watching) == 0 {
h.devices.CloseScreen(c.watching)
h.deferredCloseScreen(c.watching)
}
// Terminal sessions are single-viewer by design, so any open session
// belongs to this client. Tear it down so the next viewer doesn't
@@ -421,6 +428,30 @@ func (h *wsHub) unregister(c *wsClient) {
c.close()
}
// screenCloseGrace is how long we keep a device's screen sub-conn alive after
// the last browser viewer disconnects. A reconnect within this window (e.g.
// the user briefly switched to a TOTP app on mobile) cancels the timer and
// resumes on the hot path — no cold-start, no waiting for a new IDR.
const screenCloseGrace = 30 * time.Second
// deferredCloseScreen schedules CloseScreen after screenCloseGrace unless a
// viewer reconnects first. Must be called without h.mu held.
func (h *wsHub) deferredCloseScreen(deviceID string) {
h.mu.Lock()
if t, ok := h.screenCloseTimers[deviceID]; ok {
t.Stop()
}
h.screenCloseTimers[deviceID] = time.AfterFunc(screenCloseGrace, func() {
h.mu.Lock()
delete(h.screenCloseTimers, deviceID)
h.mu.Unlock()
if h.countWatchers(deviceID) == 0 {
h.devices.CloseScreen(deviceID)
}
})
h.mu.Unlock()
}
// ----- HTTP handler -------------------------------------------------------
func (h *wsHub) serve(w http.ResponseWriter, r *http.Request) {