Skip to content

Commit 36dac5d

Browse files
hugodecoclaude
andcommitted
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 <noreply@anthropic.com>
1 parent c22dc74 commit 36dac5d

3 files changed

Lines changed: 125 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ Code v99.99.999
2222

2323
## Unreleased
2424

25+
### Fixed
26+
27+
- `--reconnection-grace-time` is now honoured when the browser goes away.
28+
Closing the tab used to dispose the connection gracefully, which the server
29+
treats as a finished client and cleans up at once, so the grace time was never
30+
consulted; and any new connection shortened every disconnected session to the
31+
5-minute short grace, so opening a second tab cut a deliberately long grace
32+
time back down. Sessions now survive a closed browser for as long as the
33+
configured grace time. Installations that never set the flag keep the previous
34+
behaviour.
35+
2536
## [4.133.0](https://github.com/coder/code-server/releases/tag/v4.133.0) - 2026-08-17
2637

2738
Code v1.133.0

patches/series

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ signature-verification.diff
2525
copilot.diff
2626
app-name.diff
2727
csp-hashes.diff
28+
session-preservation.diff

patches/session-preservation.diff

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
Preserve the remote session when the browser goes away.
2+
3+
--reconnection-grace-time lets an operator say how long a disconnected session
4+
should be kept. Two behaviours inherited from Code make that setting unable to
5+
deliver on its promise:
6+
7+
1. Closing the tab runs the browser workbench's shutdown, which disposes the
8+
remote connection *gracefully*. The server reads a graceful dispose as "the
9+
client is finished" and cleans up immediately, so the grace time is never
10+
consulted at all -- the session dies with the tab no matter how the flag is
11+
set.
12+
13+
2. Any new connection shortens every disconnected session's grace time to
14+
ProtocolConstants.ReconnectionShortGraceTime (5 minutes). Opening a second
15+
tab is enough to cut a deliberately long grace time down to five minutes.
16+
17+
This patch makes the configured grace time authoritative:
18+
19+
- pagehide persists UI state instead of unloading, and a browser-driven unload
20+
(tab close, navigation) no longer tears the workbench down. An explicit,
21+
in-product shutdown still unloads normally, and beforeunload vetoes are still
22+
honoured.
23+
24+
- the grace time is only shortened when it was left at or below the default, so
25+
installations that never touched the flag keep Code's stock behaviour.
26+
27+
Index: code-server/lib/vscode/src/vs/server/node/remoteExtensionHostAgentServer.ts
28+
===================================================================
29+
--- code-server.orig/lib/vscode/src/vs/server/node/remoteExtensionHostAgentServer.ts
30+
+++ code-server/lib/vscode/src/vs/server/node/remoteExtensionHostAgentServer.ts
31+
@@ -24,7 +24,7 @@ import { generateUuid } from '../../base
32+
import { getOSReleaseInfo } from '../../base/node/osReleaseInfo.js';
33+
import { findFreePort } from '../../base/node/ports.js';
34+
import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js';
35+
-import { PersistentProtocol } from '../../base/parts/ipc/common/ipc.net.js';
36+
+import { PersistentProtocol, ProtocolConstants } from '../../base/parts/ipc/common/ipc.net.js';
37+
import { NodeSocket, upgradeToISocket, WebSocketNodeSocket } from '../../base/parts/ipc/node/ipc.net.js';
38+
import { IConfigurationService } from '../../platform/configuration/common/configuration.js';
39+
import { IInstantiationService } from '../../platform/instantiation/common/instantiation.js';
40+
@@ -365,13 +365,20 @@ class RemoteExtensionHostAgentServer ext
41+
// We have received a new connection.
42+
// This indicates that the server owner has connectivity.
43+
// Therefore we will shorten the reconnection grace period for disconnected connections!
44+
- for (const key in this._managementConnections) {
45+
- const managementConnection = this._managementConnections[key];
46+
- managementConnection.shortenReconnectionGraceTimeIfNecessary();
47+
- }
48+
- for (const key in this._extHostConnections) {
49+
- const extHostConnection = this._extHostConnections[key];
50+
- extHostConnection.shortenReconnectionGraceTimeIfNecessary();
51+
+ //
52+
+ // Unless the grace time was deliberately raised above the default:
53+
+ // an operator who asks for a long grace time wants disconnected
54+
+ // sessions to survive, and cutting them back to the short grace
55+
+ // every time a tab is opened would make the setting meaningless.
56+
+ if (this._reconnectionGraceTime <= ProtocolConstants.ReconnectionGraceTime) {
57+
+ for (const key in this._managementConnections) {
58+
+ const managementConnection = this._managementConnections[key];
59+
+ managementConnection.shortenReconnectionGraceTimeIfNecessary();
60+
+ }
61+
+ for (const key in this._extHostConnections) {
62+
+ const extHostConnection = this._extHostConnections[key];
63+
+ extHostConnection.shortenReconnectionGraceTimeIfNecessary();
64+
+ }
65+
}
66+
67+
state = State.Done;
68+
Index: code-server/lib/vscode/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts
69+
===================================================================
70+
--- code-server.orig/lib/vscode/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts
71+
+++ code-server/lib/vscode/src/vs/workbench/services/lifecycle/browser/lifecycleService.ts
72+
@@ -37,12 +37,18 @@ export class BrowserLifecycleService ext
73+
// Listen to `beforeUnload` to support to veto
74+
this.beforeUnloadListener = addDisposableListener(mainWindow, EventType.BEFORE_UNLOAD, (e: BeforeUnloadEvent) => this.onBeforeUnload(e));
75+
76+
- // Listen to `pagehide` to support orderly shutdown
77+
+ // Listen to `pagehide` to persist state, but do not shut down.
78+
+ // The workbench is remote: the session lives on the server and must
79+
+ // outlive the browser. Unloading here would dispose the connection
80+
+ // gracefully, which the server reads as "the client is done" and cleans
81+
+ // up immediately -- bypassing --reconnection-grace-time entirely.
82+
// We explicitly do not listen to `unload` event
83+
// which would disable certain browser caching.
84+
- // We currently do not handle the `persisted` property
85+
- // (https://github.com/microsoft/vscode/issues/136216)
86+
- this.unloadListener = addDisposableListener(mainWindow, EventType.PAGE_HIDE, () => this.onUnload());
87+
+ this.unloadListener = addDisposableListener(mainWindow, EventType.PAGE_HIDE, () => {
88+
+ this.logService.info('[lifecycle] pagehide: persisting state, preserving the remote session');
89+
+
90+
+ this.storageService.flush(WillSaveStateReason.SHUTDOWN);
91+
+ });
92+
}
93+
94+
private onBeforeUnload(event: BeforeUnloadEvent): void {
95+
@@ -146,12 +152,14 @@ export class BrowserLifecycleService ext
96+
}
97+
});
98+
99+
- // Veto: handle if provided
100+
- if (veto && typeof vetoShutdown === 'function') {
101+
- return vetoShutdown();
102+
+ // A veto handler is only provided when the browser is driving the unload
103+
+ // (closing the tab, navigating away). Honour a veto, but never unload:
104+
+ // the session is on the server and has to survive the browser leaving.
105+
+ if (typeof vetoShutdown === 'function') {
106+
+ return veto ? vetoShutdown() : undefined;
107+
}
108+
109+
- // No veto, continue to shutdown
110+
+ // No veto handling means an explicit, in-product shutdown: unload
111+
return this.onUnload();
112+
}
113+

0 commit comments

Comments
 (0)