From 36dac5d782016bb8a6817a5ef847f5d972323f41 Mon Sep 17 00:00:00 2001 From: Hugo Castro de Deco Date: Wed, 19 Aug 2026 16:03:50 -0300 Subject: [PATCH] fix: honour --reconnection-grace-time when the browser closes --reconnection-grace-time lets an operator say how long a disconnected session should be kept, but two behaviours inherited from Code stop it from delivering that. Closing the tab runs the browser workbench's shutdown, which disposes the remote connection gracefully. The server reads a graceful dispose as "the client is finished" and cleans up at once, so the grace time is never consulted -- the session dies with the tab however the flag is set. Separately, any new connection shortens every disconnected session to ProtocolConstants.ReconnectionShortGraceTime (5 minutes), so opening a second tab cuts a deliberately long grace time back down. Add patches/session-preservation.diff, which makes the configured grace time authoritative: pagehide persists UI state instead of unloading, a browser-driven unload no longer tears the workbench down (beforeunload vetoes are still honoured, and an explicit in-product shutdown still unloads), and the grace time is only shortened when it was left at or below the default, so installations that never set the flag keep Code's stock behaviour. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 11 +++ patches/series | 1 + patches/session-preservation.diff | 113 ++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 patches/session-preservation.diff diff --git a/CHANGELOG.md b/CHANGELOG.md index e7768348d00b..c9c760229c73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,17 @@ Code v99.99.999 ## Unreleased +### Fixed + +- `--reconnection-grace-time` is now honoured when the browser goes away. + Closing the tab used to dispose the connection gracefully, which the server + treats as a finished client and cleans up at once, so the grace time was never + consulted; and any new connection shortened every disconnected session to the + 5-minute short grace, so opening a second tab cut a deliberately long grace + time back down. Sessions now survive a closed browser for as long as the + configured grace time. Installations that never set the flag keep the previous + behaviour. + ## [4.133.0](https://github.com/coder/code-server/releases/tag/v4.133.0) - 2026-08-17 Code v1.133.0 diff --git a/patches/series b/patches/series index 5b548e3900dd..2acb88f6f3c7 100644 --- a/patches/series +++ b/patches/series @@ -25,3 +25,4 @@ signature-verification.diff copilot.diff app-name.diff csp-hashes.diff +session-preservation.diff diff --git a/patches/session-preservation.diff b/patches/session-preservation.diff new file mode 100644 index 000000000000..deda1a25a059 --- /dev/null +++ b/patches/session-preservation.diff @@ -0,0 +1,113 @@ +Preserve the remote session when the browser goes away. + +--reconnection-grace-time lets an operator say how long a disconnected session +should be kept. Two behaviours inherited from Code make that setting unable to +deliver on its promise: + +1. Closing the tab runs the browser workbench's shutdown, which disposes the + remote connection *gracefully*. The server reads a graceful dispose as "the + client is finished" and cleans up immediately, so the grace time is never + consulted at all -- the session dies with the tab no matter how the flag is + set. + +2. Any new connection shortens every disconnected session's grace time to + ProtocolConstants.ReconnectionShortGraceTime (5 minutes). Opening a second + tab is enough to cut a deliberately long grace time down to five minutes. + +This patch makes the configured grace time authoritative: + +- pagehide persists UI state instead of unloading, and a browser-driven unload + (tab close, navigation) no longer tears the workbench down. An explicit, + in-product shutdown still unloads normally, and beforeunload vetoes are still + honoured. + +- the grace time is only shortened when it was left at or below the default, so + installations that never touched the flag keep Code's stock behaviour. + +Index: code-server/lib/vscode/src/vs/server/node/remoteExtensionHostAgentServer.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/remoteExtensionHostAgentServer.ts ++++ code-server/lib/vscode/src/vs/server/node/remoteExtensionHostAgentServer.ts +@@ -24,7 +24,7 @@ import { generateUuid } from '../../base + import { getOSReleaseInfo } from '../../base/node/osReleaseInfo.js'; + import { findFreePort } from '../../base/node/ports.js'; + import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js'; +-import { PersistentProtocol } from '../../base/parts/ipc/common/ipc.net.js'; ++import { PersistentProtocol, ProtocolConstants } from '../../base/parts/ipc/common/ipc.net.js'; + import { NodeSocket, upgradeToISocket, WebSocketNodeSocket } from '../../base/parts/ipc/node/ipc.net.js'; + import { IConfigurationService } from '../../platform/configuration/common/configuration.js'; + import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js'; +@@ -365,13 +365,20 @@ class RemoteExtensionHostAgentServer ext + // We have received a new connection. + // This indicates that the server owner has connectivity. + // Therefore we will shorten the reconnection grace period for disconnected connections! +- for (const key in this._managementConnections) { +- const managementConnection = this._managementConnections[key]; +- managementConnection.shortenReconnectionGraceTimeIfNecessary(); +- } +- for (const key in this._extHostConnections) { +- const extHostConnection = this._extHostConnections[key]; +- extHostConnection.shortenReconnectionGraceTimeIfNecessary(); ++ // ++ // Unless the grace time was deliberately raised above the default: ++ // an operator who asks for a long grace time wants disconnected ++ // sessions to survive, and cutting them back to the short grace ++ // every time a tab is opened would make the setting meaningless. ++ if (this._reconnectionGraceTime <= ProtocolConstants.ReconnectionGraceTime) { ++ for (const key in this._managementConnections) { ++ const managementConnection = this._managementConnections[key]; ++ managementConnection.shortenReconnectionGraceTimeIfNecessary(); ++ } ++ for (const key in this._extHostConnections) { ++ const extHostConnection = this._extHostConnections[key]; ++ extHostConnection.shortenReconnectionGraceTimeIfNecessary(); ++ } + } + + state = State.Done; +Index: code-server/lib/vscode/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts ++++ code-server/lib/vscode/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts +@@ -37,12 +37,18 @@ export class BrowserLifecycleService ext + // Listen to `beforeUnload` to support to veto + this.beforeUnloadListener = addDisposableListener(mainWindow, EventType.BEFORE_UNLOAD, (e: BeforeUnloadEvent) => this.onBeforeUnload(e)); + +- // Listen to `pagehide` to support orderly shutdown ++ // Listen to `pagehide` to persist state, but do not shut down. ++ // The workbench is remote: the session lives on the server and must ++ // outlive the browser. Unloading here would dispose the connection ++ // gracefully, which the server reads as "the client is done" and cleans ++ // up immediately -- bypassing --reconnection-grace-time entirely. + // We explicitly do not listen to `unload` event + // which would disable certain browser caching. +- // We currently do not handle the `persisted` property +- // (https://github.com/microsoft/vscode/issues/136216) +- this.unloadListener = addDisposableListener(mainWindow, EventType.PAGE_HIDE, () => this.onUnload()); ++ this.unloadListener = addDisposableListener(mainWindow, EventType.PAGE_HIDE, () => { ++ this.logService.info('[lifecycle] pagehide: persisting state, preserving the remote session'); ++ ++ this.storageService.flush(WillSaveStateReason.SHUTDOWN); ++ }); + } + + private onBeforeUnload(event: BeforeUnloadEvent): void { +@@ -146,12 +152,14 @@ export class BrowserLifecycleService ext + } + }); + +- // Veto: handle if provided +- if (veto && typeof vetoShutdown === 'function') { +- return vetoShutdown(); ++ // A veto handler is only provided when the browser is driving the unload ++ // (closing the tab, navigating away). Honour a veto, but never unload: ++ // the session is on the server and has to survive the browser leaving. ++ if (typeof vetoShutdown === 'function') { ++ return veto ? vetoShutdown() : undefined; + } + +- // No veto, continue to shutdown ++ // No veto handling means an explicit, in-product shutdown: unload + return this.onUnload(); + } +