@kky42/pievo-server 1.0.0
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.
- package/.output/nitro.json +17 -0
- package/.output/public/assets/CodeField-dOzyFQNH.js +34 -0
- package/.output/public/assets/LoopView-B4H9QibC.js +55 -0
- package/.output/public/assets/Modal-D33gNSgx.js +1 -0
- package/.output/public/assets/SignIn-SHuRRyOv.js +1 -0
- package/.output/public/assets/actionUi-ChGGALzg.js +1 -0
- package/.output/public/assets/app-DElSo_jy.css +2 -0
- package/.output/public/assets/artifactView-DFuubXLJ.js +66 -0
- package/.output/public/assets/index-DfpTWRSz.js +12 -0
- package/.output/public/assets/invite._token-DJtd-XE2.js +1 -0
- package/.output/public/assets/invite._token-T8oo4Qtw.js +1 -0
- package/.output/public/assets/jsx-runtime-BseJUIpC.js +1 -0
- package/.output/public/assets/lifecycleUi-DKkChdnU.js +1 -0
- package/.output/public/assets/loopApi-BM2nj71x.js +3 -0
- package/.output/public/assets/loops._loopId-B19Jw7jC.js +5 -0
- package/.output/public/assets/loops._loopId_.runs._runId-_CflHV38.js +3 -0
- package/.output/public/assets/notifyFns-DCubjpRy.js +1 -0
- package/.output/public/assets/preload-helper-CKUiEq3b.js +3 -0
- package/.output/public/assets/routes-B9tgli5f.js +1 -0
- package/.output/public/assets/routes-CTtlM_qt.js +1 -0
- package/.output/public/assets/t._teamId-Bm21mngW.js +1 -0
- package/.output/public/assets/t._teamId-gR7BVKkR.js +1 -0
- package/.output/public/assets/ui-Dp88qUWh.js +1 -0
- package/.output/public/assets/useRouter-DsjH8G0p.js +1 -0
- package/.output/public/favicon.svg +12 -0
- package/.output/public/fonts/GeistPixel-Square.woff2 +0 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_libs/@aws-crypto/crc32+[...].mjs +419 -0
- package/.output/server/_libs/@aws-sdk/checksums+[...].mjs +10732 -0
- package/.output/server/_libs/@aws-sdk/client-s3+[...].mjs +9475 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-http+[...].mjs +137 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-ini+[...].mjs +440 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-process+[...].mjs +62 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-sso+[...].mjs +818 -0
- package/.output/server/_libs/@aws-sdk/credential-provider-web-identity+[...].mjs +59 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +11125 -0
- package/.output/server/_libs/@codemirror/autocomplete+[...].mjs +19713 -0
- package/.output/server/_libs/@codemirror/lang-css+[...].mjs +2636 -0
- package/.output/server/_libs/@codemirror/lang-html+[...].mjs +2247 -0
- package/.output/server/_libs/@codemirror/lang-json+[...].mjs +79 -0
- package/.output/server/_libs/@reduxjs/toolkit+[...].mjs +1736 -0
- package/.output/server/_libs/@smithy/credential-provider-imds+[...].mjs +342 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +14300 -0
- package/.output/server/_libs/@uiw/codemirror-extensions-basic-setup+[...].mjs +76 -0
- package/.output/server/_libs/atomic-sleep.mjs +27 -0
- package/.output/server/_libs/aws-crypto__crc32c.mjs +314 -0
- package/.output/server/_libs/aws-sdk__nested-clients.mjs +2671 -0
- package/.output/server/_libs/babel__runtime.mjs +30 -0
- package/.output/server/_libs/clsx.mjs +16 -0
- package/.output/server/_libs/codemirror__commands.mjs +1725 -0
- package/.output/server/_libs/codemirror__lint.mjs +565 -0
- package/.output/server/_libs/codemirror__search.mjs +1138 -0
- package/.output/server/_libs/codemirror__theme-one-dark.mjs +161 -0
- package/.output/server/_libs/colorette.mjs +129 -0
- package/.output/server/_libs/croner.mjs +735 -0
- package/.output/server/_libs/d3-array.mjs +231 -0
- package/.output/server/_libs/d3-color.mjs +330 -0
- package/.output/server/_libs/d3-format.mjs +252 -0
- package/.output/server/_libs/d3-interpolate.mjs +163 -0
- package/.output/server/_libs/d3-path.mjs +77 -0
- package/.output/server/_libs/d3-scale+[...].mjs +1907 -0
- package/.output/server/_libs/d3-shape.mjs +689 -0
- package/.output/server/_libs/dateformat.mjs +341 -0
- package/.output/server/_libs/decimal.js-light.mjs +884 -0
- package/.output/server/_libs/diff.mjs +732 -0
- package/.output/server/_libs/dom-serializer+[...].mjs +708 -0
- package/.output/server/_libs/domhandler.mjs +461 -0
- package/.output/server/_libs/dompurify.mjs +2111 -0
- package/.output/server/_libs/domutils.mjs +868 -0
- package/.output/server/_libs/drizzle-orm+postgres.mjs +7689 -0
- package/.output/server/_libs/electric-sql__pglite.mjs +14674 -0
- package/.output/server/_libs/end-of-stream+once+wrappy.mjs +145 -0
- package/.output/server/_libs/es-toolkit.mjs +1737 -0
- package/.output/server/_libs/eventemitter3.mjs +236 -0
- package/.output/server/_libs/fast-copy.mjs +285 -0
- package/.output/server/_libs/fast-safe-stringify.mjs +172 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +1201 -0
- package/.output/server/_libs/html-dom-parser+htmlparser2.mjs +1560 -0
- package/.output/server/_libs/html-react-parser+[...].mjs +1360 -0
- package/.output/server/_libs/initdb.wasm +0 -0
- package/.output/server/_libs/marked.mjs +1548 -0
- package/.output/server/_libs/on-exit-leak-free.mjs +80 -0
- package/.output/server/_libs/pglite.data +37667 -8
- package/.output/server/_libs/pglite.wasm +0 -0
- package/.output/server/_libs/pino+[...].mjs +3196 -0
- package/.output/server/_libs/pino-abstract-transport+split2.mjs +194 -0
- package/.output/server/_libs/pino-pretty+[...].mjs +1623 -0
- package/.output/server/_libs/pinojs__redact.mjs +280 -0
- package/.output/server/_libs/react-redux+[...].mjs +205 -0
- package/.output/server/_libs/recharts+victory-vendor.mjs +15875 -0
- package/.output/server/_libs/tanstack__history.mjs +342 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_libs/uiw__codemirror-themes.mjs +32 -0
- package/.output/server/_libs/uiw__react-codemirror.mjs +354 -0
- package/.output/server/_runtime.mjs +37 -0
- package/.output/server/_ssr/CodeField-tm4rr6G3.mjs +143 -0
- package/.output/server/_ssr/DashboardView-CC7htv28.mjs +2795 -0
- package/.output/server/_ssr/LoopPlaybook-BXH8mhiX.mjs +2105 -0
- package/.output/server/_ssr/LoopView-DCugEbHx.mjs +1106 -0
- package/.output/server/_ssr/Modal-DpKZHLZK.mjs +49 -0
- package/.output/server/_ssr/SignIn-dTndU6Hw.mjs +67 -0
- package/.output/server/_ssr/actionUi-CZbEixbf.mjs +119 -0
- package/.output/server/_ssr/adapters-BS8XosjF.mjs +130 -0
- package/.output/server/_ssr/artifactFiles-Cagtj7J0.mjs +61 -0
- package/.output/server/_ssr/artifactKind-BW0isV8L.mjs +37 -0
- package/.output/server/_ssr/artifactView-DS4O22wl.mjs +304 -0
- package/.output/server/_ssr/artifacts-DngKyW2u.mjs +2 -0
- package/.output/server/_ssr/auth-DY1DuYZd.mjs +38429 -0
- package/.output/server/_ssr/boot-BbtG0ImW.mjs +9010 -0
- package/.output/server/_ssr/boot-CMHTxQpJ.mjs +2 -0
- package/.output/server/_ssr/bun-sqlite-dialect-BW9W1_Ps-CU1Spd6z.mjs +162 -0
- package/.output/server/_ssr/core-RrmWoKne.mjs +5 -0
- package/.output/server/_ssr/createServerRpc-BD8Xn_0_.mjs +12 -0
- package/.output/server/_ssr/d1-sqlite-dialect-BLC8LXE6-DfG7tB8s.mjs +112 -0
- package/.output/server/_ssr/db-DWAuZq1o.mjs +2 -0
- package/.output/server/_ssr/dist-BIxFFVZg.mjs +342 -0
- package/.output/server/_ssr/empty-plugin-adapters-DKUlnwwl.mjs +5 -0
- package/.output/server/_ssr/factory-Blq0DYfT.mjs +6903 -0
- package/.output/server/_ssr/invite._token-BRCo9LGB.mjs +36 -0
- package/.output/server/_ssr/invite._token-DJxGLILJ.mjs +28 -0
- package/.output/server/_ssr/invite._token-NewFt27g.mjs +92 -0
- package/.output/server/_ssr/kysely-adapter-D3XTe64J.mjs +2 -0
- package/.output/server/_ssr/kysely-migration-tables-JkVUjPF_-bPAX9LR_.mjs +19 -0
- package/.output/server/_ssr/lifecycleUi-C9lg1r3r.mjs +38 -0
- package/.output/server/_ssr/logger-Dw82jNjH.mjs +2 -0
- package/.output/server/_ssr/loopApi-BtfkZQYK.mjs +532 -0
- package/.output/server/_ssr/loopApi-Cxe8WyOB.mjs +105 -0
- package/.output/server/_ssr/loops._loopId-7NQJPB9W.mjs +30 -0
- package/.output/server/_ssr/loops._loopId-DhnArGdL.mjs +1635 -0
- package/.output/server/_ssr/loops._loopId_.runs._runId-Bvuo2nki.mjs +29 -0
- package/.output/server/_ssr/loops._loopId_.runs._runId-Dip5Xk-q.mjs +674 -0
- package/.output/server/_ssr/machineFns-Dvr5ICUy.mjs +266 -0
- package/.output/server/_ssr/node-sqlite-dialect-DhyOpJTX.mjs +162 -0
- package/.output/server/_ssr/notifyFns-BcMRY5kP.mjs +118 -0
- package/.output/server/_ssr/notifyFns-Q458pZmE.mjs +580 -0
- package/.output/server/_ssr/router-J8KZ30xw.mjs +678 -0
- package/.output/server/_ssr/routes-Bcs4T1ox.mjs +28 -0
- package/.output/server/_ssr/routes-BoGrcf-b.mjs +43 -0
- package/.output/server/_ssr/routes-D4am3Zff.mjs +28 -0
- package/.output/server/_ssr/runDiff-CpEFqo7G.mjs +113 -0
- package/.output/server/_ssr/ssr.mjs +5837 -0
- package/.output/server/_ssr/start-4ifArtKx.mjs +4 -0
- package/.output/server/_ssr/store-b3JcQ08F.mjs +2 -0
- package/.output/server/_ssr/t._teamId-CeUhzgzT.mjs +32 -0
- package/.output/server/_ssr/t._teamId-DR-hQK7o.mjs +47 -0
- package/.output/server/_ssr/t._teamId-Dy_McbRt.mjs +31 -0
- package/.output/server/_ssr/teamCookie-B7hLOIie.mjs +10 -0
- package/.output/server/_ssr/teamFns-BIDnsg2A.mjs +473 -0
- package/.output/server/_ssr/teamFns-DA4ovI5g.mjs +33 -0
- package/.output/server/_ssr/tokens-ytd60FUt.mjs +2 -0
- package/.output/server/_ssr/ui-Dej2qjuS.mjs +267 -0
- package/.output/server/_tanstack-start-manifest_v-CVS6c7re.mjs +95 -0
- package/.output/server/drizzle/0000_round_marauders.sql +193 -0
- package/.output/server/drizzle/0001_naive_silver_fox.sql +25 -0
- package/.output/server/drizzle/0002_funny_sage.sql +12 -0
- package/.output/server/drizzle/0003_optimal_slapstick.sql +102 -0
- package/.output/server/drizzle/0004_remove_grok_agent.sql +5 -0
- package/.output/server/drizzle/0005_heavy_gauntlet.sql +8 -0
- package/.output/server/drizzle/0006_tranquil_sharon_carter.sql +1 -0
- package/.output/server/drizzle/0007_faulty_starbolt.sql +30 -0
- package/.output/server/drizzle/0008_durable_lifecycle_evidence.sql +15 -0
- package/.output/server/drizzle/meta/0000_snapshot.json +1333 -0
- package/.output/server/drizzle/meta/0001_snapshot.json +1498 -0
- package/.output/server/drizzle/meta/0002_snapshot.json +1576 -0
- package/.output/server/drizzle/meta/0003_snapshot.json +1666 -0
- package/.output/server/drizzle/meta/0005_snapshot.json +1666 -0
- package/.output/server/drizzle/meta/0006_snapshot.json +1672 -0
- package/.output/server/drizzle/meta/0007_snapshot.json +1787 -0
- package/.output/server/drizzle/meta/_journal.json +69 -0
- package/.output/server/index.mjs +464 -0
- package/.output/server/node_modules/tslib/modules/index.js +70 -0
- package/.output/server/node_modules/tslib/modules/package.json +3 -0
- package/.output/server/node_modules/tslib/package.json +47 -0
- package/.output/server/node_modules/tslib/tslib.js +484 -0
- package/.output/server/package.json +9 -0
- package/LICENSE +21 -0
- package/drizzle/0000_round_marauders.sql +193 -0
- package/drizzle/0001_naive_silver_fox.sql +25 -0
- package/drizzle/0002_funny_sage.sql +12 -0
- package/drizzle/0003_optimal_slapstick.sql +102 -0
- package/drizzle/0004_remove_grok_agent.sql +5 -0
- package/drizzle/0005_heavy_gauntlet.sql +8 -0
- package/drizzle/0006_tranquil_sharon_carter.sql +1 -0
- package/drizzle/0007_faulty_starbolt.sql +30 -0
- package/drizzle/0008_durable_lifecycle_evidence.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +1333 -0
- package/drizzle/meta/0001_snapshot.json +1498 -0
- package/drizzle/meta/0002_snapshot.json +1576 -0
- package/drizzle/meta/0003_snapshot.json +1666 -0
- package/drizzle/meta/0005_snapshot.json +1666 -0
- package/drizzle/meta/0006_snapshot.json +1672 -0
- package/drizzle/meta/0007_snapshot.json +1787 -0
- package/drizzle/meta/_journal.json +69 -0
- package/package.json +84 -0
- package/scripts/pievo-server.mjs +390 -0
- package/scripts/prestart.mjs +66 -0
- package/scripts/server-cli-lib.mjs +451 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_HOST = "127.0.0.1";
|
|
8
|
+
export const DEFAULT_PORT = 3000;
|
|
9
|
+
const MALFORMED_LOCK_GRACE_MS = 5_000;
|
|
10
|
+
|
|
11
|
+
export function usage() {
|
|
12
|
+
return `Usage: pievo-server <start|status|restart|stop> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
start start in the background (idempotent)
|
|
16
|
+
start --foreground run in the foreground
|
|
17
|
+
status show local process status
|
|
18
|
+
restart stop, then start the currently installed server
|
|
19
|
+
stop gracefully stop the local server
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--host <host> bind host (default: HOST/NITRO_HOST or 127.0.0.1)
|
|
23
|
+
--port <port> bind port (default: PORT/NITRO_PORT/PIEVO_PORT or 3000)
|
|
24
|
+
--data-dir <path> select the instance (database, blobs, pid, lock, and log)
|
|
25
|
+
-h, --help show this help
|
|
26
|
+
|
|
27
|
+
Restart preserves the recorded instance host/port unless a flag or bind environment
|
|
28
|
+
variable overrides it. The default bind is local-only. Set --host 0.0.0.0 only with
|
|
29
|
+
appropriate auth and network controls.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function parseArgs(argv) {
|
|
33
|
+
if (argv.includes("--help") || argv.includes("-h") || argv[0] === "help") return { command: "help" };
|
|
34
|
+
const command = argv[0];
|
|
35
|
+
if (!["start", "status", "restart", "stop", "_serve"].includes(command)) {
|
|
36
|
+
throw new Error(command ? `unknown command: ${command}` : "a command is required");
|
|
37
|
+
}
|
|
38
|
+
const out = { command, foreground: false };
|
|
39
|
+
for (let i = 1; i < argv.length; i++) {
|
|
40
|
+
const arg = argv[i];
|
|
41
|
+
if (arg === "--foreground" && (command === "start" || command === "restart")) {
|
|
42
|
+
out.foreground = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const match = arg.match(/^--(host|port|data-dir)(?:=(.*))?$/);
|
|
46
|
+
if (match) {
|
|
47
|
+
const key = match[1] === "data-dir" ? "dataDir" : match[1];
|
|
48
|
+
const value = match[2] ?? argv[++i];
|
|
49
|
+
if (!value || value.startsWith("--")) throw new Error(`${arg.split("=")[0]} requires a value`);
|
|
50
|
+
out[key] = value;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`unknown option for ${command}: ${arg}`);
|
|
54
|
+
}
|
|
55
|
+
if ((command === "status" || command === "stop") && (out.host || out.port || out.foreground)) {
|
|
56
|
+
throw new Error(`${command} only accepts --data-dir`);
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function validPort(value) {
|
|
62
|
+
const n = Number(value);
|
|
63
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) throw new Error(`invalid port: ${value}`);
|
|
64
|
+
return n;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function firstSet(env, names) {
|
|
68
|
+
for (const name of names) {
|
|
69
|
+
if (env[name] !== undefined && String(env[name]).trim()) return env[name];
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildEnvPlan(options, baseEnv = process.env) {
|
|
75
|
+
const dataDir = path.resolve(options.dataDir || baseEnv.PIEVO_DATA_DIR?.trim() || path.join(os.homedir(), ".pievo"));
|
|
76
|
+
const host = String(options.host || firstSet(baseEnv, ["NITRO_HOST", "HOST"]) || DEFAULT_HOST).trim();
|
|
77
|
+
if (!host) throw new Error("host cannot be empty");
|
|
78
|
+
const port = validPort(options.port || firstSet(baseEnv, ["NITRO_PORT", "PORT", "PIEVO_PORT"]) || DEFAULT_PORT);
|
|
79
|
+
const env = {
|
|
80
|
+
...baseEnv,
|
|
81
|
+
NODE_ENV: "production",
|
|
82
|
+
PIEVO_DATA_DIR: dataDir,
|
|
83
|
+
NITRO_HOST: host,
|
|
84
|
+
HOST: host,
|
|
85
|
+
NITRO_PORT: String(port),
|
|
86
|
+
PORT: String(port),
|
|
87
|
+
};
|
|
88
|
+
delete env.PIEVO_SERVER_LAUNCH_NONCE;
|
|
89
|
+
if (!env.DATABASE_URL && !env.PIEVO_DB) env.PIEVO_DB = "pglite";
|
|
90
|
+
return {
|
|
91
|
+
env,
|
|
92
|
+
dataDir,
|
|
93
|
+
host,
|
|
94
|
+
port,
|
|
95
|
+
pidFile: path.join(dataDir, "server.pid"),
|
|
96
|
+
lockFile: path.join(dataDir, "server.start.lock"),
|
|
97
|
+
logFile: path.join(dataDir, "server.log"),
|
|
98
|
+
readyUrl: `http://${hostForUrl(host)}:${port}/api/ready`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function buildRestartPlan(options, baseEnv, record) {
|
|
103
|
+
const preserved = { ...options };
|
|
104
|
+
if (preserved.host === undefined && firstSet(baseEnv, ["NITRO_HOST", "HOST"]) === undefined && record?.host) {
|
|
105
|
+
preserved.host = record.host;
|
|
106
|
+
}
|
|
107
|
+
if (preserved.port === undefined && firstSet(baseEnv, ["NITRO_PORT", "PORT", "PIEVO_PORT"]) === undefined && record?.port) {
|
|
108
|
+
preserved.port = String(record.port);
|
|
109
|
+
}
|
|
110
|
+
return buildEnvPlan(preserved, baseEnv);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function withLaunchNonce(plan, nonce = crypto.randomBytes(24).toString("base64url")) {
|
|
114
|
+
return {
|
|
115
|
+
...plan,
|
|
116
|
+
launchNonce: nonce,
|
|
117
|
+
env: { ...plan.env, PIEVO_SERVER_LAUNCH_NONCE: nonce },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hostForUrl(host) {
|
|
122
|
+
if (host === "0.0.0.0" || host === "::") return "127.0.0.1";
|
|
123
|
+
return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function processStartTime(pid, exec = execFileSync) {
|
|
127
|
+
// Linux (including node:slim containers): procfs needs no optional `ps` binary.
|
|
128
|
+
// Boot id + kernel start ticks uniquely identify a pid across reuse and reboot.
|
|
129
|
+
try {
|
|
130
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
131
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/);
|
|
132
|
+
const startTicks = fields[19]; // field 22 overall; fields[] begins at field 3
|
|
133
|
+
const bootId = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
134
|
+
if (startTicks && bootId) return `linux:${bootId}:${startTicks}`;
|
|
135
|
+
} catch {}
|
|
136
|
+
// macOS and other POSIX hosts: one identical ps representation at write/check.
|
|
137
|
+
try {
|
|
138
|
+
const value = exec("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
139
|
+
encoding: "utf8",
|
|
140
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
141
|
+
}).trim();
|
|
142
|
+
return value ? `ps:${value}` : undefined;
|
|
143
|
+
} catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function validPidRecord(value) {
|
|
149
|
+
if (![1, 2].includes(value?.version)) return false;
|
|
150
|
+
if (!Number.isInteger(value.pid) || value.pid <= 0 || typeof value.startTime !== "string" || !value.startTime) return false;
|
|
151
|
+
if (typeof value.host !== "string" || !value.host || !Number.isInteger(value.port)) return false;
|
|
152
|
+
if (value.version === 2) {
|
|
153
|
+
if (!['starting', 'running'].includes(value.state)) return false;
|
|
154
|
+
if (typeof value.launchNonce !== "string" || !value.launchNonce) return false;
|
|
155
|
+
if (typeof value.managedGroup !== "boolean") return false;
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readPidResult(pidFile) {
|
|
161
|
+
let text;
|
|
162
|
+
try {
|
|
163
|
+
text = fs.readFileSync(pidFile, "utf8");
|
|
164
|
+
} catch (error) {
|
|
165
|
+
return error?.code === "ENOENT"
|
|
166
|
+
? { kind: "absent" }
|
|
167
|
+
: { kind: "malformed", error: `cannot read pid record: ${error?.message ?? error}` };
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
const value = JSON.parse(text);
|
|
171
|
+
return validPidRecord(value)
|
|
172
|
+
? { kind: "valid", record: value }
|
|
173
|
+
: { kind: "malformed", error: "malformed pid record" };
|
|
174
|
+
} catch {
|
|
175
|
+
return { kind: "malformed", error: "malformed pid record" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function readPidRecord(pidFile) {
|
|
180
|
+
const result = readPidResult(pidFile);
|
|
181
|
+
return result.kind === "valid" ? result.record : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sameIdentity(a, b) {
|
|
185
|
+
if (!a || !b || a.pid !== b.pid || a.startTime !== b.startTime) return false;
|
|
186
|
+
if (a.launchNonce !== undefined && b.launchNonce !== a.launchNonce) return false;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function atomicWrite(file, value, mode = 0o600) {
|
|
191
|
+
const temp = `${file}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
|
|
192
|
+
fs.writeFileSync(temp, `${JSON.stringify(value)}\n`, { mode });
|
|
193
|
+
fs.renameSync(temp, file);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function writePidRecord(plan, options = {}) {
|
|
197
|
+
const pid = options.pid ?? process.pid;
|
|
198
|
+
const startTime = options.startTime ?? processStartTime(pid);
|
|
199
|
+
if (!startTime) throw new Error("cannot determine process start time; refusing to create an unsafe pid file");
|
|
200
|
+
if (!options.launchNonce) throw new Error("launch nonce is required for a managed pid record");
|
|
201
|
+
fs.mkdirSync(plan.dataDir, { recursive: true });
|
|
202
|
+
const record = {
|
|
203
|
+
version: 2,
|
|
204
|
+
pid,
|
|
205
|
+
startTime,
|
|
206
|
+
host: plan.host,
|
|
207
|
+
port: plan.port,
|
|
208
|
+
state: options.state ?? "starting",
|
|
209
|
+
launchNonce: options.launchNonce,
|
|
210
|
+
managedGroup: options.managedGroup ?? false,
|
|
211
|
+
startedAt: new Date().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
atomicWrite(plan.pidFile, record);
|
|
214
|
+
return record;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function updatePidRecordState(plan, expected, state) {
|
|
218
|
+
const record = readPidRecord(plan.pidFile);
|
|
219
|
+
if (!sameIdentity(expected, record)) throw new Error("pid authority changed during startup");
|
|
220
|
+
const updated = { ...record, state };
|
|
221
|
+
atomicWrite(plan.pidFile, updated);
|
|
222
|
+
return updated;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function clearPidRecord(pidFile, expected) {
|
|
226
|
+
if (expected !== undefined) {
|
|
227
|
+
const record = readPidRecord(pidFile);
|
|
228
|
+
if (typeof expected === "number") {
|
|
229
|
+
if (record?.pid !== expected) return false;
|
|
230
|
+
} else if (!sameIdentity(expected, record)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
fs.rmSync(pidFile);
|
|
236
|
+
return true;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error?.code === "ENOENT") return false;
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function defaultProbe(pid) {
|
|
244
|
+
try {
|
|
245
|
+
process.kill(pid, 0);
|
|
246
|
+
return "alive";
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (error?.code === "ESRCH") return "gone";
|
|
249
|
+
if (error?.code === "EPERM") return "alive";
|
|
250
|
+
return "unknown";
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Whether the exact recorded process still owns this pid. */
|
|
255
|
+
export function recordedProcessState(record, deps = {}) {
|
|
256
|
+
const probe = deps.probe ?? defaultProbe;
|
|
257
|
+
const startTime = deps.startTime ?? processStartTime;
|
|
258
|
+
const probed = probe(record.pid);
|
|
259
|
+
if (probed === "gone" || probed === false) return { state: "gone" };
|
|
260
|
+
if (probed === "unknown") return { state: "unsafe", error: "cannot determine whether the recorded process is alive" };
|
|
261
|
+
const actual = startTime(record.pid);
|
|
262
|
+
if (!actual) return { state: "unsafe", error: "cannot verify the live process start time" };
|
|
263
|
+
return actual === record.startTime
|
|
264
|
+
? { state: "same" }
|
|
265
|
+
: { state: "gone", reused: true };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function pidStatus(pidFile, deps = {}) {
|
|
269
|
+
let result;
|
|
270
|
+
if (deps.read) {
|
|
271
|
+
const record = deps.read(pidFile);
|
|
272
|
+
result = record ? { kind: "valid", record } : { kind: "absent" };
|
|
273
|
+
} else {
|
|
274
|
+
result = readPidResult(pidFile);
|
|
275
|
+
}
|
|
276
|
+
if (result.kind === "absent") return { state: "stopped" };
|
|
277
|
+
if (result.kind === "malformed") return { state: "unsafe", error: result.error };
|
|
278
|
+
const identity = recordedProcessState(result.record, {
|
|
279
|
+
probe: deps.probe ?? (deps.alive ? (pid) => deps.alive(pid) ? "alive" : "gone" : undefined),
|
|
280
|
+
startTime: deps.startTime,
|
|
281
|
+
});
|
|
282
|
+
if (identity.state === "unsafe") return { state: "unsafe", record: result.record, error: identity.error };
|
|
283
|
+
if (identity.state === "gone") {
|
|
284
|
+
const clear = deps.clear ?? (() => clearPidRecord(pidFile, result.record));
|
|
285
|
+
clear(result.record);
|
|
286
|
+
return { state: "stopped", stale: true, record: result.record };
|
|
287
|
+
}
|
|
288
|
+
return { state: "running", record: result.record };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export async function readinessReady(url, expectedNonce, deps = {}) {
|
|
292
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetchImpl(url, { signal: deps.signal ?? AbortSignal.timeout(1_500) });
|
|
295
|
+
if (!response.ok) return false;
|
|
296
|
+
const body = await response.json();
|
|
297
|
+
return body?.ok === true && body?.nonce === expectedNonce;
|
|
298
|
+
} catch {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function waitFor(predicate, timeoutMs, intervalMs = 100) {
|
|
304
|
+
const deadline = Date.now() + timeoutMs;
|
|
305
|
+
while (Date.now() < deadline) {
|
|
306
|
+
if (await predicate()) return true;
|
|
307
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function defaultWaitForGone(record, timeoutMs, deps) {
|
|
313
|
+
const deadline = Date.now() + timeoutMs;
|
|
314
|
+
while (Date.now() < deadline) {
|
|
315
|
+
const state = recordedProcessState(record, deps);
|
|
316
|
+
if (state.state === "gone") return state;
|
|
317
|
+
if (state.state === "unsafe") return state;
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
319
|
+
}
|
|
320
|
+
return recordedProcessState(record, deps);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Signal one verified process (or its managed launch group) and prove it is gone. */
|
|
324
|
+
export async function terminateRecordedProcess(record, deps = {}) {
|
|
325
|
+
const signal = deps.signal ?? ((target, name) => process.kill(target, name));
|
|
326
|
+
const waitForGone = deps.waitForGone ?? ((timeout) => defaultWaitForGone(record, timeout, deps));
|
|
327
|
+
const gracefulTimeoutMs = deps.gracefulTimeoutMs ?? 10_000;
|
|
328
|
+
const forceTimeoutMs = deps.forceTimeoutMs ?? 2_000;
|
|
329
|
+
const target = record.managedGroup ? -record.pid : record.pid;
|
|
330
|
+
const send = (name) => {
|
|
331
|
+
try {
|
|
332
|
+
signal(target, name);
|
|
333
|
+
return true;
|
|
334
|
+
} catch (error) {
|
|
335
|
+
// Exit can race the signal syscall. ESRCH is success only after a fresh
|
|
336
|
+
// identity check positively proves that the recorded process is gone.
|
|
337
|
+
if (error?.code === "ESRCH") {
|
|
338
|
+
const state = recordedProcessState(record, deps);
|
|
339
|
+
if (state.state === "gone") return false;
|
|
340
|
+
if (state.state === "unsafe") throw new Error(state.error);
|
|
341
|
+
}
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const before = recordedProcessState(record, deps);
|
|
347
|
+
if (before.state === "gone") return { forced: false };
|
|
348
|
+
if (before.state === "unsafe") throw new Error(before.error);
|
|
349
|
+
if (!send("SIGTERM")) return { forced: false };
|
|
350
|
+
let after = await waitForGone(gracefulTimeoutMs);
|
|
351
|
+
if (after.state === "gone") return { forced: false };
|
|
352
|
+
if (after.state === "unsafe") throw new Error(after.error);
|
|
353
|
+
|
|
354
|
+
// Re-verify immediately before SIGKILL. Never signal a pid that may have been reused.
|
|
355
|
+
const preKill = recordedProcessState(record, deps);
|
|
356
|
+
if (preKill.state === "gone") return { forced: false };
|
|
357
|
+
if (preKill.state === "unsafe") throw new Error(preKill.error);
|
|
358
|
+
if (!send("SIGKILL")) return { forced: false };
|
|
359
|
+
after = await waitForGone(forceTimeoutMs);
|
|
360
|
+
if (after.state === "unsafe") throw new Error(after.error);
|
|
361
|
+
if (after.state !== "gone") throw new Error(`process ${record.pid} did not exit after SIGKILL`);
|
|
362
|
+
return { forced: true };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function lockOwnerState(lock, deps = {}) {
|
|
366
|
+
if (!Number.isInteger(lock?.pid) || lock.pid <= 0 || typeof lock.startTime !== "string" || !lock.startTime) return undefined;
|
|
367
|
+
return recordedProcessState(lock, deps);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Atomic per-data-dir launch lock. Stale locks recover; live/uncertain owners fail closed. */
|
|
371
|
+
export function acquireStartLock(plan, deps = {}) {
|
|
372
|
+
fs.mkdirSync(plan.dataDir, { recursive: true });
|
|
373
|
+
const now = deps.now ?? Date.now;
|
|
374
|
+
const malformedGraceMs = deps.malformedGraceMs ?? MALFORMED_LOCK_GRACE_MS;
|
|
375
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
376
|
+
const startTime = (deps.startTime ?? processStartTime)(process.pid);
|
|
377
|
+
if (!startTime) throw new Error("cannot determine launcher process start time");
|
|
378
|
+
const token = crypto.randomBytes(18).toString("base64url");
|
|
379
|
+
let fd;
|
|
380
|
+
try {
|
|
381
|
+
fd = fs.openSync(plan.lockFile, "wx", 0o600);
|
|
382
|
+
fs.writeFileSync(fd, `${JSON.stringify({ version: 1, pid: process.pid, startTime, token, createdAt: new Date(now()).toISOString() })}\n`);
|
|
383
|
+
fs.fsyncSync(fd);
|
|
384
|
+
fs.closeSync(fd);
|
|
385
|
+
fd = undefined;
|
|
386
|
+
return () => {
|
|
387
|
+
try {
|
|
388
|
+
const current = JSON.parse(fs.readFileSync(plan.lockFile, "utf8"));
|
|
389
|
+
if (current?.token === token) fs.rmSync(plan.lockFile);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (error?.code !== "ENOENT") throw error;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
} catch (error) {
|
|
395
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch {}
|
|
396
|
+
if (error?.code !== "EEXIST") throw error;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let lock;
|
|
400
|
+
try {
|
|
401
|
+
lock = JSON.parse(fs.readFileSync(plan.lockFile, "utf8"));
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (error?.code === "ENOENT") continue;
|
|
404
|
+
let age = 0;
|
|
405
|
+
try { age = now() - fs.statSync(plan.lockFile).mtimeMs; } catch (statError) {
|
|
406
|
+
if (statError?.code === "ENOENT") continue;
|
|
407
|
+
throw statError;
|
|
408
|
+
}
|
|
409
|
+
if (age >= malformedGraceMs) {
|
|
410
|
+
fs.rmSync(plan.lockFile, { force: true });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const busy = new Error("another pievo-server lifecycle operation is in progress (lock is still being written)");
|
|
414
|
+
busy.code = "START_LOCKED";
|
|
415
|
+
throw busy;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const owner = lockOwnerState(lock, deps);
|
|
419
|
+
if (!owner) {
|
|
420
|
+
const age = now() - fs.statSync(plan.lockFile).mtimeMs;
|
|
421
|
+
if (age >= malformedGraceMs) {
|
|
422
|
+
fs.rmSync(plan.lockFile, { force: true });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
} else if (owner.state === "gone") {
|
|
426
|
+
fs.rmSync(plan.lockFile, { force: true });
|
|
427
|
+
continue;
|
|
428
|
+
} else if (owner.state === "unsafe") {
|
|
429
|
+
throw new Error(`${owner.error}; refusing to recover lifecycle lock for pid ${lock.pid}`);
|
|
430
|
+
}
|
|
431
|
+
const busy = new Error("another pievo-server lifecycle operation is in progress");
|
|
432
|
+
busy.code = "START_LOCKED";
|
|
433
|
+
throw busy;
|
|
434
|
+
}
|
|
435
|
+
throw new Error("could not safely acquire pievo-server lifecycle lock");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export async function acquireStartLockEventually(plan, timeoutMs, deps = {}) {
|
|
439
|
+
let last;
|
|
440
|
+
const ok = await waitFor(() => {
|
|
441
|
+
try {
|
|
442
|
+
last = acquireStartLock(plan, deps);
|
|
443
|
+
return true;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (error?.code !== "START_LOCKED") throw error;
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
}, timeoutMs, deps.intervalMs ?? 100);
|
|
449
|
+
if (!ok) throw new Error("timed out waiting for another pievo-server lifecycle operation");
|
|
450
|
+
return last;
|
|
451
|
+
}
|