fix(bridge): resolve websocket zombie connection and bounding memory leaks
This commit is contained in:
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fixLSConnection = fixLSConnection;
|
||||
exports.activate = activate;
|
||||
exports.deactivate = deactivate;
|
||||
const vscode = __importStar(require("vscode"));
|
||||
@@ -251,18 +252,24 @@ async function initSDK(context) {
|
||||
* found (wrong workspace).
|
||||
*/
|
||||
async function fixLSConnection() {
|
||||
if (!sdk?.ls)
|
||||
return;
|
||||
if (!sdk?.ls) {
|
||||
logToFile('[LS-FIX] skipped: sdk.ls not available');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const folders = vscode.workspace.workspaceFolders;
|
||||
if (!folders || folders.length === 0)
|
||||
return;
|
||||
if (!folders || folders.length === 0) {
|
||||
logToFile('[LS-FIX] skipped: no workspace folders');
|
||||
return false;
|
||||
}
|
||||
// Generate the workspace hint the same way SDK does, but we'll match case-insensitively
|
||||
const folder = folders[0].uri.fsPath;
|
||||
const parts = folder.replace(/\\/g, '/').split('/');
|
||||
const hint = parts.slice(-2).join('_').replace(/[-.\s]/g, '_').toLowerCase();
|
||||
if (!hint)
|
||||
return;
|
||||
if (!hint) {
|
||||
logToFile('[LS-FIX] skipped: empty hint');
|
||||
return false;
|
||||
}
|
||||
// Find all language_server processes with csrf_token
|
||||
const { exec } = cp;
|
||||
const { promisify } = require('util');
|
||||
@@ -274,12 +281,18 @@ async function fixLSConnection() {
|
||||
const result = await execAsync(`powershell.exe -NoProfile -EncodedCommand ${encoded}`, { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
||||
output = result.stdout;
|
||||
}
|
||||
catch {
|
||||
return; // Can't discover processes — leave SDK's choice
|
||||
catch (psErr) {
|
||||
logToFile(`[LS-FIX] skipped: PowerShell failed — ${psErr.message?.substring(0, 100)}`);
|
||||
return false;
|
||||
}
|
||||
const lines = output.split('\n').filter((l) => l.trim().length > 0);
|
||||
if (lines.length <= 1)
|
||||
return; // Only one LS — no ambiguity
|
||||
if (lines.length === 0) {
|
||||
logToFile('[LS-FIX] skipped: no LS processes found');
|
||||
return false;
|
||||
}
|
||||
// NOTE: Do NOT skip on single LS — SDK may have fallen back to wrong LS
|
||||
// due to case-sensitive hint mismatch, even when only one process exists.
|
||||
logToFile(`[LS-FIX] found ${lines.length} LS process(es), hint="${hint}"`);
|
||||
// Find the line whose workspace_id matches our workspace (case-insensitive)
|
||||
let matchedLine = null;
|
||||
for (const line of lines) {
|
||||
@@ -296,7 +309,7 @@ async function fixLSConnection() {
|
||||
}
|
||||
if (!matchedLine) {
|
||||
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// Extract port and csrf_token from matched line
|
||||
const csrfMatch = matchedLine.match(/--csrf_token[= ](\S+)/);
|
||||
@@ -304,7 +317,7 @@ async function fixLSConnection() {
|
||||
const pidMatch = matchedLine.split('|')[0]?.trim();
|
||||
if (!csrfMatch || !extPortMatch) {
|
||||
logToFile(`[LS-FIX] Matched LS but missing csrf/port args`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
const csrfToken = csrfMatch[1];
|
||||
const extPort = parseInt(extPortMatch[1], 10);
|
||||
@@ -312,7 +325,7 @@ async function fixLSConnection() {
|
||||
// Check if SDK already connected to this LS
|
||||
if (sdk.ls.port === extPort) {
|
||||
logToFile(`[LS-FIX] SDK already on correct LS port=${extPort}`);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
// Find ConnectRPC port via netstat (same as SDK logic)
|
||||
let netstatOutput;
|
||||
@@ -325,7 +338,7 @@ async function fixLSConnection() {
|
||||
logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`);
|
||||
sdk.ls.setConnection(extPort, csrfToken, false);
|
||||
logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${extPort} hint="${hint}" PID=${pid}`);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
const portMatches = netstatOutput.matchAll(/127\.0\.0\.1:(\d+)/g);
|
||||
const ports = [];
|
||||
@@ -358,7 +371,7 @@ async function fixLSConnection() {
|
||||
if (ok) {
|
||||
sdk.ls.setConnection(port, csrfToken, useTls);
|
||||
logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${port} ${proto} hint="${hint}" PID=${pid}`);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch { /* try next */ }
|
||||
@@ -367,9 +380,11 @@ async function fixLSConnection() {
|
||||
// Last resort: use extension_server_port
|
||||
sdk.ls.setConnection(extPort, csrfToken, false);
|
||||
logToFile(`[LS-FIX] ✅ Reconnected via ext_port=${extPort} hint="${hint}" PID=${pid}`);
|
||||
return true;
|
||||
}
|
||||
catch (err) {
|
||||
logToFile(`[LS-FIX] error: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// ─── Approval Observer + Product.json Checksums extracted to ./html-patcher.ts ───
|
||||
@@ -515,6 +530,7 @@ async function activate(context) {
|
||||
recentDiscordSentTexts,
|
||||
writeChatSnapshot,
|
||||
writeChatSnapshotWithFiles,
|
||||
fixLSConnection,
|
||||
});
|
||||
// Start HTTP bridge with live step-probe state (prevents stale primitive bug)
|
||||
const httpBridgeCtx = {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
||||
"name": "gravity-bridge",
|
||||
"displayName": "Gravity Bridge",
|
||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||
"version": "0.5.5",
|
||||
"version": "0.5.6",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
@@ -92,6 +92,7 @@ export function resetPendingState(): void {
|
||||
* re-detection of WAITING steps whose pending was lost during disconnect.
|
||||
*/
|
||||
export function resetPendingStateForReconnect(): void {
|
||||
if (!ctx) return; // Prevent startup race conditions
|
||||
ctx.lastPendingStepIndex = -1;
|
||||
ctx.stallProbed = false;
|
||||
ctx.sawRunningAfterPending = false;
|
||||
|
||||
@@ -122,6 +122,7 @@ export class WSBridgeClient {
|
||||
private reconnectDelay = INITIAL_RECONNECT_DELAY;
|
||||
private reconnectTimer: NodeJS.Timeout | null = null;
|
||||
private heartbeatTimer: NodeJS.Timeout | null = null;
|
||||
private pongTimeoutTimer: NodeJS.Timeout | null = null;
|
||||
private authTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
// Message queue (survives reconnection)
|
||||
@@ -245,6 +246,10 @@ export class WSBridgeClient {
|
||||
|
||||
ws.on('pong', () => {
|
||||
// Server responded to our ping — connection is alive
|
||||
if (this.pongTimeoutTimer) {
|
||||
clearTimeout(this.pongTimeoutTimer);
|
||||
this.pongTimeoutTimer = null;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// ─── Browser-style WebSocket API (.onopen / .onmessage) ───
|
||||
@@ -469,6 +474,16 @@ export class WSBridgeClient {
|
||||
// Node.js ws has .ping(), browser WebSocket doesn't
|
||||
if (typeof this.ws.ping === 'function') {
|
||||
this.ws.ping();
|
||||
|
||||
// Set timeout waiting for pong
|
||||
if (this.pongTimeoutTimer) clearTimeout(this.pongTimeoutTimer);
|
||||
this.pongTimeoutTimer = setTimeout(() => {
|
||||
this.logFn('[WS] Heartbeat timeout — no pong received, terminating connection');
|
||||
if (this.ws) {
|
||||
try { this.ws.terminate(); } catch { try { this.ws.close(); } catch { } }
|
||||
}
|
||||
this._onDisconnect();
|
||||
}, 10000); // 10s timeout
|
||||
} else {
|
||||
// Fallback: send heartbeat as JSON message
|
||||
this.ws.send(JSON.stringify({ type: 'heartbeat' }));
|
||||
@@ -485,6 +500,10 @@ export class WSBridgeClient {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
if (this.pongTimeoutTimer) {
|
||||
clearTimeout(this.pongTimeoutTimer);
|
||||
this.pongTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Reconnection ───
|
||||
@@ -540,6 +559,11 @@ export class WSBridgeClient {
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
if (this.pongTimeoutTimer) {
|
||||
clearTimeout(this.pongTimeoutTimer);
|
||||
this.pongTimeoutTimer = null;
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.close();
|
||||
|
||||
Reference in New Issue
Block a user