244 lines
8.1 KiB
JavaScript
244 lines
8.1 KiB
JavaScript
const http = require('http');
|
|
const https = require('https');
|
|
const os = require('os');
|
|
const fs = require('fs');
|
|
const {
|
|
extractArgument,
|
|
listLanguageServerProcesses,
|
|
runCommand,
|
|
} = require('./language-server-process-utils');
|
|
|
|
const API_PATH = '/exa.language_server_pb.LanguageServerService/GetUserStatus';
|
|
const REQUEST_TIMEOUT_MS = 3000;
|
|
|
|
const requestBody = JSON.stringify({
|
|
metadata: {
|
|
ideName: 'antigravity',
|
|
extensionName: 'antigravity',
|
|
locale: 'en',
|
|
},
|
|
});
|
|
|
|
function printHelp() {
|
|
console.log(`Usage: node scripts/debug/fetch-real-quota.js
|
|
|
|
Discover a running Antigravity Language Server and fetch GetUserStatus.
|
|
The complete real response is printed.`);
|
|
}
|
|
|
|
function belongsToProcess(line, pid) {
|
|
if (os.platform() === 'win32') {
|
|
return new RegExp(`\\s${pid}\\s*$`).test(line);
|
|
}
|
|
if (os.platform() === 'darwin') {
|
|
const columns = line.trim().split(/\s+/);
|
|
return Number(columns[1]) === pid;
|
|
}
|
|
return new RegExp(`(?:pid=${pid}[,)]|\\s${pid}/)`).test(line);
|
|
}
|
|
|
|
function extractListeningPorts(stdout, pid) {
|
|
const ports = [];
|
|
for (const line of stdout.split(/\r?\n/)) {
|
|
if (!/(LISTEN|ABH)/i.test(line) || !belongsToProcess(line, pid)) continue;
|
|
for (const match of line.matchAll(/(?:127\.0\.0\.1|\[?::1\]?):(\d{2,5})/g)) {
|
|
const port = Number(match[1]);
|
|
if (port > 0 && port < 65536) ports.push(port);
|
|
}
|
|
}
|
|
return ports;
|
|
}
|
|
|
|
async function discoverProcListeningPorts(pid) {
|
|
let fdNames;
|
|
try {
|
|
fdNames = await fs.promises.readdir(`/proc/${pid}/fd`);
|
|
} catch {
|
|
return [];
|
|
}
|
|
|
|
const socketInodes = new Set();
|
|
await Promise.all(fdNames.map(async fdName => {
|
|
try {
|
|
const target = await fs.promises.readlink(`/proc/${pid}/fd/${fdName}`);
|
|
const match = target.match(/^socket:\[(\d+)\]$/);
|
|
if (match) socketInodes.add(match[1]);
|
|
} catch {
|
|
// File descriptors can disappear while the process is running.
|
|
}
|
|
}));
|
|
if (socketInodes.size === 0) return [];
|
|
|
|
const ports = [];
|
|
for (const tablePath of ['/proc/net/tcp', '/proc/net/tcp6']) {
|
|
let table;
|
|
try {
|
|
table = await fs.promises.readFile(tablePath, 'utf8');
|
|
} catch {
|
|
continue;
|
|
}
|
|
|
|
for (const row of table.split(/\r?\n/).slice(1)) {
|
|
const columns = row.trim().split(/\s+/);
|
|
if (columns.length < 10 || columns[3] !== '0A' || !socketInodes.has(columns[9])) continue;
|
|
|
|
const [address, portHex] = columns[1].split(':');
|
|
const isIpv4Loopback = address === '0100007F';
|
|
const isIpv6Loopback = address === '00000000000000000000000001000000';
|
|
if (!portHex || (!isIpv4Loopback && !isIpv6Loopback)) continue;
|
|
|
|
const port = Number.parseInt(portHex, 16);
|
|
if (port > 0 && port < 65536) ports.push(port);
|
|
}
|
|
}
|
|
return ports;
|
|
}
|
|
|
|
async function discoverListeningPorts(pid) {
|
|
let ports = [];
|
|
if (os.platform() === 'win32') {
|
|
const result = await runCommand('netstat -ano');
|
|
ports = extractListeningPorts(result.stdout, pid);
|
|
} else if (os.platform() === 'darwin') {
|
|
const result = await runCommand('lsof -nP -iTCP -sTCP:LISTEN');
|
|
ports = extractListeningPorts(result.stdout, pid);
|
|
} else {
|
|
const ssResult = await runCommand('ss -tlnp');
|
|
ports = extractListeningPorts(ssResult.stdout, pid);
|
|
if (ports.length === 0) {
|
|
const netstatResult = await runCommand('netstat -tlnp');
|
|
ports = extractListeningPorts(netstatResult.stdout, pid);
|
|
}
|
|
if (ports.length === 0) {
|
|
ports = await discoverProcListeningPorts(pid);
|
|
}
|
|
}
|
|
|
|
return [...new Set(ports)].sort((a, b) => a - b);
|
|
}
|
|
|
|
function tryPort(port, csrfToken, protocol) {
|
|
return new Promise(resolve => {
|
|
const client = protocol === 'https' ? https : http;
|
|
const request = client.request({
|
|
hostname: '127.0.0.1',
|
|
port,
|
|
path: API_PATH,
|
|
method: 'POST',
|
|
...(protocol === 'https' ? { rejectUnauthorized: false } : {}),
|
|
headers: {
|
|
'X-Codeium-Csrf-Token': csrfToken,
|
|
'Connect-Protocol-Version': '1',
|
|
'Content-Type': 'application/json',
|
|
'Content-Length': Buffer.byteLength(requestBody),
|
|
},
|
|
timeout: REQUEST_TIMEOUT_MS,
|
|
}, response => {
|
|
let body = '';
|
|
response.setEncoding('utf8');
|
|
response.on('data', chunk => {
|
|
body += chunk;
|
|
});
|
|
response.on('end', () => {
|
|
resolve({ port, statusCode: response.statusCode || 0, body });
|
|
});
|
|
});
|
|
|
|
request.on('error', () => resolve(null));
|
|
request.on('timeout', () => {
|
|
request.destroy();
|
|
resolve(null);
|
|
});
|
|
request.end(requestBody);
|
|
});
|
|
}
|
|
|
|
function parseResponse(body) {
|
|
const trimmed = body.trim();
|
|
if (!trimmed) return [];
|
|
|
|
try {
|
|
return [JSON.parse(trimmed)];
|
|
} catch {
|
|
// Streaming responses may contain newline-delimited JSON or framing lines.
|
|
}
|
|
|
|
const values = [];
|
|
for (const line of trimmed.split(/\r?\n/).filter(Boolean)) {
|
|
try {
|
|
values.push(JSON.parse(line));
|
|
} catch {
|
|
// Streaming responses can include non-JSON framing lines.
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
async function main() {
|
|
if (process.argv.includes('--help') || process.argv.includes('-h')) {
|
|
printHelp();
|
|
return;
|
|
}
|
|
|
|
console.log('Fetch real Antigravity quota');
|
|
console.log(`Platform: ${os.platform()} (${os.arch()})`);
|
|
|
|
const processes = await listLanguageServerProcesses();
|
|
const processInfo = processes.find(item => extractArgument(item.commandLine, 'csrf_token'));
|
|
if (!processInfo) {
|
|
throw new Error('No language_server process with --csrf_token was found.');
|
|
}
|
|
|
|
const csrfToken = extractArgument(processInfo.commandLine, 'csrf_token');
|
|
const extensionPort = Number(extractArgument(processInfo.commandLine, 'extension_server_port')) || null;
|
|
console.log(`Process: PID ${processInfo.pid}, token ${csrfToken}`);
|
|
|
|
const listeningPorts = await discoverListeningPorts(processInfo.pid);
|
|
const candidates = listeningPorts.filter(port => port !== extensionPort);
|
|
console.log(`Candidate localhost ports: ${candidates.join(', ') || 'none'}`);
|
|
if (candidates.length === 0) {
|
|
throw new Error('No candidate localhost listening ports were found.');
|
|
}
|
|
|
|
for (const port of candidates) {
|
|
for (const protocol of ['https', 'http']) {
|
|
process.stdout.write(`Trying ${protocol}://127.0.0.1:${port}... `);
|
|
const result = await tryPort(port, csrfToken, protocol);
|
|
if (!result) {
|
|
console.log('no response');
|
|
continue;
|
|
}
|
|
if (result.statusCode === 401 || result.statusCode === 403) {
|
|
console.log(`HTTP ${result.statusCode}, token rejected`);
|
|
continue;
|
|
}
|
|
if (result.statusCode < 200 || result.statusCode >= 300) {
|
|
console.log(`HTTP ${result.statusCode}`);
|
|
continue;
|
|
}
|
|
|
|
console.log(`connected (${protocol.toUpperCase()}, HTTP ${result.statusCode})`);
|
|
const parsed = parseResponse(result.body);
|
|
if (parsed.length === 0) {
|
|
throw new Error(`Port ${port} returned no parseable JSON response.`);
|
|
}
|
|
console.log('\nLanguage Server response:');
|
|
console.log(JSON.stringify(parsed.length === 1 ? parsed[0] : parsed, null, 2));
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new Error('No candidate port accepted the Language Server request.');
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch(error => {
|
|
console.error(`Quota fetch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
discoverListeningPorts,
|
|
};
|