Spaces:
Running
Running
deploy(hf): sync szl-holdings/nexus@00edebc993282efd5b43b0144b3d3d27ab43e264 derived COPY set
31b3c47 verified Download scripts/preview.mjs from SZLHOLDINGS/nexus: direct link, hf CLI and curl.
- Browser
- Download file 11.1 kB
-
https://huggingface.co/spaces/SZLHOLDINGS/nexus/resolve/main/scripts/preview.mjs
- Command line
-
hf download hf://spaces/SZLHOLDINGS/nexus/scripts/preview.mjs
-
curl -L -o preview.mjs https://huggingface.co/spaces/SZLHOLDINGS/nexus/resolve/main/scripts/preview.mjs
11.1 kB
| /** | |
| * Owns :8081, the built-output QA preview. | |
| * | |
| * `vite preview` is strictPort, so a preview left over from an earlier turn | |
| * both fails the next start and keeps serving the previous build's output. | |
| * Every restart therefore kills the current port owner first, whoever started | |
| * it. Owners come from /proc, so this runs only inside the Linux sandbox. | |
| * | |
| * node scripts/preview.mjs stop|restart | |
| */ | |
| import { spawn } from "node:child_process"; | |
| import { | |
| existsSync, | |
| mkdirSync, | |
| openSync, | |
| readFileSync, | |
| readdirSync, | |
| readlinkSync, | |
| rmSync, | |
| writeFileSync, | |
| } from "node:fs"; | |
| import { dirname, join } from "node:path"; | |
| import { fileURLToPath, pathToFileURL } from "node:url"; | |
| const PREVIEW_PORT = 8081; | |
| const PREVIEW_URL = `http://127.0.0.1:${PREVIEW_PORT}/`; | |
| const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); | |
| const PID_FILE = join(ROOT, ".grok/preview.pid"); | |
| const LOG_FILE = join(ROOT, ".grok/preview.log"); | |
| const READY_TIMEOUT_MS = Number(process.env.PREVIEW_READY_TIMEOUT_MS || 60000); | |
| const GRACE_MS = 3000; | |
| const POLL_MS = 100; | |
| export function parsePreviewArgs(argv) { | |
| const [action, ...rest] = argv; | |
| if (!action) return { error: "usage: node scripts/preview.mjs stop|restart" }; | |
| if (rest.length > 0) return { error: `unexpected argument: ${rest[0]}` }; | |
| if (!["stop", "restart"].includes(action)) { | |
| return { error: `unknown action: ${action} (expected stop or restart)` }; | |
| } | |
| return { action }; | |
| } | |
| export function parsePid(text) { | |
| const pid = Number.parseInt(String(text ?? "").trim(), 10); | |
| // pid 1 is the sandbox init — never the preview, and dangerous to signal. | |
| return Number.isInteger(pid) && pid > 1 ? pid : null; | |
| } | |
| /** pgid of a process from its /proc/<pid>/stat line. */ | |
| export function parsePgid(stat) { | |
| const line = String(stat ?? ""); | |
| // The comm field is parenthesised and may itself contain spaces; state, ppid | |
| // and pgrp are the three fields after it. | |
| const end = line.lastIndexOf(") "); | |
| if (end === -1) return null; | |
| const pgid = Number.parseInt(line.slice(end + 2).split(/\s+/)[2], 10); | |
| return Number.isInteger(pgid) && pgid > 0 ? pgid : null; | |
| } | |
| const TCP_LISTEN = "0A"; | |
| /** Socket inodes of the LISTEN sockets on `port` in a /proc/net/tcp{,6} dump. */ | |
| export function parseListenerInodes(procNetTcp, port) { | |
| const wanted = `:${port.toString(16).toUpperCase().padStart(4, "0")}`; | |
| const inodes = []; | |
| for (const line of String(procNetTcp ?? "").split("\n")) { | |
| const cols = line.trim().split(/\s+/); | |
| if (cols.length < 10 || cols[3] !== TCP_LISTEN || !cols[1].endsWith(wanted)) continue; | |
| if (/^\d+$/.test(cols[9])) inodes.push(cols[9]); | |
| } | |
| return inodes; | |
| } | |
| export function looksLikePreviewProcess(cmdline) { | |
| // /proc/<pid>/cmdline is NUL-separated. | |
| const argv = String(cmdline ?? "") | |
| .split("\0") | |
| .filter(Boolean) | |
| .join(" "); | |
| // The sandbox service runs scripts/preview-thumbnail.mjs in this box, and | |
| // this script can be running concurrently: neither is ever a target. | |
| if (/\bpreview[\w-]*\.mjs\b/.test(argv)) return false; | |
| // The `npm run preview` wrapper (`npm-cli.js run preview`) and its vite child. | |
| // `preview` must be the whole script name: `run preview:stop`/`preview:restart` | |
| // are this tooling's own wrappers, and `vite build --outDir preview-dist` is | |
| // not a server. | |
| return /\brun\s+preview(?:\s|$)/.test(argv) || /\bvite\b\s+preview\b/.test(argv); | |
| } | |
| /** | |
| * Pids to signal. Port owners are owners by definition; the pidfile pid is only | |
| * a claim left by an earlier run — pids are re-used across hibernate/revive, so | |
| * signal it only when its command line still looks like the preview. | |
| */ | |
| export function previewOwners({ portPids, pidFilePid, cmdlineOf }) { | |
| const owners = new Set(portPids); | |
| if ( | |
| pidFilePid !== null && | |
| !owners.has(pidFilePid) && | |
| looksLikePreviewProcess(cmdlineOf(pidFilePid)) | |
| ) { | |
| owners.add(pidFilePid); | |
| } | |
| return [...owners]; | |
| } | |
| async function waitForExit(pids, { isAlive, sleep, timeoutMs, pollMs }) { | |
| let remaining = pids.filter((pid) => isAlive(pid)); | |
| for (let waited = 0; remaining.length > 0 && waited < timeoutMs; waited += pollMs) { | |
| await sleep(pollMs); | |
| remaining = remaining.filter((pid) => isAlive(pid)); | |
| } | |
| return remaining; | |
| } | |
| /** | |
| * SIGTERM every live pid, then SIGKILL whatever outlives the grace period. | |
| * Returns `{ signalled, killed, stubborn }` — `stubborn` is still alive after | |
| * the SIGKILL wait, which means the port is not reliably free. | |
| */ | |
| export async function terminatePids( | |
| pids, | |
| { kill, isAlive, sleep, graceMs = GRACE_MS, pollMs = POLL_MS }, | |
| ) { | |
| const signalled = pids.filter((pid) => isAlive(pid)); | |
| for (const pid of signalled) kill(pid, "SIGTERM"); | |
| const killed = await waitForExit(signalled, { isAlive, sleep, timeoutMs: graceMs, pollMs }); | |
| for (const pid of killed) kill(pid, "SIGKILL"); | |
| const stubborn = await waitForExit(killed, { isAlive, sleep, timeoutMs: graceMs, pollMs }); | |
| return { signalled, killed, stubborn }; | |
| } | |
| /** | |
| * What `stop` reports. `after` is the post-kill port check: `unattributed: true` | |
| * means a listener exists whose pid could not be resolved, so it may not claim | |
| * the port is free. | |
| */ | |
| export function stopOutcome({ signalled, stubborn, after }) { | |
| const held = [...new Set([...stubborn, ...after.pids])]; | |
| if (held.length > 0) { | |
| return { ok: false, error: `port ${PREVIEW_PORT} is still held by pid(s) ${held.join(", ")}` }; | |
| } | |
| if (after.unattributed) { | |
| return { | |
| ok: false, | |
| error: `port ${PREVIEW_PORT} is held by a process this script cannot see`, | |
| }; | |
| } | |
| const message = | |
| signalled.length > 0 | |
| ? `stopped pid(s) ${signalled.join(", ")} — port ${PREVIEW_PORT} is free` | |
| : `nothing was listening on ${PREVIEW_PORT}`; | |
| return { ok: true, message }; | |
| } | |
| const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); | |
| function isAlive(pid) { | |
| try { | |
| process.kill(pid, 0); | |
| return true; | |
| } catch (err) { | |
| return err?.code === "EPERM"; | |
| } | |
| } | |
| function pgidOf(pid) { | |
| try { | |
| return parsePgid(readFileSync(`/proc/${pid}/stat`, "utf8")); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function killPid(pid, signal) { | |
| // restart() detaches the server into its own process group, so signal the | |
| // group to reach `vite` under the `npm` wrapper. Only for a leader: `-pid` on | |
| // a pid that leads no group still reaches any unrelated group numbered pid. | |
| if (pgidOf(pid) === pid) { | |
| try { | |
| process.kill(-pid, signal); | |
| return; | |
| } catch { | |
| // The group exited between the pgid read and the signal. | |
| } | |
| } | |
| try { | |
| process.kill(pid, signal); | |
| } catch { | |
| // Exited between the liveness check and the signal. | |
| } | |
| } | |
| function cmdlineOf(pid) { | |
| try { | |
| return readFileSync(`/proc/${pid}/cmdline`, "utf8"); | |
| } catch { | |
| // Usually a dead pid — the stale pidfile this corroboration exists for. | |
| return ""; | |
| } | |
| } | |
| function readPidFile() { | |
| try { | |
| return parsePid(readFileSync(PID_FILE, "utf8")); | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function pidsForSocketInodes(inodes) { | |
| const targets = new Set([...inodes].map((inode) => `socket:[${inode}]`)); | |
| const pids = []; | |
| for (const entry of readdirSync("/proc")) { | |
| const pid = parsePid(entry); | |
| if (pid === null || pid === process.pid) continue; | |
| let fds; | |
| try { | |
| fds = readdirSync(`/proc/${pid}/fd`); | |
| } catch { | |
| // Exited mid-scan, or owned by another user. | |
| continue; | |
| } | |
| for (const fd of fds) { | |
| try { | |
| if (targets.has(readlinkSync(`/proc/${pid}/fd/${fd}`))) { | |
| pids.push(pid); | |
| break; | |
| } | |
| } catch { | |
| // fd closed mid-scan. | |
| } | |
| } | |
| } | |
| return pids; | |
| } | |
| /** | |
| * `{ pids, unattributed }` — `unattributed: true` when the port has a listener | |
| * whose owning pid could not be resolved (an fd dir we may not read). | |
| */ | |
| function portOwners() { | |
| const inodes = new Set(); | |
| for (const file of ["/proc/net/tcp", "/proc/net/tcp6"]) { | |
| let dump; | |
| try { | |
| dump = readFileSync(file, "utf8"); | |
| } catch { | |
| // tcp6 is absent when the box has no IPv6. | |
| continue; | |
| } | |
| for (const inode of parseListenerInodes(dump, PREVIEW_PORT)) inodes.add(inode); | |
| } | |
| const pids = inodes.size > 0 ? pidsForSocketInodes(inodes) : []; | |
| return { pids, unattributed: inodes.size > 0 && pids.length === 0 }; | |
| } | |
| async function stop(announce = true) { | |
| const owners = previewOwners({ | |
| portPids: portOwners().pids, | |
| pidFilePid: readPidFile(), | |
| cmdlineOf, | |
| }); | |
| const { signalled, stubborn } = await terminatePids(owners, { kill: killPid, isAlive, sleep }); | |
| const outcome = stopOutcome({ signalled, stubborn, after: portOwners() }); | |
| if (!outcome.ok) { | |
| // Keep the pidfile: a survivor the port scan cannot attribute leaves it as | |
| // the only record a retry could use. | |
| console.error(`[preview] ${outcome.error}`); | |
| return false; | |
| } | |
| rmSync(PID_FILE, { force: true }); | |
| if (announce) console.log(`[preview] ${outcome.message}`); | |
| return true; | |
| } | |
| async function waitForReady(failure) { | |
| const deadline = Date.now() + READY_TIMEOUT_MS; | |
| while (Date.now() < deadline && failure() === null) { | |
| try { | |
| // Any HTTP response means the server is bound; a 404 is still ready. | |
| await fetch(PREVIEW_URL, { signal: AbortSignal.timeout(2000) }); | |
| return true; | |
| } catch { | |
| await sleep(250); | |
| } | |
| } | |
| return false; | |
| } | |
| async function restart() { | |
| if (!(await stop())) return 1; | |
| mkdirSync(dirname(LOG_FILE), { recursive: true }); | |
| const log = openSync(LOG_FILE, "a"); | |
| const child = spawn("npm", ["run", "preview"], { | |
| cwd: ROOT, | |
| detached: true, | |
| stdio: ["ignore", log, log], | |
| }); | |
| child.unref(); | |
| writeFileSync(PID_FILE, `${child.pid}\n`); | |
| let failure = null; | |
| child.on("error", (err) => { | |
| failure = `npm run preview could not be spawned: ${err.message}`; | |
| }); | |
| child.on("exit", (code, signal) => { | |
| failure = `npm run preview exited early (${signal ?? `code ${code}`})`; | |
| }); | |
| if (!(await waitForReady(() => failure))) { | |
| const secs = Math.round(READY_TIMEOUT_MS / 1000); | |
| const why = | |
| failure ?? | |
| `nothing answered on ${PREVIEW_URL} within ${secs}s — check that vite.config.ts ` + | |
| `still sets preview.port ${PREVIEW_PORT}`; | |
| console.error(`[preview] ${why} — see ${LOG_FILE}`); | |
| // A server that binds a few seconds later would serve a build the agent has | |
| // already been told to distrust. | |
| await stop(false); | |
| return 1; | |
| } | |
| console.log(`[preview] serving ${PREVIEW_URL} (pid ${child.pid}, log ${LOG_FILE})`); | |
| return 0; | |
| } | |
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { | |
| const args = parsePreviewArgs(process.argv.slice(2)); | |
| if (args.error) { | |
| console.error(`[preview] ${args.error}`); | |
| process.exit(1); | |
| } | |
| if (!existsSync("/proc/self")) { | |
| console.error("[preview] no /proc — this script only runs inside the sandbox"); | |
| process.exit(1); | |
| } | |
| process.exitCode = args.action === "stop" ? ((await stop()) ? 0 : 1) : await restart(); | |
| } | |