fix(ext): v0.5.8 false positive zombie socket disconnect bug resolve (timestamp replace setTimeout)
This commit is contained in:
@@ -106,8 +106,8 @@ function detectProjectName() {
|
||||
const cwd = folders[0].uri.fsPath;
|
||||
try {
|
||||
const remoteUrl = cp.execSync('git remote get-url origin', {
|
||||
cwd, encoding: 'utf-8', timeout: 3000
|
||||
}).trim();
|
||||
cwd, encoding: 'utf-8', timeout: 2000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).toString().trim();
|
||||
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/);
|
||||
if (match && match[1]) {
|
||||
return match[1].toLowerCase().replace(/[\s\-]+/g, '_');
|
||||
@@ -278,7 +278,7 @@ async function fixLSConnection() {
|
||||
try {
|
||||
const psScript = `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'language_server' -and $_.CommandLine -match 'csrf_token' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }`;
|
||||
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
||||
const result = await execAsync(`powershell.exe -NoProfile -EncodedCommand ${encoded}`, { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
||||
const result = await execAsync(`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand ${encoded}`, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
||||
output = result.stdout;
|
||||
}
|
||||
catch (psErr) {
|
||||
@@ -330,12 +330,14 @@ async function fixLSConnection() {
|
||||
// Find ConnectRPC port via netstat (same as SDK logic)
|
||||
let netstatOutput;
|
||||
try {
|
||||
const result = await execAsync(`netstat -aon | findstr "LISTENING" | findstr "${pid}"`, { encoding: 'utf8', timeout: 5000, windowsHide: true });
|
||||
netstatOutput = result.stdout;
|
||||
const result = await execAsync(`netstat -aon`, { encoding: 'utf8', timeout: 4000, windowsHide: true });
|
||||
netstatOutput = result.stdout.split('\n')
|
||||
.filter((l) => l.includes('LISTENING') && l.includes(pid.toString()))
|
||||
.join('\n');
|
||||
}
|
||||
catch {
|
||||
catch (err) {
|
||||
// Netstat failed — try extension_server_port as fallback
|
||||
logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`);
|
||||
logToFile(`[LS-FIX] netstat failed: ${err.message.substring(0, 80)}, 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 true;
|
||||
@@ -540,14 +542,25 @@ async function activate(context) {
|
||||
get lastPendingStepIndex() { return (0, step_probe_1.getStepProbeContext)().lastPendingStepIndex; },
|
||||
};
|
||||
const bridgePort = await (0, http_bridge_1.startHttpBridge)(httpBridgeCtx, sdk);
|
||||
let localPort = bridgePort;
|
||||
if (bridgePort) {
|
||||
await (0, html_patcher_1.setupApprovalObserver)(sdk, bridgePort, logToFile);
|
||||
try {
|
||||
const externalUri = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${bridgePort}`));
|
||||
const match = externalUri.authority.match(/:(\d+)$/);
|
||||
if (match) {
|
||||
localPort = parseInt(match[1], 10);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
logToFile(`[OBSERVER] asExternalUri failed: ${e.message}`);
|
||||
}
|
||||
await (0, html_patcher_1.setupApprovalObserver)(sdk, localPort, logToFile);
|
||||
}
|
||||
else {
|
||||
logToFile('[OBSERVER] HTTP bridge failed — skipping observer setup');
|
||||
}
|
||||
statusBar.text = '$(check) Bridge';
|
||||
statusBar.tooltip = `Gravity Bridge: ${projectName} (POLL + Observer active)`;
|
||||
statusBar.tooltip = `Gravity Bridge Control | port:${localPort} | project:${projectName}`;
|
||||
// Register SDK-powered commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('gravityBridge.approve', async () => {
|
||||
try {
|
||||
|
||||
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.6",
|
||||
"version": "0.5.8",
|
||||
"publisher": "variet",
|
||||
"engines": {
|
||||
"vscode": "^1.100.0"
|
||||
|
||||
@@ -71,8 +71,8 @@ function detectProjectName(): string {
|
||||
const cwd = folders[0].uri.fsPath;
|
||||
try {
|
||||
const remoteUrl = cp.execSync('git remote get-url origin', {
|
||||
cwd, encoding: 'utf-8', timeout: 3000
|
||||
}).trim();
|
||||
cwd, encoding: 'utf-8', timeout: 2000, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore']
|
||||
}).toString().trim();
|
||||
const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/);
|
||||
if (match && match[1]) {
|
||||
return match[1].toLowerCase().replace(/[\s\-]+/g, '_');
|
||||
@@ -233,8 +233,8 @@ export async function fixLSConnection(): Promise<boolean> {
|
||||
const psScript = `Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match 'language_server' -and $_.CommandLine -match 'csrf_token' } | ForEach-Object { $_.ProcessId.ToString() + '|' + $_.CommandLine }`;
|
||||
const encoded = Buffer.from(psScript, 'utf16le').toString('base64');
|
||||
const result = await execAsync(
|
||||
`powershell.exe -NoProfile -EncodedCommand ${encoded}`,
|
||||
{ encoding: 'utf8', timeout: 15000, windowsHide: true }
|
||||
`powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -EncodedCommand ${encoded}`,
|
||||
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
||||
);
|
||||
output = result.stdout;
|
||||
} catch (psErr: any) {
|
||||
@@ -292,13 +292,15 @@ export async function fixLSConnection(): Promise<boolean> {
|
||||
let netstatOutput: string;
|
||||
try {
|
||||
const result = await execAsync(
|
||||
`netstat -aon | findstr "LISTENING" | findstr "${pid}"`,
|
||||
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
|
||||
`netstat -aon`,
|
||||
{ encoding: 'utf8', timeout: 4000, windowsHide: true }
|
||||
);
|
||||
netstatOutput = result.stdout;
|
||||
} catch {
|
||||
netstatOutput = result.stdout.split('\n')
|
||||
.filter((l: string) => l.includes('LISTENING') && l.includes(pid.toString()))
|
||||
.join('\n');
|
||||
} catch (err: any) {
|
||||
// Netstat failed — try extension_server_port as fallback
|
||||
logToFile(`[LS-FIX] netstat failed, using ext_port=${extPort} for PID=${pid}`);
|
||||
logToFile(`[LS-FIX] netstat failed: ${err.message.substring(0, 80)}, 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 true;
|
||||
@@ -526,13 +528,21 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
get lastPendingStepIndex() { return getStepProbeContext().lastPendingStepIndex; },
|
||||
};
|
||||
const bridgePort = await startHttpBridge(httpBridgeCtx, sdk);
|
||||
let localPort = bridgePort;
|
||||
if (bridgePort) {
|
||||
await setupApprovalObserver(sdk, bridgePort, logToFile);
|
||||
try {
|
||||
const externalUri = await vscode.env.asExternalUri(vscode.Uri.parse(`http://127.0.0.1:${bridgePort}`));
|
||||
const match = externalUri.authority.match(/:(\d+)$/);
|
||||
if (match) { localPort = parseInt(match[1], 10); }
|
||||
} catch (e: any) {
|
||||
logToFile(`[OBSERVER] asExternalUri failed: ${e.message}`);
|
||||
}
|
||||
await setupApprovalObserver(sdk, localPort, logToFile);
|
||||
} else {
|
||||
logToFile('[OBSERVER] HTTP bridge failed — skipping observer setup');
|
||||
}
|
||||
statusBar.text = '$(check) Bridge';
|
||||
statusBar.tooltip = `Gravity Bridge: ${projectName} (POLL + Observer active)`;
|
||||
statusBar.tooltip = `Gravity Bridge Control | port:${localPort} | project:${projectName}`;
|
||||
|
||||
// Register SDK-powered commands
|
||||
context.subscriptions.push(
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
*/
|
||||
|
||||
export function generateApprovalObserverScript(_port: number): string {
|
||||
// Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge
|
||||
return `
|
||||
// Port is hardcoded as fallback, but renderer also reads ag-bridge-ports.json for multi-bridge
|
||||
return `
|
||||
// ── Gravity Bridge v3: Approval Observer (deep DOM traversal — iframes, webviews, shadow DOMs) ──
|
||||
(function(){
|
||||
'use strict';
|
||||
@@ -217,20 +217,36 @@ export function generateApprovalObserverScript(_port: number): string {
|
||||
}
|
||||
|
||||
function discoverPort(cb){
|
||||
log('Trying hardcoded port '+HARDCODED_PORT+'...');
|
||||
tryPingAsync(HARDCODED_PORT).then(function(ok){
|
||||
if(ok){log('Port discovered (hardcoded): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||
log('Hardcoded port failed, retrying with backoff...');
|
||||
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||
tryPingAsync(HARDCODED_PORT).then(function(ok2){
|
||||
if(ok2){clearInterval(timer);log('Port discovered (retry #'+attempts+'): '+HARDCODED_PORT);cb(HARDCODED_PORT);}
|
||||
log('Waiting for Gravity Bridge status bar item to appear in DOM...');
|
||||
var attempts=0;
|
||||
var timer=setInterval(function(){
|
||||
attempts++;
|
||||
// Search for our specific port injected by the extension host for THIS window.
|
||||
// This prevents cross-project leakage by ignoring the hardcoded port from the shared HTML file.
|
||||
var items = document.querySelectorAll('[aria-label^="Gravity Bridge Control"], [title^="Gravity Bridge Control"]');
|
||||
if (items.length > 0) {
|
||||
var text = items[0].getAttribute('aria-label') || items[0].getAttribute('title') || '';
|
||||
var m = text.match(/port:(\\d+)/);
|
||||
if (m && m[1]) {
|
||||
var domPort = parseInt(m[1], 10);
|
||||
log('Determined correct window port from DOM: ' + domPort);
|
||||
clearInterval(timer);
|
||||
tryPingAsync(domPort).then(function(ok){
|
||||
if(ok){ cb(domPort); } else { log('Ping failed on DOM port ' + domPort); cb(HARDCODED_PORT); }
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback if status bar never appears
|
||||
if(attempts>150){
|
||||
clearInterval(timer);
|
||||
log('DOM discovery timeout after 5 min. Falling back to hardcoded.');
|
||||
tryPingAsync(HARDCODED_PORT).then(function(ok){
|
||||
if(ok){ cb(HARDCODED_PORT); }
|
||||
});
|
||||
},2000);
|
||||
});
|
||||
}
|
||||
},2000);
|
||||
}
|
||||
|
||||
discoverPort(function(port){
|
||||
|
||||
@@ -122,8 +122,8 @@ 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;
|
||||
private lastPongTime: number = 0;
|
||||
|
||||
// Message queue (survives reconnection)
|
||||
private messageQueue: WSMessage[] = [];
|
||||
@@ -240,16 +240,9 @@ export class WSBridgeClient {
|
||||
this._onDisconnect();
|
||||
});
|
||||
|
||||
ws.on('error', (err: Error) => {
|
||||
this.logFn(`[WS] Error: ${err.message}`);
|
||||
});
|
||||
|
||||
ws.on('pong', () => {
|
||||
// Server responded to our ping — connection is alive
|
||||
if (this.pongTimeoutTimer) {
|
||||
clearTimeout(this.pongTimeoutTimer);
|
||||
this.pongTimeoutTimer = null;
|
||||
}
|
||||
this.lastPongTime = Date.now();
|
||||
});
|
||||
} else {
|
||||
// ─── Browser-style WebSocket API (.onopen / .onmessage) ───
|
||||
@@ -348,6 +341,7 @@ export class WSBridgeClient {
|
||||
this.instanceNumber = authOk.instance_number;
|
||||
this.sessionToken = authOk.session_token;
|
||||
this.reconnectDelay = INITIAL_RECONNECT_DELAY;
|
||||
this.lastPongTime = Date.now(); // Reset pong timer on auth
|
||||
|
||||
if (this.authTimer) {
|
||||
clearTimeout(this.authTimer);
|
||||
@@ -470,20 +464,20 @@ export class WSBridgeClient {
|
||||
this._stopHeartbeat();
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.ws && this.connected) {
|
||||
// Check for zombie connection (no pong for 60s)
|
||||
if (Date.now() - this.lastPongTime > 60000) {
|
||||
this.logFn('[WS] Heartbeat timeout — no pong received for 60s (zombie connection), terminating');
|
||||
if (this.ws) {
|
||||
try { this.ws.terminate(); } catch { try { this.ws.close(); } catch { } }
|
||||
}
|
||||
this._onDisconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 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' }));
|
||||
@@ -500,10 +494,6 @@ export class WSBridgeClient {
|
||||
clearInterval(this.heartbeatTimer);
|
||||
this.heartbeatTimer = null;
|
||||
}
|
||||
if (this.pongTimeoutTimer) {
|
||||
clearTimeout(this.pongTimeoutTimer);
|
||||
this.pongTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Reconnection ───
|
||||
@@ -559,11 +549,6 @@ 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