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:
@@ -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) {
|
||||
|
||||
@@ -244,7 +244,7 @@ func (h *wsHub) handleDisconnect(c *wsClient, _ []byte) {
|
||||
h.mu.Unlock()
|
||||
c.queue([]byte(`{"cmd":"disconnect_result","ok":true}`))
|
||||
if prev != "" && h.countWatchers(prev) == 0 {
|
||||
h.devices.CloseScreen(prev)
|
||||
h.deferredCloseScreen(prev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,11 @@ func (h *wsHub) handleConnect(c *wsClient, raw []byte) {
|
||||
c.queueBinary(cache.Keyframe)
|
||||
}
|
||||
h.mu.Lock()
|
||||
// Cancel any pending deferred close so the relay stays alive.
|
||||
if t, ok := h.screenCloseTimers[in.ID]; ok {
|
||||
t.Stop()
|
||||
delete(h.screenCloseTimers, in.ID)
|
||||
}
|
||||
c.watching = in.ID
|
||||
h.mu.Unlock()
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user