fix(extension): SDK LS 대소문자 매칭 버그 수정 — fixLSConnection() 추가 (멀티프로젝트 신호 누락 해결)

This commit is contained in:
2026-03-10 22:50:04 +09:00
parent 4d780ec5e7
commit a0d46f1ff3
6 changed files with 321 additions and 1 deletions

View File

@@ -214,6 +214,13 @@ async function initSDK(context: vscode.ExtensionContext): Promise<boolean> {
sdk = new AntigravitySDK(context);
await sdk.initialize();
console.log('Gravity Bridge: ✅ SDK initialized');
// ── FIX: SDK's _findLSProcess() uses case-sensitive String.includes() ──
// workspace_id in LS process has 'Desktop' (capital D), but SDK hint
// generates 'desktop' (lowercase) → match fails → connects to WRONG LS.
// Re-discover the correct LS using case-insensitive workspace_id matching.
await fixLSConnection();
return true;
} catch (err: any) {
console.log(`Gravity Bridge: SDK init failed: ${err.message}`);
@@ -221,6 +228,158 @@ async function initSDK(context: vscode.ExtensionContext): Promise<boolean> {
}
}
/**
* Fix SDK's LS connection by finding the correct language_server process
* for this workspace using case-insensitive matching.
*
* SDK bug: _findLSProcess() compares workspaceHint via JS String.includes()
* which is case-sensitive. workspace_id in process args has original casing
* (e.g., file_c_3A_Users_Certes_Desktop_variet_agent) but SDK hint is
* lowercased (desktop_variet_agent) → no match → falls back to first LS
* found (wrong workspace).
*/
async function fixLSConnection(): Promise<void> {
if (!sdk?.ls) return;
try {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) return;
// 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;
// Find all language_server processes with csrf_token
const { exec } = cp;
const { promisify } = require('util');
const execAsync = promisify(exec);
let output: string;
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 }
);
output = result.stdout;
} catch {
return; // Can't discover processes — leave SDK's choice
}
const lines = output.split('\n').filter((l: string) => l.trim().length > 0);
if (lines.length <= 1) return; // Only one LS — no ambiguity
// Find the line whose workspace_id matches our workspace (case-insensitive)
let matchedLine: string | null = null;
for (const line of lines) {
const lower = line.toLowerCase();
// Match workspace_id arg against our hint
const wsMatch = line.match(/--workspace_id[= ](\S+)/i);
if (wsMatch) {
const wsid = wsMatch[1].toLowerCase();
if (wsid.includes(hint)) {
matchedLine = line;
break;
}
}
}
if (!matchedLine) {
logToFile(`[LS-FIX] No LS process matched hint="${hint}" (${lines.length} processes)`);
return;
}
// Extract port and csrf_token from matched line
const csrfMatch = matchedLine.match(/--csrf_token[= ](\S+)/);
const extPortMatch = matchedLine.match(/--extension_server_port[= ](\d+)/);
const pidMatch = matchedLine.split('|')[0]?.trim();
if (!csrfMatch || !extPortMatch) {
logToFile(`[LS-FIX] Matched LS but missing csrf/port args`);
return;
}
const csrfToken = csrfMatch[1];
const extPort = parseInt(extPortMatch[1], 10);
const pid = parseInt(pidMatch || '0', 10);
// Check if SDK already connected to this LS
if (sdk.ls.port === extPort) {
logToFile(`[LS-FIX] SDK already on correct LS port=${extPort}`);
return;
}
// Find ConnectRPC port via netstat (same as SDK logic)
let netstatOutput: string;
try {
const result = await execAsync(
`netstat -aon | findstr "LISTENING" | findstr "${pid}"`,
{ encoding: 'utf8', timeout: 5000, windowsHide: true }
);
netstatOutput = result.stdout;
} catch {
// Netstat failed — try extension_server_port as fallback
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;
}
const portMatches = netstatOutput.matchAll(/127\.0\.0\.1:(\d+)/g);
const ports: number[] = [];
for (const m of portMatches) {
const p = parseInt(m[1], 10);
if (p !== extPort && !ports.includes(p)) {
ports.push(p);
}
}
// Try each port — prefer HTTPS, fall back to HTTP
const httpModule = require('http');
const httpsModule = require('https');
for (const useTls of [true, false]) {
const mod = useTls ? httpsModule : httpModule;
const proto = useTls ? 'https' : 'http';
for (const port of ports) {
try {
const ok = await new Promise<boolean>((resolve) => {
const req = mod.request(
`${proto}://127.0.0.1:${port}/exa.language_server_pb.LanguageServerService/GetUserStatus`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': 2 },
rejectUnauthorized: false,
timeout: 2000,
},
(res: any) => resolve(res.statusCode === 200 || res.statusCode === 401)
);
req.on('error', () => resolve(false));
req.on('timeout', () => { req.destroy(); resolve(false); });
req.write('{}');
req.end();
});
if (ok) {
sdk.ls.setConnection(port, csrfToken, useTls);
logToFile(`[LS-FIX] ✅ Reconnected to correct LS: port=${port} ${proto} hint="${hint}" PID=${pid}`);
return;
}
} catch { /* try next */ }
}
}
// Last resort: use extension_server_port
sdk.ls.setConnection(extPort, csrfToken, false);
logToFile(`[LS-FIX] ✅ Reconnected via ext_port=${extPort} hint="${hint}" PID=${pid}`);
} catch (err: any) {
logToFile(`[LS-FIX] error: ${err.message}`);
}
}
// ─── Approval Observer via SDK IntegrationManager ───
async function setupApprovalObserver() {