Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 111 additions & 13 deletions src/lib/usage-limits.js
Original file line number Diff line number Diff line change
Expand Up @@ -2167,10 +2167,19 @@ async function detectAntigravityProcess({
);
processes = parseWindowsProcesses(result?.stdout);
} else {
const result = await runCommand(commandRunner, "/bin/ps", ["-ax", "-o", "pid=,command="], {
let result = await runCommand(commandRunner, "/bin/ps", ["-ax", "-o", "pid=,command="], {
timeout: timeoutMs,
signal,
});
const isSpawnFailure = result?.error
&& result.error.code !== "ETIMEDOUT"
&& result.error.name !== "AbortError";
if (isSpawnFailure && !result?.stdout) {
result = await runCommand(commandRunner, "ps", ["-ax", "-o", "pid=,command="], {
timeout: timeoutMs,
signal,
});
}
processes = String(result?.stdout || "")
.split("\n")
.map(parseProcessLine)
Expand Down Expand Up @@ -2633,10 +2642,70 @@ function parseWindowsListeningPorts(output, pid) {
return Array.from(ports).sort((a, b) => a - b);
}

function parseLinuxProcListeningPorts(pid, { procRoot = "/proc" } = {}) {
const numPid = Number(pid);
if (!Number.isFinite(numPid) || numPid <= 0) return [];
const inodes = new Set();
try {
const fds = fs.readdirSync(path.join(procRoot, String(numPid), "fd"));
for (const fd of fds) {
try {
const link = fs.readlinkSync(path.join(procRoot, String(numPid), "fd", fd));
const m = link.match(/^socket:\[(\d+)\]$/);
if (m) inodes.add(m[1]);
} catch {}
}
} catch {
return [];
}
if (inodes.size === 0) return [];

const ports = new Set();
for (const table of [path.join(procRoot, "net", "tcp"), path.join(procRoot, "net", "tcp6")]) {
try {
const content = fs.readFileSync(table, "utf8");
for (const line of content.split("\n")) {
const parts = line.trim().split(/\s+/);
if (parts.length > 9 && parts[3] === "0A") {
const inode = parts[9];
if (inodes.has(inode)) {
const portHex = parts[1]?.split(":")[1];
if (portHex) {
const port = parseInt(portHex, 16);
if (Number.isInteger(port) && port > 0 && port <= 65535) {
ports.add(port);
}
}
}
}
}
} catch {}
}
return Array.from(ports).sort((a, b) => a - b);
}

function parseSsListeningPorts(output, pid) {
const ports = new Set();
const pidPattern = pid ? new RegExp(`\\bpid=${pid}\\b`) : null;
for (const line of String(output || "").split("\n")) {
if (!line.includes("LISTEN")) continue;
if (pidPattern && !pidPattern.test(line)) continue;
const match = line.match(/:(\d+)\s+/);
if (match) {
const port = Number(match[1]);
if (Number.isInteger(port) && port > 0 && port <= 65535) {
ports.add(port);
}
}
}
return Array.from(ports).sort((a, b) => a - b);
}

async function listAntigravityPorts(pid, {
commandRunner,
platform = process.platform,
timeoutMs = ANTIGRAVITY_PROCESS_SCAN_TIMEOUT_MS,
procRoot = "/proc",
signal,
} = {}) {
if (platform === "win32") {
Expand All @@ -2652,21 +2721,48 @@ async function listAntigravityPorts(pid, {
}
return ports;
}

// On Linux, try procfs first when no mock command runner is provided (zero-spawn, no dependencies).
if (platform === "linux" && !commandRunner) {
const procPorts = parseLinuxProcListeningPorts(pid, { procRoot });
if (procPorts.length > 0) return procPorts;
}

const lsof = await resolveLsofBinary({ commandRunner });
if (!lsof) {
throw new Error("Antigravity port detection needs lsof. Install it, then retry.");
if (lsof) {
const result = await runCommand(
commandRunner,
lsof,
["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", String(pid)],
{ timeout: timeoutMs, signal },
);
const ports = parseListeningPorts(result?.stdout);
if (ports.length > 0) return ports;
}
const result = await runCommand(
commandRunner,
lsof,
["-nP", "-iTCP", "-sTCP:LISTEN", "-a", "-p", String(pid)],
{ timeout: timeoutMs, signal },
);
const ports = parseListeningPorts(result?.stdout);
if (!ports.length) {
throw new Error("Antigravity is running but not exposing ports yet. Try again in a few seconds.");

// On Linux, fall back to procfs (honoring procRoot) or ss if lsof is absent or yielded no ports.
if (platform === "linux") {
const procPorts = parseLinuxProcListeningPorts(pid, { procRoot });
if (procPorts.length > 0) return procPorts;

const ss = await whichBinary("ss", { commandRunner });
if (ss) {
const result = await runCommand(
commandRunner,
ss,
["-H", "-tlpn"],
{ timeout: timeoutMs, signal },
);
const ssPorts = parseSsListeningPorts(result?.stdout, pid);
if (ssPorts.length > 0) return ssPorts;
}
}

if (!lsof && platform !== "linux") {
throw new Error("Antigravity port detection needs lsof. Install it, then retry.");
}
return ports;

throw new Error("Antigravity is running but not exposing ports yet. Try again in a few seconds.");
}

function antigravityDefaultBody() {
Expand Down Expand Up @@ -3969,6 +4065,8 @@ module.exports = {
loadAntigravityCredentials,
parseListeningPorts,
parseWindowsListeningPorts,
parseLinuxProcListeningPorts,
parseSsListeningPorts,
listAntigravityPorts,
detectAntigravityProcess,
fetchAntigravityLimits,
Expand Down
116 changes: 116 additions & 0 deletions test/usage-limits.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const {
loadAntigravityCredentials,
parseListeningPorts,
parseWindowsListeningPorts,
parseLinuxProcListeningPorts,
parseSsListeningPorts,
listAntigravityPorts,
detectAntigravityProcess,
fetchAntigravityLimits,
Expand Down Expand Up @@ -3455,6 +3457,120 @@ lang 123 me 23u IPv4 0x124 0t0 TCP 127.0.0.1:51235 (LIS
assert.equal(result.configured, false);
});

it("falls back to ps when /bin/ps is missing or returns error", async () => {
const calls = [];
const commandRunner = (command, args) => {
calls.push({ command, args });
if (command === "/bin/ps") {
return { status: null, stdout: "", stderr: "", error: new Error("spawn /bin/ps ENOENT") };
}
if (command === "ps") {
return {
status: 0,
stdout: "\n 456 agy\n",
};
}
return { status: 1, stdout: "", stderr: "" };
};

const result = await detectAntigravityProcess({ commandRunner });
assert.equal(calls[0].command, "/bin/ps");
assert.equal(calls[1].command, "ps");
assert.equal(result.configured, true);
assert.equal(result.pid, 456);
});

it("discovers listening ports via Linux procfs", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tokentracker-procfs-test-"));
try {
const pidDir = path.join(tmp, "456", "fd");
const netDir = path.join(tmp, "net");
fs.mkdirSync(pidDir, { recursive: true });
fs.mkdirSync(netDir, { recursive: true });
fs.symlinkSync("socket:[123456]", path.join(pidDir, "12"));
fs.writeFileSync(
path.join(netDir, "tcp"),
[
" sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode",
" 0: 0100007F:8A11 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 123456 1 00000000 100 0 0 10 0",
].join("\n"),
"utf8",
);

const ports = parseLinuxProcListeningPorts(456, { procRoot: tmp });
assert.deepEqual(ports, [35345]);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});

it("does not retry ps on timeout or abort errors", async () => {
const calls = [];
const timeoutError = new Error("spawn /bin/ps ETIMEDOUT");
timeoutError.code = "ETIMEDOUT";

const commandRunner = (command, args) => {
calls.push({ command, args });
return { status: null, stdout: "", stderr: "", error: timeoutError };
};

const result = await detectAntigravityProcess({ commandRunner });
assert.equal(calls.length, 1);
assert.equal(calls[0].command, "/bin/ps");
assert.equal(result.configured, false);
});

it("parses listening ports from ss command output", () => {
const output = [
"State Recv-Q Send-Q Local Address:Port Peer Address:Port Process",
'LISTEN 0 4096 127.0.0.1:35345 0.0.0.0:* users:(("agy",pid=456,fd=14))',
'LISTEN 0 4096 127.0.0.1:32919 0.0.0.0:* users:(("agy",pid=456,fd=12))',
'LISTEN 0 4096 127.0.0.1:9999 0.0.0.0:* users:(("other",pid=789,fd=3))',
].join("\n");

const ports = parseSsListeningPorts(output, 456);
assert.deepEqual(ports, [32919, 35345]);
});

it("exercises listAntigravityPorts Linux fallback chain (procfs miss -> lsof miss -> ss hit)", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tokentracker-listports-fallback-"));
try {
const calls = [];
const commandRunner = (command, args) => {
calls.push({ command, args });
if (command === "which") {
if (args[0] === "lsof") return { status: 1, stdout: "", stderr: "" };
if (args[0] === "ss") return { status: 0, stdout: "/usr/bin/ss\n", stderr: "" };
}
if (command === "/usr/bin/ss" || command === "ss") {
return {
status: 0,
stdout: [
"State Recv-Q Send-Q Local Address:Port Peer Address:Port Process",
'LISTEN 0 4096 127.0.0.1:35345 0.0.0.0:* users:(("agy",pid=456,fd=14))',
'LISTEN 0 4096 127.0.0.1:32919 0.0.0.0:* users:(("agy",pid=456,fd=12))',
].join("\n"),
stderr: "",
};
}
return { status: 1, stdout: "", stderr: "" };
};

const ports = await listAntigravityPorts(456, {
commandRunner,
platform: "linux",
procRoot: tmp, // procfs miss
});

assert.deepEqual(ports, [32919, 35345]);
assert.ok(calls.some((c) => (c.command === "which" && c.args?.[0] === "lsof") || String(c.command).endsWith("lsof")));
assert.ok(calls.some((c) => c.command === "which" && c.args?.[0] === "ss"));
assert.ok(calls.some((c) => c.command === "/usr/bin/ss" || c.command === "ss"));
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});

it("persists live Antigravity quota for use after the process exits", async () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tokentracker-antigravity-cache-write-"));
try {
Expand Down