chore(telemetry): implement universal path normalizer and omni-reaper to prevent preview sprawl
This commit is contained in:
@@ -4568,18 +4568,26 @@ function normalizeFsPath(
|
||||
);
|
||||
}
|
||||
|
||||
// Prevent doubled subdirectory paths (e.g. getacquired-2-0/getacquired-2-0/)
|
||||
if (p.startsWith("getacquired-2-0/")) {
|
||||
p = p.substring("getacquired-2-0/".length);
|
||||
// 1. Strip any leading /workspace/
|
||||
let normalized = p.replace(/^\/workspace\//, "");
|
||||
|
||||
// 2. Strip any leading ./
|
||||
if (normalized.startsWith("./")) {
|
||||
normalized = normalized.slice(2);
|
||||
}
|
||||
|
||||
// 3. Strip project slug prefix if the model hallucinates it (e.g. getacquired-2-0/src...)
|
||||
if (projectSlug && normalized.startsWith(`${projectSlug}/`)) {
|
||||
normalized = normalized.slice(projectSlug.length + 1);
|
||||
}
|
||||
|
||||
// Handle any other legacy hardcoded occurrences
|
||||
if (normalized.startsWith("getacquired-2-0/")) {
|
||||
normalized = normalized.substring("getacquired-2-0/".length);
|
||||
}
|
||||
|
||||
const projectRoot = FS_ROOT;
|
||||
let abs: string;
|
||||
if (p.startsWith("/")) {
|
||||
abs = p;
|
||||
} else {
|
||||
abs = `${projectRoot}/${p}`.replace(/\/+/g, "/");
|
||||
}
|
||||
const abs = `${projectRoot}/${normalized}`.replace(/\/+/g, "/");
|
||||
const norm = abs.replace(/\/[^/]+\/\.\.(?=\/|$)/g, "").replace(/\/+/g, "/");
|
||||
|
||||
// When projectSlug is set, REJECT paths outside the project root.
|
||||
@@ -4588,7 +4596,7 @@ function normalizeFsPath(
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}". Did you mean "${projectRoot}/${p.replace(/^\/+/, "")}"?`,
|
||||
error: `PATH_OUTSIDE_PROJECT: path "${p}" resolves to "${norm}" which is outside the active project at "${projectRoot}".`,
|
||||
},
|
||||
{ status: 400 },
|
||||
);
|
||||
|
||||
@@ -827,31 +827,40 @@ export async function startDevServer(
|
||||
throw new PortOutOfRangeError(opts.port);
|
||||
}
|
||||
|
||||
// 2. Stop ALL tracked rows for this project on the target port.
|
||||
// Previous runs may have crashed or exited without being marked
|
||||
// stopped, causing stale rows to accumulate. We reap them
|
||||
// unconditionally before starting anything new — the AI's intent
|
||||
// is "I want THIS command on THIS port", so most-recent-write-wins.
|
||||
const existingRows = await query<{ id: string; pid: number | null }>(
|
||||
`SELECT id, pid FROM fs_dev_servers
|
||||
WHERE project_id = $1 AND port = $2 AND state IN ('starting','running','failed')`,
|
||||
[opts.projectId, opts.port],
|
||||
// 2. Stop ALL tracked rows for this project on ALL preview ports.
|
||||
// Because our socket reaper is infallible, the AI never needs to
|
||||
// sprawl across multiple ports. We unconditionally reap and stop
|
||||
// every active preview server for this project before starting a new one
|
||||
// to keep the dashboard clean and prevent memory leaks.
|
||||
const existingRows = await query<{
|
||||
id: string;
|
||||
pid: number | null;
|
||||
port: number;
|
||||
}>(
|
||||
`SELECT id, pid, port FROM fs_dev_servers
|
||||
WHERE project_id = $1 AND state IN ('starting','running','failed')`,
|
||||
[opts.projectId],
|
||||
);
|
||||
|
||||
const killPortNodeCmd =
|
||||
`node -e '` +
|
||||
`const fs = require("fs"); ` +
|
||||
`const port = ${opts.port}; ` +
|
||||
`const portsToKill = [${existingRows
|
||||
.map((r) => r.port)
|
||||
.concat(opts.port)
|
||||
.join(",")}]; ` +
|
||||
`try { ` +
|
||||
`const hexPort = port.toString(16).toUpperCase().padStart(4, "0"); ` +
|
||||
`const tcp = fs.readFileSync("/proc/net/tcp", "utf8"); ` +
|
||||
`const inodes = []; ` +
|
||||
`tcp.split("\\n").forEach(line => { ` +
|
||||
`const parts = line.trim().split(/\\s+/); ` +
|
||||
`if (parts.length > 9) { ` +
|
||||
`const local = parts[1]; ` +
|
||||
`for (const p of portsToKill) { ` +
|
||||
`const hexPort = p.toString(16).toUpperCase().padStart(4, "0"); ` +
|
||||
`if (local.endsWith(":" + hexPort)) { inodes.push(parts[9]); } ` +
|
||||
`} ` +
|
||||
`} ` +
|
||||
`}); ` +
|
||||
`if (inodes.length > 0) { ` +
|
||||
`fs.readdirSync("/proc").forEach(file => { ` +
|
||||
@@ -872,7 +881,7 @@ export async function startDevServer(
|
||||
`}); ` +
|
||||
`} ` +
|
||||
`} catch (e) { ` +
|
||||
`try { require("child_process").execSync("fuser -k -9 ${opts.port}/tcp 2>/dev/null || true"); } catch (err) {} ` +
|
||||
`try { require("child_process").execSync("fuser -k -9 " + portsToKill.join(",") + "/tcp 2>/dev/null || true"); } catch (err) {} ` +
|
||||
`}'`;
|
||||
|
||||
for (const row of existingRows) {
|
||||
|
||||
Reference in New Issue
Block a user