@evandu/super-pi 0.1.80 → 0.1.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,5 +21,5 @@
21
21
  "src/client/types.ts": "11fd40e5784a2e765635f4f57d6edd5c97ad0eae85b1825b9da32a88d25b4db9",
22
22
  "src/client/vite-env.d.ts": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5"
23
23
  },
24
- "recordedAt": "2026-10-18T00:04:03.485Z"
24
+ "recordedAt": "2026-10-18T00:09:25.847Z"
25
25
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,6 +16,7 @@
16
16
  "dist",
17
17
  "agents",
18
18
  "bin",
19
+ "scripts/restart-server.mjs",
19
20
  "README.md"
20
21
  ],
21
22
  "engines": {
@@ -0,0 +1,62 @@
1
+ import { spawn, execFile } from "node:child_process";
2
+ import { openSync, readFileSync } from "node:fs";
3
+ import { platform } from "node:os";
4
+ import { promisify } from "node:util";
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const port = Number(process.env.SUPER_PI_PORT ?? 3080);
8
+ const entry = "dist/server/index.js";
9
+ const allowedEntries = [entry, "src/server/index.ts"];
10
+ const waitMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ async function pidsOnPort() {
12
+ if (platform() === "win32") {
13
+ const { stdout } = await execFileAsync("netstat", ["-ano", "-p", "TCP"]);
14
+ return [...new Set(stdout.split(/\r?\n/).filter((line) => line.includes(`:${port}`) && /LISTENING\s+\d+/.test(line)).map((line) => line.trim().split(/\s+/).at(-1)))];
15
+ }
16
+ try { const { stdout } = await execFileAsync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"]); return stdout.trim().split(/\s+/).filter(Boolean); }
17
+ catch { const { stdout } = await execFileAsync("sh", ["-c", `ss -lptn 'sport = :${port}' 2>/dev/null`]); return [...new Set([...stdout.matchAll(/pid=(\d+)/g)].map((match) => match[1]))]; }
18
+ }
19
+ async function commandLine(pid) {
20
+ if (platform() === "win32") { const { stdout } = await execFileAsync("powershell", ["-NoProfile", "-Command", `(Get-CimInstance Win32_Process -Filter \"ProcessId=${pid}\").CommandLine`]); return stdout.trim(); }
21
+ try { return (await import("node:fs/promises")).readFile(`/proc/${pid}/cmdline`, "utf8").then((value) => value.replaceAll("\0", " ").trim()); } catch { const { stdout } = await execFileAsync("ps", ["-p", pid, "-o", "command="]); return stdout.trim(); }
22
+ }
23
+ async function requestShutdown() {
24
+ const headers = {};
25
+ if (process.env.SUPER_PI_ACCESS_TOKEN) headers.authorization = `Bearer ${process.env.SUPER_PI_ACCESS_TOKEN}`;
26
+ try { return (await fetch(`http://127.0.0.1:${port}/api/shutdown`, { method: "POST", headers, signal: AbortSignal.timeout(2000) })).status; }
27
+ catch { return null; }
28
+ }
29
+ async function waitForPortRelease(timeoutMs) {
30
+ for (let elapsed = 0; elapsed < timeoutMs; elapsed += 100) { if (!(await pidsOnPort()).length) return true; await waitMs(100); }
31
+ return false;
32
+ }
33
+ // On Windows a detached, console-less node.exe has no window to receive WM_CLOSE, so plain
34
+ // taskkill (SIGTERM-like) always fails; /F (or SIGKILL elsewhere) is the only force option.
35
+ async function forceKill(pid) {
36
+ console.log(`Force-killing PID ${pid} (graceful shutdown unavailable or timed out)...`);
37
+ if (platform() === "win32") await execFileAsync("taskkill", ["/PID", pid, "/T", "/F"]).catch((error) => console.log(`taskkill /F warning: ${error.message}`));
38
+ else process.kill(Number(pid), "SIGKILL");
39
+ }
40
+ async function stopConflicts() {
41
+ const pids = await pidsOnPort(); if (!pids.length) { console.log(`Port ${port} is free.`); return true; }
42
+ for (const pid of pids) { const cmd = await commandLine(pid); console.log(`Port ${port} is occupied by PID ${pid}: ${cmd}`); if (!allowedEntries.some((candidate) => cmd.includes(candidate))) { console.error(`Refusing to kill non-super-pi process on port ${port}: ${cmd}`); process.exitCode = 2; return false; } }
43
+ const status = await requestShutdown();
44
+ if (status !== null && status >= 200 && status < 300) {
45
+ console.log(`Graceful shutdown accepted (HTTP ${status}); waiting for port ${port} to release...`);
46
+ if (await waitForPortRelease(3000)) { console.log(`Port ${port} released after graceful shutdown.`); return true; }
47
+ console.log(`Graceful shutdown did not release port ${port} within 3s.`);
48
+ } else {
49
+ console.log(`Graceful shutdown unavailable (HTTP ${status ?? "no response"}); the running server may predate the shutdown endpoint.`);
50
+ }
51
+ for (const pid of await pidsOnPort()) await forceKill(pid);
52
+ if (await waitForPortRelease(2000)) { console.log(`Port ${port} released after force kill.`); return true; }
53
+ console.error(`Port ${port} is still occupied after force kill.`); process.exitCode = 1; return false;
54
+ }
55
+ if (await stopConflicts() === false) process.exit(2);
56
+ console.log(`Starting ${entry} on port ${port}...`);
57
+ const logPath = process.env.SUPER_PI_LOG ?? "restart-server.log";
58
+ const logFd = openSync(logPath, "a");
59
+ const child = spawn(process.execPath, [entry], { stdio: ["ignore", logFd, logFd], detached: true, env: process.env });
60
+ child.unref();
61
+ for (let elapsed = 0; elapsed < 10000; elapsed += 200) { try { const response = await fetch(`http://127.0.0.1:${port}/api/health`); const body = await response.json(); if (response.ok && body.status === "ok" && body.service === "super-pi") { console.log("Server ready: /api/health passed."); process.exit(0); } } catch {} await waitMs(200); }
62
+ console.error(`Server failed readiness check. See ${logPath}:`); try { console.error(readFileSync(logPath, "utf8").slice(-8000)); } catch {} child.kill(); process.exit(1);