@primitive.ai/prim 0.1.0-alpha.41 → 0.1.0-alpha.42
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/README.md +64 -6
- package/dist/{chunk-UWAJCF7P.js → chunk-BSZGI5RL.js} +1 -1
- package/dist/{chunk-S2O4P4A3.js → chunk-F5QCFBJ6.js} +14 -3
- package/dist/{chunk-HSZPTVKU.js → chunk-F7O7V6ZM.js} +88 -12
- package/dist/chunk-IMAIBPUC.js +210 -0
- package/dist/chunk-NDIK4FBF.js +184 -0
- package/dist/{chunk-H4OR42TJ.js → chunk-OKUXEBPT.js} +149 -36
- package/dist/{chunk-AAGJFO7C.js → chunk-QY75BQMF.js} +25 -1
- package/dist/chunk-S5NUHFAD.js +55 -0
- package/dist/{chunk-26VA3ADF.js → chunk-W6PAKEDO.js} +32 -6
- package/dist/daemon/server.js +1150 -77
- package/dist/hooks/post-commit.js +5 -5
- package/dist/hooks/post-tool-use.js +31 -19
- package/dist/hooks/pre-commit.js +2 -2
- package/dist/hooks/pre-tool-use.js +3 -5
- package/dist/hooks/prim-hook.js +77 -20
- package/dist/hooks/session-start.js +79 -24
- package/dist/index.js +1588 -319
- package/dist/statusline-main.js +248 -0
- package/package.json +6 -5
- package/dist/chunk-LUPD2JSH.js +0 -78
package/dist/daemon/server.js
CHANGED
|
@@ -1,16 +1,673 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
getClient,
|
|
4
|
-
getSiteUrl,
|
|
5
|
-
getTokenExpiresAt,
|
|
6
|
-
refreshToken
|
|
7
|
-
} from "../chunk-26VA3ADF.js";
|
|
8
2
|
|
|
9
3
|
// src/daemon/server.ts
|
|
10
|
-
import { existsSync
|
|
4
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
11
5
|
import { createServer } from "net";
|
|
6
|
+
import { homedir as homedir5 } from "os";
|
|
7
|
+
import { join as join6 } from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
|
|
10
|
+
// src/client.ts
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
12
12
|
import { homedir } from "os";
|
|
13
|
-
import { join } from "path";
|
|
13
|
+
import { join, resolve } from "path";
|
|
14
|
+
function loadEnvFile() {
|
|
15
|
+
const envVars = {};
|
|
16
|
+
const candidates = [".env.local", ".env"];
|
|
17
|
+
for (const file of candidates) {
|
|
18
|
+
const filePath = resolve(process.cwd(), file);
|
|
19
|
+
if (existsSync(filePath)) {
|
|
20
|
+
const content = readFileSync(filePath, "utf-8");
|
|
21
|
+
for (const line of content.split("\n")) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
24
|
+
const eqIdx = trimmed.indexOf("=");
|
|
25
|
+
if (eqIdx === -1) continue;
|
|
26
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
27
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
28
|
+
envVars[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return envVars;
|
|
33
|
+
}
|
|
34
|
+
var TOKEN_FILE_PATH = join(homedir(), ".config", "prim", "token");
|
|
35
|
+
var REFRESH_TOKEN_PATH = TOKEN_FILE_PATH.replace("/token", "/refresh_token");
|
|
36
|
+
var TOKEN_EXPIRES_PATH = join(homedir(), ".config", "prim", "token_expires_at");
|
|
37
|
+
var REFRESH_THRESHOLD_MS = 6e4;
|
|
38
|
+
function isTokenExpiringSoon() {
|
|
39
|
+
if (!existsSync(TOKEN_EXPIRES_PATH)) return false;
|
|
40
|
+
const expiresAt = Number(readFileSync(TOKEN_EXPIRES_PATH, "utf-8").trim());
|
|
41
|
+
return !Number.isNaN(expiresAt) && Date.now() >= expiresAt - REFRESH_THRESHOLD_MS;
|
|
42
|
+
}
|
|
43
|
+
function getJwtExpiry(token) {
|
|
44
|
+
const parts = token.split(".");
|
|
45
|
+
if (parts.length !== 3) return void 0;
|
|
46
|
+
try {
|
|
47
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
48
|
+
return payload.exp ? payload.exp * 1e3 : void 0;
|
|
49
|
+
} catch {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function saveTokenExpiry(token, expiresIn) {
|
|
54
|
+
const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : getJwtExpiry(token);
|
|
55
|
+
if (expiresAt) {
|
|
56
|
+
writeFileSync(TOKEN_EXPIRES_PATH, String(expiresAt), { mode: 384 });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getTokenExpiresAt() {
|
|
60
|
+
if (!existsSync(TOKEN_EXPIRES_PATH)) return void 0;
|
|
61
|
+
const val = Number(readFileSync(TOKEN_EXPIRES_PATH, "utf-8").trim());
|
|
62
|
+
return Number.isNaN(val) ? void 0 : val;
|
|
63
|
+
}
|
|
64
|
+
function getAuthToken() {
|
|
65
|
+
if (process.env.PRIM_TOKEN) {
|
|
66
|
+
return process.env.PRIM_TOKEN;
|
|
67
|
+
}
|
|
68
|
+
if (existsSync(TOKEN_FILE_PATH)) {
|
|
69
|
+
const token = readFileSync(TOKEN_FILE_PATH, "utf-8").trim();
|
|
70
|
+
if (token) {
|
|
71
|
+
return token;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const envVars = loadEnvFile();
|
|
75
|
+
if (envVars.PRIM_TOKEN) {
|
|
76
|
+
return envVars.PRIM_TOKEN;
|
|
77
|
+
}
|
|
78
|
+
return void 0;
|
|
79
|
+
}
|
|
80
|
+
var DEFAULT_API_URL = "https://api.getprimitive.ai";
|
|
81
|
+
function getSiteUrl() {
|
|
82
|
+
if (process.env.PRIM_API_URL) {
|
|
83
|
+
return process.env.PRIM_API_URL;
|
|
84
|
+
}
|
|
85
|
+
const envVars = loadEnvFile();
|
|
86
|
+
if (envVars.PRIM_API_URL) {
|
|
87
|
+
return envVars.PRIM_API_URL;
|
|
88
|
+
}
|
|
89
|
+
return DEFAULT_API_URL;
|
|
90
|
+
}
|
|
91
|
+
async function performTokenRefresh(options = {}) {
|
|
92
|
+
if (!existsSync(REFRESH_TOKEN_PATH)) {
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
const refreshTokenValue = readFileSync(REFRESH_TOKEN_PATH, "utf-8").trim();
|
|
96
|
+
if (!refreshTokenValue) {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
const siteUrl = getSiteUrl();
|
|
100
|
+
const response = await fetch(`${siteUrl}/mcp/broker/refresh`, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ refresh_token: refreshTokenValue }),
|
|
104
|
+
signal: options.signal
|
|
105
|
+
});
|
|
106
|
+
if (!response.ok) {
|
|
107
|
+
if (options.quiet) return void 0;
|
|
108
|
+
const detail = (await response.text().catch(() => "")).slice(0, 200);
|
|
109
|
+
process.stderr.write(
|
|
110
|
+
`[prim] token refresh rejected by broker: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}
|
|
111
|
+
`
|
|
112
|
+
);
|
|
113
|
+
return void 0;
|
|
114
|
+
}
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
if (!data.access_token) {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
writeFileSync(TOKEN_FILE_PATH, data.access_token, { mode: 384 });
|
|
120
|
+
if (data.refresh_token) {
|
|
121
|
+
writeFileSync(REFRESH_TOKEN_PATH, data.refresh_token, { mode: 384 });
|
|
122
|
+
}
|
|
123
|
+
saveTokenExpiry(data.access_token, data.expires_in);
|
|
124
|
+
return data.access_token;
|
|
125
|
+
}
|
|
126
|
+
var _refreshInFlight;
|
|
127
|
+
function refreshToken(options = {}) {
|
|
128
|
+
if (_refreshInFlight) {
|
|
129
|
+
return _refreshInFlight;
|
|
130
|
+
}
|
|
131
|
+
const attempt = performTokenRefresh(options).then((token) => {
|
|
132
|
+
if (token) {
|
|
133
|
+
_cachedToken = token;
|
|
134
|
+
}
|
|
135
|
+
return token;
|
|
136
|
+
}).finally(() => {
|
|
137
|
+
if (_refreshInFlight === attempt) {
|
|
138
|
+
_refreshInFlight = void 0;
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
_refreshInFlight = attempt;
|
|
142
|
+
return attempt;
|
|
143
|
+
}
|
|
144
|
+
var HttpError = class extends Error {
|
|
145
|
+
status;
|
|
146
|
+
constructor(status, message) {
|
|
147
|
+
super(message);
|
|
148
|
+
this.name = "HttpError";
|
|
149
|
+
this.status = status;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
var _cachedToken;
|
|
153
|
+
async function request(method, path, body, options) {
|
|
154
|
+
const siteUrl = getSiteUrl();
|
|
155
|
+
const url = `${siteUrl}${path}`;
|
|
156
|
+
if (!_cachedToken) {
|
|
157
|
+
_cachedToken = getAuthToken();
|
|
158
|
+
}
|
|
159
|
+
if (_cachedToken && isTokenExpiringSoon()) {
|
|
160
|
+
const newToken = await refreshToken({
|
|
161
|
+
signal: options?.signal,
|
|
162
|
+
quiet: options?.quietRefresh
|
|
163
|
+
});
|
|
164
|
+
if (newToken) {
|
|
165
|
+
_cachedToken = newToken;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const doFetch = async (token) => {
|
|
169
|
+
const headers = {
|
|
170
|
+
"Content-Type": "application/json"
|
|
171
|
+
};
|
|
172
|
+
if (token) {
|
|
173
|
+
headers.Authorization = `Bearer ${token}`;
|
|
174
|
+
}
|
|
175
|
+
return fetch(url, {
|
|
176
|
+
method,
|
|
177
|
+
headers,
|
|
178
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0,
|
|
179
|
+
signal: options?.signal
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
const tokenUsed = _cachedToken;
|
|
183
|
+
let res = await doFetch(tokenUsed);
|
|
184
|
+
if (res.status === 401) {
|
|
185
|
+
const newToken = _cachedToken && _cachedToken !== tokenUsed ? _cachedToken : await refreshToken({
|
|
186
|
+
signal: options?.signal,
|
|
187
|
+
quiet: options?.quietRefresh
|
|
188
|
+
});
|
|
189
|
+
if (newToken) {
|
|
190
|
+
_cachedToken = newToken;
|
|
191
|
+
res = await doFetch(newToken);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!res.ok) {
|
|
195
|
+
if (res.status === 401) {
|
|
196
|
+
throw new HttpError(401, "Authentication expired. Run `prim auth login` to re-authenticate.");
|
|
197
|
+
}
|
|
198
|
+
const errorBody = await res.json().catch(() => null);
|
|
199
|
+
throw new HttpError(res.status, errorBody?.error ?? `HTTP ${res.status}`);
|
|
200
|
+
}
|
|
201
|
+
return res.json();
|
|
202
|
+
}
|
|
203
|
+
function getClient() {
|
|
204
|
+
return {
|
|
205
|
+
get: (path, options) => request("GET", path, void 0, options),
|
|
206
|
+
post: (path, body, options) => request("POST", path, body, options)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/flusher.ts
|
|
211
|
+
import { createReadStream, renameSync, unlinkSync } from "fs";
|
|
212
|
+
import { createInterface } from "readline";
|
|
213
|
+
|
|
214
|
+
// src/ingest-response.ts
|
|
215
|
+
var IngestAcknowledgementError = class extends Error {
|
|
216
|
+
constructor(message) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.name = "IngestAcknowledgementError";
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function requireDurableIngestAcknowledgement(value, expected) {
|
|
222
|
+
if (typeof value !== "object" || value === null) {
|
|
223
|
+
throw new IngestAcknowledgementError("ingest response was not an object");
|
|
224
|
+
}
|
|
225
|
+
const response = value;
|
|
226
|
+
if (response.disposition !== "persisted") {
|
|
227
|
+
throw new IngestAcknowledgementError("ingest response did not confirm persistence");
|
|
228
|
+
}
|
|
229
|
+
if (response.acknowledged !== expected) {
|
|
230
|
+
throw new IngestAcknowledgementError(
|
|
231
|
+
`ingest acknowledged ${String(response.acknowledged)} of ${String(expected)} moves`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return value;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/journal.ts
|
|
238
|
+
import {
|
|
239
|
+
appendFileSync,
|
|
240
|
+
closeSync,
|
|
241
|
+
existsSync as existsSync2,
|
|
242
|
+
fstatSync,
|
|
243
|
+
mkdirSync,
|
|
244
|
+
openSync,
|
|
245
|
+
opendirSync,
|
|
246
|
+
readFileSync as readFileSync2,
|
|
247
|
+
readSync,
|
|
248
|
+
readdirSync
|
|
249
|
+
} from "fs";
|
|
250
|
+
import { homedir as homedir2 } from "os";
|
|
251
|
+
import { dirname, join as join2 } from "path";
|
|
252
|
+
var JOURNAL_DIR = join2(homedir2(), ".config", "prim", "moves");
|
|
253
|
+
var JOURNAL_BASENAME = "journal.ndjson";
|
|
254
|
+
var JOURNAL_STATS_SAMPLE_BYTES = 64 * 1024;
|
|
255
|
+
var PENDING_STATS_MAX_FILES = 128;
|
|
256
|
+
function envSlug(apiUrl) {
|
|
257
|
+
const slug = apiUrl.replace(/^https?:\/\//, "").replace(/[^a-zA-Z0-9._-]/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
|
|
258
|
+
if (slug.length === 0 || slug === "." || slug === "..") {
|
|
259
|
+
return "default";
|
|
260
|
+
}
|
|
261
|
+
return slug;
|
|
262
|
+
}
|
|
263
|
+
function currentEnvDir() {
|
|
264
|
+
return join2(JOURNAL_DIR, envSlug(getSiteUrl()));
|
|
265
|
+
}
|
|
266
|
+
function parseMoves(content) {
|
|
267
|
+
const moves = [];
|
|
268
|
+
for (const line of content.split("\n")) {
|
|
269
|
+
if (line.length === 0) {
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
moves.push(JSON.parse(line));
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return moves;
|
|
278
|
+
}
|
|
279
|
+
function listBuckets() {
|
|
280
|
+
const out = [];
|
|
281
|
+
const envDir = currentEnvDir();
|
|
282
|
+
if (!existsSync2(envDir)) {
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
286
|
+
if (!entry.isDirectory()) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const path = join2(envDir, entry.name, JOURNAL_BASENAME);
|
|
290
|
+
if (existsSync2(path)) {
|
|
291
|
+
out.push({ bucket: entry.name, path });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
var FLUSHING_PREFIX = `${JOURNAL_BASENAME}.flushing.`;
|
|
297
|
+
function oldestCapturedAt(moves) {
|
|
298
|
+
let oldest;
|
|
299
|
+
for (const move of moves) {
|
|
300
|
+
if (!Number.isFinite(move.capturedAt)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
oldest = oldest === void 0 ? move.capturedAt : Math.min(oldest, move.capturedAt);
|
|
304
|
+
}
|
|
305
|
+
return oldest;
|
|
306
|
+
}
|
|
307
|
+
function sampleJournalFile(path, maxBytes = JOURNAL_STATS_SAMPLE_BYTES) {
|
|
308
|
+
const fd = openSync(path, "r");
|
|
309
|
+
try {
|
|
310
|
+
const initial = fstatSync(fd);
|
|
311
|
+
const target = Math.min(initial.size, Math.max(0, maxBytes));
|
|
312
|
+
const buffer = Buffer.allocUnsafe(target);
|
|
313
|
+
let sampledBytes = 0;
|
|
314
|
+
while (sampledBytes < target) {
|
|
315
|
+
const count = readSync(fd, buffer, sampledBytes, target - sampledBytes, sampledBytes);
|
|
316
|
+
if (count === 0) {
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
sampledBytes += count;
|
|
320
|
+
}
|
|
321
|
+
const stat = fstatSync(fd);
|
|
322
|
+
const sampled = stat.size > sampledBytes;
|
|
323
|
+
let content = buffer.subarray(0, sampledBytes).toString("utf-8");
|
|
324
|
+
if (sampled) {
|
|
325
|
+
const lastNewline = content.lastIndexOf("\n");
|
|
326
|
+
content = lastNewline === -1 ? "" : content.slice(0, lastNewline + 1);
|
|
327
|
+
}
|
|
328
|
+
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
329
|
+
const moves = parseMoves(content);
|
|
330
|
+
return {
|
|
331
|
+
sizeBytes: stat.size,
|
|
332
|
+
mtimeMs: stat.mtimeMs,
|
|
333
|
+
lineCount: sampled && stat.size > 0 ? Math.max(1, lines.length) : lines.length,
|
|
334
|
+
oldestCapturedAt: oldestCapturedAt(moves),
|
|
335
|
+
sampled,
|
|
336
|
+
sampledBytes
|
|
337
|
+
};
|
|
338
|
+
} finally {
|
|
339
|
+
closeSync(fd);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function parseFlushingPid(name) {
|
|
343
|
+
const segments = name.slice(FLUSHING_PREFIX.length).split(".");
|
|
344
|
+
const last = segments.length >= 2 ? segments[segments.length - 1] : void 0;
|
|
345
|
+
return last !== void 0 && /^[0-9]+$/.test(last) ? Number(last) : void 0;
|
|
346
|
+
}
|
|
347
|
+
function listFlushingInDir(dir, bucket, options = {}) {
|
|
348
|
+
if (!existsSync2(dir)) {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
const out = [];
|
|
352
|
+
let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
|
|
353
|
+
const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
354
|
+
const entries = opendirSync(dir);
|
|
355
|
+
try {
|
|
356
|
+
let entry = entries.readSync();
|
|
357
|
+
while (entry && out.length < maxFiles) {
|
|
358
|
+
if (!entry.isFile() || !entry.name.startsWith(FLUSHING_PREFIX)) {
|
|
359
|
+
entry = entries.readSync();
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const path = join2(dir, entry.name);
|
|
363
|
+
let sample;
|
|
364
|
+
try {
|
|
365
|
+
sample = sampleJournalFile(path, remainingBytes);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
if (err.code === "ENOENT") {
|
|
368
|
+
entry = entries.readSync();
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
373
|
+
remainingBytes = Math.max(0, remainingBytes - sample.sampledBytes);
|
|
374
|
+
out.push({
|
|
375
|
+
bucket,
|
|
376
|
+
path,
|
|
377
|
+
pid: parseFlushingPid(entry.name),
|
|
378
|
+
...sample
|
|
379
|
+
});
|
|
380
|
+
entry = entries.readSync();
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
entries.closeSync();
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
}
|
|
387
|
+
function listFlushing(options = {}) {
|
|
388
|
+
const envDir = currentEnvDir();
|
|
389
|
+
if (!existsSync2(envDir)) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
const out = [];
|
|
393
|
+
let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
|
|
394
|
+
const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
395
|
+
for (const entry of readdirSync(envDir, { withFileTypes: true })) {
|
|
396
|
+
if (out.length >= maxFiles) {
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
if (entry.isDirectory()) {
|
|
400
|
+
const found = listFlushingInDir(join2(envDir, entry.name), entry.name, {
|
|
401
|
+
sampleBytes: remainingBytes,
|
|
402
|
+
maxFiles: maxFiles - out.length
|
|
403
|
+
});
|
|
404
|
+
out.push(...found);
|
|
405
|
+
remainingBytes = Math.max(
|
|
406
|
+
0,
|
|
407
|
+
remainingBytes - found.reduce((count, file) => count + file.sampledBytes, 0)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
function bucketStats(options = {}) {
|
|
414
|
+
const out = [];
|
|
415
|
+
let remainingBytes = options.sampleBytes ?? JOURNAL_STATS_SAMPLE_BYTES;
|
|
416
|
+
const maxFiles = options.maxFiles ?? Number.POSITIVE_INFINITY;
|
|
417
|
+
for (const { bucket, path } of listBuckets()) {
|
|
418
|
+
if (out.length >= maxFiles) {
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
let sample;
|
|
422
|
+
try {
|
|
423
|
+
sample = sampleJournalFile(path, remainingBytes);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
if (err.code === "ENOENT") {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
throw err;
|
|
429
|
+
}
|
|
430
|
+
remainingBytes = Math.max(0, remainingBytes - sample.sampledBytes);
|
|
431
|
+
out.push({
|
|
432
|
+
bucket,
|
|
433
|
+
path,
|
|
434
|
+
...sample
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
function pendingJournalStats() {
|
|
440
|
+
const perKindBytes = Math.floor(JOURNAL_STATS_SAMPLE_BYTES / 2);
|
|
441
|
+
const perKindFiles = Math.floor(PENDING_STATS_MAX_FILES / 2);
|
|
442
|
+
const live = bucketStats({ sampleBytes: perKindBytes, maxFiles: perKindFiles });
|
|
443
|
+
const stranded = listFlushing({ sampleBytes: perKindBytes, maxFiles: perKindFiles });
|
|
444
|
+
const timestamps = [...live, ...stranded].map((entry) => entry.oldestCapturedAt).filter((value) => value !== void 0);
|
|
445
|
+
const liveSampled = live.some((entry) => entry.sampled) || live.length >= perKindFiles;
|
|
446
|
+
const strandedSampled = stranded.some((entry) => entry.sampled) || stranded.length >= perKindFiles;
|
|
447
|
+
return {
|
|
448
|
+
pendingCount: live.reduce((count, entry) => count + entry.lineCount, 0) + stranded.reduce((count, entry) => count + entry.lineCount, 0),
|
|
449
|
+
oldestPendingAt: timestamps.length > 0 ? Math.min(...timestamps) : void 0,
|
|
450
|
+
strandedCount: stranded.reduce((count, entry) => count + entry.lineCount, 0),
|
|
451
|
+
strandedFileCount: stranded.length,
|
|
452
|
+
sampled: liveSampled || strandedSampled,
|
|
453
|
+
strandedSampled
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/flusher.ts
|
|
458
|
+
var BATCH_SIZE = 500;
|
|
459
|
+
var HTTP_TIMEOUT_MS = 1e4;
|
|
460
|
+
var ORPHAN_QUARANTINE_MS = 6e4;
|
|
461
|
+
async function drainFlushingPath(flushingPath, client2 = getClient()) {
|
|
462
|
+
const postBatch = async (batch2) => {
|
|
463
|
+
const response = await client2.post(
|
|
464
|
+
"/api/cli/moves/ingest",
|
|
465
|
+
{ batch: batch2 },
|
|
466
|
+
{ signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) }
|
|
467
|
+
);
|
|
468
|
+
requireDurableIngestAcknowledgement(response, batch2.length);
|
|
469
|
+
};
|
|
470
|
+
const input = createReadStream(flushingPath, { encoding: "utf-8" });
|
|
471
|
+
const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
|
|
472
|
+
let batch = [];
|
|
473
|
+
let total = 0;
|
|
474
|
+
try {
|
|
475
|
+
for await (const line of lines) {
|
|
476
|
+
if (line.length === 0) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
batch.push(JSON.parse(line));
|
|
481
|
+
} catch {
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
if (batch.length === BATCH_SIZE) {
|
|
485
|
+
await postBatch(batch);
|
|
486
|
+
total += batch.length;
|
|
487
|
+
batch = [];
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
if (batch.length > 0) {
|
|
491
|
+
await postBatch(batch);
|
|
492
|
+
total += batch.length;
|
|
493
|
+
}
|
|
494
|
+
} finally {
|
|
495
|
+
lines.close();
|
|
496
|
+
input.destroy();
|
|
497
|
+
}
|
|
498
|
+
unlinkSync(flushingPath);
|
|
499
|
+
return total;
|
|
500
|
+
}
|
|
501
|
+
async function drainPath(path) {
|
|
502
|
+
const tmpPath = `${path}.flushing.${String(Date.now())}.${String(process.pid)}`;
|
|
503
|
+
try {
|
|
504
|
+
renameSync(path, tmpPath);
|
|
505
|
+
} catch (err) {
|
|
506
|
+
if (err.code === "ENOENT") {
|
|
507
|
+
return 0;
|
|
508
|
+
}
|
|
509
|
+
throw err;
|
|
510
|
+
}
|
|
511
|
+
return drainFlushingPath(tmpPath);
|
|
512
|
+
}
|
|
513
|
+
function processIsAlive(pid) {
|
|
514
|
+
try {
|
|
515
|
+
process.kill(pid, 0);
|
|
516
|
+
return true;
|
|
517
|
+
} catch {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
function selectRecoverable(files, now, opts = {}) {
|
|
522
|
+
const quarantineMs = opts.quarantineMs ?? ORPHAN_QUARANTINE_MS;
|
|
523
|
+
const isAlive = opts.isAlive ?? processIsAlive;
|
|
524
|
+
return files.filter((f) => {
|
|
525
|
+
if (f.pid === void 0) {
|
|
526
|
+
return now - f.mtimeMs > quarantineMs;
|
|
527
|
+
}
|
|
528
|
+
return f.pid === opts.ownerPid || !isAlive(f.pid);
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async function recoverOrphans(candidates = listFlushing({ sampleBytes: 0 }), options = {}) {
|
|
532
|
+
const summary = { flushed: 0, errors: [], failedBuckets: /* @__PURE__ */ new Set() };
|
|
533
|
+
const recoverable = selectRecoverable(candidates, options.now ?? Date.now(), {
|
|
534
|
+
ownerPid: options.ownerPid ?? process.pid,
|
|
535
|
+
isAlive: options.isAlive
|
|
536
|
+
}).sort((a, b) => a.mtimeMs - b.mtimeMs || a.path.localeCompare(b.path));
|
|
537
|
+
const drain = options.drain ?? drainFlushingPath;
|
|
538
|
+
for (const file of recoverable) {
|
|
539
|
+
if (summary.failedBuckets.has(file.bucket)) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
try {
|
|
543
|
+
summary.flushed += await drain(file.path);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
summary.errors.push(err);
|
|
546
|
+
summary.failedBuckets.add(file.bucket);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return summary;
|
|
550
|
+
}
|
|
551
|
+
var FlushError = class extends Error {
|
|
552
|
+
flushed;
|
|
553
|
+
constructor(cause, flushed) {
|
|
554
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
555
|
+
this.name = "FlushError";
|
|
556
|
+
this.flushed = flushed;
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
async function flushOnce() {
|
|
560
|
+
const recovered = await recoverOrphans();
|
|
561
|
+
let total = recovered.flushed;
|
|
562
|
+
const errors = recovered.errors;
|
|
563
|
+
for (const { bucket, path } of listBuckets()) {
|
|
564
|
+
if (recovered.failedBuckets.has(bucket)) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
total += await drainPath(path);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
errors.push(err);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (errors.length > 0) {
|
|
574
|
+
throw new FlushError(errors[0], total);
|
|
575
|
+
}
|
|
576
|
+
return { flushed: total };
|
|
577
|
+
}
|
|
578
|
+
var flushInFlight;
|
|
579
|
+
function flush() {
|
|
580
|
+
if (flushInFlight) {
|
|
581
|
+
return flushInFlight;
|
|
582
|
+
}
|
|
583
|
+
const attempt = flushOnce().finally(() => {
|
|
584
|
+
if (flushInFlight === attempt) {
|
|
585
|
+
flushInFlight = void 0;
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
flushInFlight = attempt;
|
|
589
|
+
return attempt;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/daemon/client.ts
|
|
593
|
+
import { createConnection } from "net";
|
|
594
|
+
import { homedir as homedir3 } from "os";
|
|
595
|
+
import { join as join3 } from "path";
|
|
596
|
+
var SOCK_PATH = join3(homedir3(), ".config", "prim", "sock");
|
|
597
|
+
var DEFAULT_TIMEOUT_MS = 250;
|
|
598
|
+
var nextRequestId = 1;
|
|
599
|
+
function daemonRequest(method, params = {}, opts = {}) {
|
|
600
|
+
const id = nextRequestId++;
|
|
601
|
+
const request2 = { id, method, params };
|
|
602
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
603
|
+
return new Promise((resolve2) => {
|
|
604
|
+
let settled = false;
|
|
605
|
+
let socket;
|
|
606
|
+
let buffer = "";
|
|
607
|
+
const settle = (value) => {
|
|
608
|
+
if (settled) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
settled = true;
|
|
612
|
+
if (socket) {
|
|
613
|
+
try {
|
|
614
|
+
socket.destroy();
|
|
615
|
+
} catch {
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
resolve2(value);
|
|
619
|
+
};
|
|
620
|
+
const timer = setTimeout(() => settle(null), timeoutMs);
|
|
621
|
+
timer.unref();
|
|
622
|
+
try {
|
|
623
|
+
socket = createConnection(SOCK_PATH);
|
|
624
|
+
} catch {
|
|
625
|
+
clearTimeout(timer);
|
|
626
|
+
settle(null);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
socket.on("error", () => {
|
|
630
|
+
clearTimeout(timer);
|
|
631
|
+
settle(null);
|
|
632
|
+
});
|
|
633
|
+
socket.on("connect", () => {
|
|
634
|
+
try {
|
|
635
|
+
socket?.write(`${JSON.stringify(request2)}
|
|
636
|
+
`);
|
|
637
|
+
} catch {
|
|
638
|
+
clearTimeout(timer);
|
|
639
|
+
settle(null);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
socket.on("data", (chunk) => {
|
|
643
|
+
buffer += chunk.toString("utf-8");
|
|
644
|
+
const newlineIdx = buffer.indexOf("\n");
|
|
645
|
+
if (newlineIdx === -1) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const line = buffer.slice(0, newlineIdx);
|
|
649
|
+
try {
|
|
650
|
+
const res = JSON.parse(line);
|
|
651
|
+
if (res.id !== id) {
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
settle(null);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
clearTimeout(timer);
|
|
657
|
+
settle(res.ok && res.result !== void 0 ? res.result : null);
|
|
658
|
+
} catch {
|
|
659
|
+
clearTimeout(timer);
|
|
660
|
+
settle(null);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
socket.on("end", () => {
|
|
664
|
+
if (!settled) {
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
settle(null);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
}
|
|
14
671
|
|
|
15
672
|
// src/daemon/env-binding.ts
|
|
16
673
|
function normalizeApiUrl(url) {
|
|
@@ -28,21 +685,242 @@ function assertCallerEnvMatches(callerEnv, boundEnv) {
|
|
|
28
685
|
}
|
|
29
686
|
}
|
|
30
687
|
|
|
688
|
+
// src/daemon/health.ts
|
|
689
|
+
import { chmodSync, existsSync as existsSync3, mkdirSync as mkdirSync2, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
690
|
+
import { homedir as homedir4 } from "os";
|
|
691
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
692
|
+
var CONFIG_DIR_MODE = 448;
|
|
693
|
+
var HEALTH_FILE_MODE = 384;
|
|
694
|
+
var INGESTION_SLA_MS = 3e4;
|
|
695
|
+
var HEARTBEAT_FRESH_MS = 9e4;
|
|
696
|
+
var HEARTBEAT_RETRY_CAP_MS = 3e4;
|
|
697
|
+
var INGESTION_RETRY_BASE_MS = 5e3;
|
|
698
|
+
var INGESTION_RETRY_CAP_MS = 15e3;
|
|
699
|
+
var DAEMON_HEALTH_PATH = join4(homedir4(), ".config", "prim", "daemon-health.json");
|
|
700
|
+
function createDaemonHealthState(version, pid, startedAt2) {
|
|
701
|
+
return {
|
|
702
|
+
schemaVersion: 1,
|
|
703
|
+
version,
|
|
704
|
+
pid,
|
|
705
|
+
startedAt: startedAt2,
|
|
706
|
+
updatedAt: startedAt2,
|
|
707
|
+
healthy: false,
|
|
708
|
+
heartbeat: { healthy: false, consecutiveFailures: 0 },
|
|
709
|
+
ingestion: {
|
|
710
|
+
healthy: true,
|
|
711
|
+
consecutiveFailures: 0,
|
|
712
|
+
pendingCount: 0,
|
|
713
|
+
pendingSampled: false,
|
|
714
|
+
strandedCount: 0,
|
|
715
|
+
lastAcknowledgedCount: 0
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function refreshDaemonHealth(state, now) {
|
|
720
|
+
state.heartbeat.healthy = state.heartbeat.consecutiveFailures === 0 && state.heartbeat.lastSuccessAt !== void 0 && now - state.heartbeat.lastSuccessAt < HEARTBEAT_FRESH_MS;
|
|
721
|
+
state.ingestion.healthy = state.ingestion.consecutiveFailures === 0 && !state.ingestion.pendingSampled && (state.ingestion.pendingCount === 0 || state.ingestion.oldestPendingAt !== void 0 && now - state.ingestion.oldestPendingAt <= INGESTION_SLA_MS);
|
|
722
|
+
state.healthy = state.heartbeat.healthy && state.ingestion.healthy;
|
|
723
|
+
}
|
|
724
|
+
function retryDelayMs(consecutiveFailures, capMs, random) {
|
|
725
|
+
const exponent = Math.max(0, consecutiveFailures - 1);
|
|
726
|
+
const base = Math.min(capMs, INGESTION_RETRY_BASE_MS * 2 ** exponent);
|
|
727
|
+
const jitter = 0.8 + Math.min(1, Math.max(0, random())) * 0.4;
|
|
728
|
+
return Math.min(capMs, Math.round(base * jitter));
|
|
729
|
+
}
|
|
730
|
+
function heartbeatRetryDelayMs(consecutiveFailures, random = Math.random) {
|
|
731
|
+
return retryDelayMs(consecutiveFailures, HEARTBEAT_RETRY_CAP_MS, random);
|
|
732
|
+
}
|
|
733
|
+
function ingestionRetryDelayMs(consecutiveFailures, random = Math.random) {
|
|
734
|
+
return retryDelayMs(consecutiveFailures, INGESTION_RETRY_CAP_MS, random);
|
|
735
|
+
}
|
|
736
|
+
function writeDaemonHealthState(state, path = DAEMON_HEALTH_PATH) {
|
|
737
|
+
const dir = dirname2(path);
|
|
738
|
+
if (!existsSync3(dir)) {
|
|
739
|
+
mkdirSync2(dir, { recursive: true, mode: CONFIG_DIR_MODE });
|
|
740
|
+
}
|
|
741
|
+
const tmp = join4(dir, `.${process.pid}.daemon-health.tmp`);
|
|
742
|
+
writeFileSync2(tmp, `${JSON.stringify(state, null, 2)}
|
|
743
|
+
`, { mode: HEALTH_FILE_MODE });
|
|
744
|
+
chmodSync(tmp, HEALTH_FILE_MODE);
|
|
745
|
+
renameSync2(tmp, path);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// src/daemon/instance-lock.ts
|
|
749
|
+
import { randomUUID } from "crypto";
|
|
750
|
+
import {
|
|
751
|
+
chmodSync as chmodSync2,
|
|
752
|
+
existsSync as existsSync4,
|
|
753
|
+
mkdirSync as mkdirSync3,
|
|
754
|
+
readFileSync as readFileSync3,
|
|
755
|
+
renameSync as renameSync3,
|
|
756
|
+
rmSync,
|
|
757
|
+
statSync,
|
|
758
|
+
unlinkSync as unlinkSync2,
|
|
759
|
+
writeFileSync as writeFileSync3
|
|
760
|
+
} from "fs";
|
|
761
|
+
import { join as join5 } from "path";
|
|
762
|
+
var DIR_MODE = 448;
|
|
763
|
+
var FILE_MODE = 384;
|
|
764
|
+
var DAEMON_STARTUP_GRACE_MS = 5e3;
|
|
765
|
+
function readLockRecord(path) {
|
|
766
|
+
try {
|
|
767
|
+
const value = JSON.parse(readFileSync3(path, "utf-8"));
|
|
768
|
+
if (Number.isInteger(value.pid) && typeof value.instanceId === "string" && typeof value.startedAt === "number") {
|
|
769
|
+
return value;
|
|
770
|
+
}
|
|
771
|
+
} catch {
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
function numericPid(path) {
|
|
776
|
+
try {
|
|
777
|
+
const pid = Number(readFileSync3(path, "utf-8").trim());
|
|
778
|
+
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
779
|
+
} catch {
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function daemonProcessIsAlive(pid) {
|
|
784
|
+
try {
|
|
785
|
+
process.kill(pid, 0);
|
|
786
|
+
return true;
|
|
787
|
+
} catch {
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function readDaemonOwner(configDir) {
|
|
792
|
+
return readLockRecord(join5(configDir, "daemon.lock", "owner.json"));
|
|
793
|
+
}
|
|
794
|
+
function readDaemonPid(configDir) {
|
|
795
|
+
const path = join5(configDir, "daemon.pid");
|
|
796
|
+
const pid = numericPid(path);
|
|
797
|
+
if (pid === void 0) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
return { pid, mtimeMs: statSync(path).mtimeMs };
|
|
802
|
+
} catch {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function acquireDaemonOwnership(configDir, opts = {}) {
|
|
807
|
+
const pid = opts.pid ?? process.pid;
|
|
808
|
+
const now = opts.now ?? Date.now();
|
|
809
|
+
const isAlive = opts.isAlive ?? daemonProcessIsAlive;
|
|
810
|
+
const lockDir = join5(configDir, "daemon.lock");
|
|
811
|
+
const ownerPath = join5(lockDir, "owner.json");
|
|
812
|
+
const pidPath = join5(configDir, "daemon.pid");
|
|
813
|
+
const instanceId = randomUUID();
|
|
814
|
+
if (!existsSync4(configDir)) {
|
|
815
|
+
mkdirSync3(configDir, { recursive: true, mode: DIR_MODE });
|
|
816
|
+
}
|
|
817
|
+
for (; ; ) {
|
|
818
|
+
try {
|
|
819
|
+
mkdirSync3(lockDir, { mode: DIR_MODE });
|
|
820
|
+
break;
|
|
821
|
+
} catch (err) {
|
|
822
|
+
if (err.code !== "EEXIST") {
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
825
|
+
const existing = readLockRecord(ownerPath);
|
|
826
|
+
if (existing && isAlive(existing.pid)) {
|
|
827
|
+
throw new Error(`daemon already running (pid=${String(existing.pid)})`);
|
|
828
|
+
}
|
|
829
|
+
if (!existing) {
|
|
830
|
+
let mtimeMs;
|
|
831
|
+
try {
|
|
832
|
+
mtimeMs = statSync(lockDir).mtimeMs;
|
|
833
|
+
} catch (statErr) {
|
|
834
|
+
if (statErr.code === "ENOENT") {
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
throw statErr;
|
|
838
|
+
}
|
|
839
|
+
if (now - mtimeMs < DAEMON_STARTUP_GRACE_MS) {
|
|
840
|
+
throw new Error("daemon startup already in progress");
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
const stale = `${lockDir}.stale.${String(pid)}.${instanceId}`;
|
|
844
|
+
try {
|
|
845
|
+
renameSync3(lockDir, stale);
|
|
846
|
+
rmSync(stale, { recursive: true, force: true });
|
|
847
|
+
} catch (renameErr) {
|
|
848
|
+
if (renameErr.code !== "ENOENT") {
|
|
849
|
+
throw renameErr;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
const ownership2 = {
|
|
855
|
+
configDir,
|
|
856
|
+
lockDir,
|
|
857
|
+
ownerPath,
|
|
858
|
+
pidPath,
|
|
859
|
+
pid,
|
|
860
|
+
instanceId
|
|
861
|
+
};
|
|
862
|
+
const record = { pid, instanceId, startedAt: now };
|
|
863
|
+
writeFileSync3(ownerPath, `${JSON.stringify(record)}
|
|
864
|
+
`, { mode: FILE_MODE });
|
|
865
|
+
chmodSync2(ownerPath, FILE_MODE);
|
|
866
|
+
const legacyPid = numericPid(pidPath);
|
|
867
|
+
if (legacyPid !== void 0 && legacyPid !== pid && isAlive(legacyPid)) {
|
|
868
|
+
rmSync(lockDir, { recursive: true, force: true });
|
|
869
|
+
throw new Error(`daemon already running (pid=${String(legacyPid)})`);
|
|
870
|
+
}
|
|
871
|
+
writeFileSync3(pidPath, String(pid), { mode: FILE_MODE });
|
|
872
|
+
chmodSync2(pidPath, FILE_MODE);
|
|
873
|
+
return ownership2;
|
|
874
|
+
}
|
|
875
|
+
function ownsDaemonInstance(ownership2) {
|
|
876
|
+
const current = readLockRecord(ownership2.ownerPath);
|
|
877
|
+
return current?.pid === ownership2.pid && current.instanceId === ownership2.instanceId;
|
|
878
|
+
}
|
|
879
|
+
function releaseDaemonOwnership(ownership2, socketPath) {
|
|
880
|
+
if (!ownsDaemonInstance(ownership2)) {
|
|
881
|
+
return false;
|
|
882
|
+
}
|
|
883
|
+
let cleanupError;
|
|
884
|
+
if (socketPath) {
|
|
885
|
+
try {
|
|
886
|
+
unlinkSync2(socketPath);
|
|
887
|
+
} catch (err) {
|
|
888
|
+
if (err.code !== "ENOENT") {
|
|
889
|
+
cleanupError = err;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
if (numericPid(ownership2.pidPath) === ownership2.pid) {
|
|
894
|
+
try {
|
|
895
|
+
unlinkSync2(ownership2.pidPath);
|
|
896
|
+
} catch (err) {
|
|
897
|
+
if (err.code !== "ENOENT") {
|
|
898
|
+
cleanupError ??= err;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
rmSync(ownership2.lockDir, { recursive: true, force: true });
|
|
903
|
+
if (cleanupError) {
|
|
904
|
+
throw cleanupError;
|
|
905
|
+
}
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
|
|
31
909
|
// src/daemon/server.ts
|
|
32
|
-
var CONFIG_DIR =
|
|
33
|
-
var
|
|
34
|
-
var PID_PATH = join(CONFIG_DIR, "daemon.pid");
|
|
910
|
+
var CONFIG_DIR = join6(homedir5(), ".config", "prim");
|
|
911
|
+
var SOCK_PATH2 = join6(CONFIG_DIR, "sock");
|
|
35
912
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
913
|
+
var INGESTION_POLL_INTERVAL_MS = 15e3;
|
|
36
914
|
var TOKEN_CHECK_INTERVAL_MS = 6e4;
|
|
37
915
|
var TOKEN_REFRESH_THRESHOLD_MS = 9e4;
|
|
38
916
|
var HTTP_PROXY_TIMEOUT_MS = 1e4;
|
|
39
917
|
var PRESENCE_FRESH_WINDOW_MS = 9e4;
|
|
40
|
-
var SOCKET_DIR_MODE = 448;
|
|
41
|
-
var PID_FILE_MODE = 384;
|
|
42
918
|
var EXIT_OK = 0;
|
|
43
919
|
var EXIT_CRASH = 1;
|
|
44
920
|
var startedAt = Date.now();
|
|
45
921
|
var client = getClient();
|
|
922
|
+
var runtimeVersion = resolveRuntimeVersion();
|
|
923
|
+
var daemonHealth = createDaemonHealthState(runtimeVersion, process.pid, startedAt);
|
|
46
924
|
var activeSessionId = process.env.PRIM_DAEMON_SESSION_ID ?? `daemon-${process.pid}`;
|
|
47
925
|
var lastHeartbeatAt;
|
|
48
926
|
var lastOnlineCount;
|
|
@@ -50,57 +928,165 @@ var lastOnlineNames;
|
|
|
50
928
|
var lastOnlineTeammates;
|
|
51
929
|
var lastOkAtLocal;
|
|
52
930
|
var heartbeatTimer;
|
|
931
|
+
var ingestionTimer;
|
|
53
932
|
var tokenCheckTimer;
|
|
54
|
-
|
|
933
|
+
var heartbeatInFlight;
|
|
934
|
+
var heartbeatRerunRequested = false;
|
|
935
|
+
var ownership;
|
|
936
|
+
var socketServer;
|
|
937
|
+
var shuttingDown = false;
|
|
938
|
+
function resolveRuntimeVersion() {
|
|
939
|
+
if (process.env.PRIM_RUNTIME_VERSION) {
|
|
940
|
+
return process.env.PRIM_RUNTIME_VERSION;
|
|
941
|
+
}
|
|
55
942
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
943
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
944
|
+
const pkg = JSON.parse(readFileSync4(join6(here, "..", "..", "package.json"), "utf-8"));
|
|
945
|
+
return pkg.version ?? "unknown";
|
|
58
946
|
} catch {
|
|
59
|
-
return
|
|
947
|
+
return "unknown";
|
|
60
948
|
}
|
|
61
949
|
}
|
|
62
|
-
function
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
950
|
+
function errorMessage(err) {
|
|
951
|
+
return err instanceof Error ? err.message : String(err);
|
|
952
|
+
}
|
|
953
|
+
function persistHealth() {
|
|
954
|
+
refreshDaemonHealth(daemonHealth, Date.now());
|
|
955
|
+
daemonHealth.updatedAt = Date.now();
|
|
956
|
+
try {
|
|
957
|
+
writeDaemonHealthState(daemonHealth);
|
|
958
|
+
} catch (err) {
|
|
959
|
+
process.stderr.write(`[prim-daemon] health-state error: ${errorMessage(err)}
|
|
960
|
+
`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function updatePendingHealth() {
|
|
964
|
+
const pending = pendingJournalStats();
|
|
965
|
+
daemonHealth.ingestion.pendingCount = pending.pendingCount;
|
|
966
|
+
daemonHealth.ingestion.pendingSampled = pending.sampled;
|
|
967
|
+
daemonHealth.ingestion.oldestPendingAt = pending.oldestPendingAt;
|
|
968
|
+
daemonHealth.ingestion.strandedCount = pending.strandedCount;
|
|
969
|
+
}
|
|
970
|
+
async function takeOwnership() {
|
|
971
|
+
const now = Date.now();
|
|
972
|
+
const recordedOwner = readDaemonOwner(CONFIG_DIR);
|
|
973
|
+
const recordedPid = readDaemonPid(CONFIG_DIR);
|
|
974
|
+
const candidatePids = new Set(
|
|
975
|
+
[recordedOwner?.pid, recordedPid?.pid].filter(
|
|
976
|
+
(pid) => pid !== void 0 && daemonProcessIsAlive(pid)
|
|
977
|
+
)
|
|
978
|
+
);
|
|
979
|
+
if (existsSync5(SOCK_PATH2) || candidatePids.size > 0) {
|
|
980
|
+
const snapshot = await daemonRequest(
|
|
981
|
+
"status_snapshot",
|
|
982
|
+
{},
|
|
983
|
+
{ timeoutMs: 500 }
|
|
984
|
+
);
|
|
985
|
+
if (typeof snapshot?.pid === "number") {
|
|
986
|
+
throw new Error(`daemon already running (pid=${String(snapshot.pid)})`);
|
|
67
987
|
}
|
|
68
988
|
}
|
|
69
|
-
|
|
70
|
-
|
|
989
|
+
const ownerIsStarting = recordedOwner !== void 0 && candidatePids.has(recordedOwner.pid) && now - recordedOwner.startedAt < DAEMON_STARTUP_GRACE_MS;
|
|
990
|
+
const legacyIsStarting = recordedPid !== void 0 && candidatePids.has(recordedPid.pid) && now - recordedPid.mtimeMs < DAEMON_STARTUP_GRACE_MS;
|
|
991
|
+
if (ownerIsStarting || legacyIsStarting) {
|
|
992
|
+
throw new Error("daemon startup already in progress");
|
|
71
993
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
994
|
+
if (recordedOwner !== void 0 && candidatePids.has(recordedOwner.pid)) {
|
|
995
|
+
throw new Error(
|
|
996
|
+
`daemon ownership held by live pid=${String(recordedOwner.pid)} (socket unavailable)`
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
if (recordedPid !== void 0 && candidatePids.has(recordedPid.pid)) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
`legacy daemon pid=${String(recordedPid.pid)} is still alive (socket unavailable)`
|
|
1002
|
+
);
|
|
78
1003
|
}
|
|
1004
|
+
ownership = acquireDaemonOwnership(CONFIG_DIR);
|
|
79
1005
|
try {
|
|
80
|
-
|
|
81
|
-
} catch {
|
|
1006
|
+
unlinkSync3(SOCK_PATH2);
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
if (err.code !== "ENOENT") {
|
|
1009
|
+
throw err;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function cleanup() {
|
|
1014
|
+
if (ownership) {
|
|
1015
|
+
try {
|
|
1016
|
+
releaseDaemonOwnership(ownership, SOCK_PATH2);
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
process.stderr.write(`[prim-daemon] cleanup error: ${errorMessage(err)}
|
|
1019
|
+
`);
|
|
1020
|
+
}
|
|
82
1021
|
}
|
|
83
1022
|
}
|
|
84
|
-
async function
|
|
1023
|
+
async function performHeartbeat() {
|
|
1024
|
+
daemonHealth.heartbeat.lastAttemptAt = Date.now();
|
|
1025
|
+
persistHealth();
|
|
85
1026
|
try {
|
|
86
|
-
const result = await client.post(
|
|
87
|
-
|
|
88
|
-
|
|
1027
|
+
const result = await client.post(
|
|
1028
|
+
"/api/cli/presence/heartbeat",
|
|
1029
|
+
{ sessionId: activeSessionId },
|
|
1030
|
+
{ signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) }
|
|
1031
|
+
);
|
|
89
1032
|
if (result.accepted) {
|
|
90
|
-
|
|
1033
|
+
const now = Date.now();
|
|
1034
|
+
lastOkAtLocal = now;
|
|
1035
|
+
daemonHealth.heartbeat.lastSuccessAt = now;
|
|
1036
|
+
daemonHealth.heartbeat.lastError = void 0;
|
|
1037
|
+
daemonHealth.heartbeat.consecutiveFailures = 0;
|
|
91
1038
|
if (typeof result.lastHeartbeatAt === "number") {
|
|
92
1039
|
lastHeartbeatAt = result.lastHeartbeatAt;
|
|
93
1040
|
}
|
|
94
1041
|
lastOnlineCount = typeof result.onlineCount === "number" ? result.onlineCount : void 0;
|
|
95
1042
|
lastOnlineNames = Array.isArray(result.onlineNames) ? result.onlineNames : void 0;
|
|
96
1043
|
lastOnlineTeammates = Array.isArray(result.onlineTeammates) ? result.onlineTeammates : void 0;
|
|
1044
|
+
} else {
|
|
1045
|
+
daemonHealth.heartbeat.lastRejectedAt = Date.now();
|
|
1046
|
+
daemonHealth.heartbeat.lastError = typeof result.unavailable === "string" ? result.unavailable : "heartbeat rejected";
|
|
1047
|
+
daemonHealth.heartbeat.consecutiveFailures += 1;
|
|
97
1048
|
}
|
|
1049
|
+
persistHealth();
|
|
98
1050
|
} catch (err) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
1051
|
+
daemonHealth.heartbeat.lastError = errorMessage(err);
|
|
1052
|
+
daemonHealth.heartbeat.consecutiveFailures += 1;
|
|
1053
|
+
persistHealth();
|
|
1054
|
+
process.stderr.write(`[prim-daemon] heartbeat error: ${errorMessage(err)}
|
|
1055
|
+
`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function sendHeartbeat() {
|
|
1059
|
+
if (heartbeatInFlight) {
|
|
1060
|
+
heartbeatRerunRequested = true;
|
|
1061
|
+
return heartbeatInFlight;
|
|
1062
|
+
}
|
|
1063
|
+
if (heartbeatTimer) {
|
|
1064
|
+
clearTimeout(heartbeatTimer);
|
|
1065
|
+
heartbeatTimer = void 0;
|
|
1066
|
+
}
|
|
1067
|
+
heartbeatInFlight = (async () => {
|
|
1068
|
+
do {
|
|
1069
|
+
heartbeatRerunRequested = false;
|
|
1070
|
+
await performHeartbeat();
|
|
1071
|
+
} while (heartbeatRerunRequested && !shuttingDown);
|
|
1072
|
+
})().finally(() => {
|
|
1073
|
+
heartbeatInFlight = void 0;
|
|
1074
|
+
scheduleHeartbeat();
|
|
1075
|
+
});
|
|
1076
|
+
return heartbeatInFlight;
|
|
1077
|
+
}
|
|
1078
|
+
function scheduleHeartbeat() {
|
|
1079
|
+
if (shuttingDown) {
|
|
1080
|
+
return;
|
|
103
1081
|
}
|
|
1082
|
+
const failures = daemonHealth.heartbeat.consecutiveFailures;
|
|
1083
|
+
const delay = failures > 0 ? heartbeatRetryDelayMs(failures) : HEARTBEAT_INTERVAL_MS;
|
|
1084
|
+
daemonHealth.heartbeat.nextRetryAt = failures > 0 ? Date.now() + delay : void 0;
|
|
1085
|
+
persistHealth();
|
|
1086
|
+
heartbeatTimer = setTimeout(() => {
|
|
1087
|
+
heartbeatTimer = void 0;
|
|
1088
|
+
void sendHeartbeat();
|
|
1089
|
+
}, delay);
|
|
104
1090
|
}
|
|
105
1091
|
async function ensureTokenFresh() {
|
|
106
1092
|
const expiresAt = getTokenExpiresAt();
|
|
@@ -109,11 +1095,70 @@ async function ensureTokenFresh() {
|
|
|
109
1095
|
}
|
|
110
1096
|
if (Date.now() >= expiresAt - TOKEN_REFRESH_THRESHOLD_MS) {
|
|
111
1097
|
try {
|
|
112
|
-
await refreshToken();
|
|
1098
|
+
await refreshToken({ signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
|
|
113
1099
|
} catch {
|
|
114
1100
|
}
|
|
115
1101
|
}
|
|
116
1102
|
}
|
|
1103
|
+
function scheduleIngestion(delayMs) {
|
|
1104
|
+
if (shuttingDown) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
ingestionTimer = setTimeout(() => {
|
|
1108
|
+
ingestionTimer = void 0;
|
|
1109
|
+
void runIngestionLoop();
|
|
1110
|
+
}, delayMs);
|
|
1111
|
+
}
|
|
1112
|
+
async function runIngestionLoop() {
|
|
1113
|
+
try {
|
|
1114
|
+
updatePendingHealth();
|
|
1115
|
+
} catch (err) {
|
|
1116
|
+
const failures = daemonHealth.ingestion.consecutiveFailures + 1;
|
|
1117
|
+
const delay = ingestionRetryDelayMs(failures);
|
|
1118
|
+
daemonHealth.ingestion.consecutiveFailures = failures;
|
|
1119
|
+
daemonHealth.ingestion.lastError = `journal scan failed: ${errorMessage(err)}`;
|
|
1120
|
+
daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
|
|
1121
|
+
persistHealth();
|
|
1122
|
+
scheduleIngestion(delay);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
if (daemonHealth.ingestion.pendingCount === 0 && !daemonHealth.ingestion.pendingSampled) {
|
|
1126
|
+
daemonHealth.ingestion.consecutiveFailures = 0;
|
|
1127
|
+
daemonHealth.ingestion.lastError = void 0;
|
|
1128
|
+
daemonHealth.ingestion.nextRetryAt = void 0;
|
|
1129
|
+
persistHealth();
|
|
1130
|
+
scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
daemonHealth.ingestion.lastAttemptAt = Date.now();
|
|
1134
|
+
daemonHealth.ingestion.nextRetryAt = void 0;
|
|
1135
|
+
persistHealth();
|
|
1136
|
+
try {
|
|
1137
|
+
const result = await flush();
|
|
1138
|
+
daemonHealth.ingestion.lastSuccessAt = Date.now();
|
|
1139
|
+
daemonHealth.ingestion.lastAcknowledgedCount = result.flushed;
|
|
1140
|
+
daemonHealth.ingestion.consecutiveFailures = 0;
|
|
1141
|
+
daemonHealth.ingestion.lastError = void 0;
|
|
1142
|
+
updatePendingHealth();
|
|
1143
|
+
persistHealth();
|
|
1144
|
+
scheduleIngestion(INGESTION_POLL_INTERVAL_MS);
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
const failures = daemonHealth.ingestion.consecutiveFailures + 1;
|
|
1147
|
+
const delay = ingestionRetryDelayMs(failures);
|
|
1148
|
+
daemonHealth.ingestion.lastAcknowledgedCount = err instanceof FlushError ? err.flushed : 0;
|
|
1149
|
+
daemonHealth.ingestion.consecutiveFailures = failures;
|
|
1150
|
+
daemonHealth.ingestion.lastError = errorMessage(err);
|
|
1151
|
+
daemonHealth.ingestion.nextRetryAt = Date.now() + delay;
|
|
1152
|
+
try {
|
|
1153
|
+
updatePendingHealth();
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
persistHealth();
|
|
1157
|
+
process.stderr.write(`[prim-daemon] ingestion error: ${errorMessage(err)}
|
|
1158
|
+
`);
|
|
1159
|
+
scheduleIngestion(delay);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
117
1162
|
async function handleConflictCheck(params) {
|
|
118
1163
|
assertCallerEnvMatches(params.callerEnv, getSiteUrl());
|
|
119
1164
|
if (typeof params.file !== "string") {
|
|
@@ -139,11 +1184,22 @@ async function proxyGet(params, allowedPrefix) {
|
|
|
139
1184
|
return await client.get(path, { signal: AbortSignal.timeout(HTTP_PROXY_TIMEOUT_MS) });
|
|
140
1185
|
}
|
|
141
1186
|
function handleStatusSnapshot(params) {
|
|
1187
|
+
const wasHealthy = daemonHealth.healthy;
|
|
1188
|
+
const heartbeatWasHealthy = daemonHealth.heartbeat.healthy;
|
|
1189
|
+
const ingestionWasHealthy = daemonHealth.ingestion.healthy;
|
|
1190
|
+
refreshDaemonHealth(daemonHealth, Date.now());
|
|
1191
|
+
if (wasHealthy !== daemonHealth.healthy || heartbeatWasHealthy !== daemonHealth.heartbeat.healthy || ingestionWasHealthy !== daemonHealth.ingestion.healthy) {
|
|
1192
|
+
persistHealth();
|
|
1193
|
+
}
|
|
142
1194
|
const base = {
|
|
143
1195
|
pid: process.pid,
|
|
144
1196
|
uptimeMs: Date.now() - startedAt,
|
|
145
1197
|
sessionId: activeSessionId,
|
|
146
|
-
lastHeartbeatAt
|
|
1198
|
+
lastHeartbeatAt,
|
|
1199
|
+
version: runtimeVersion,
|
|
1200
|
+
healthy: daemonHealth.healthy,
|
|
1201
|
+
heartbeat: { ...daemonHealth.heartbeat },
|
|
1202
|
+
ingestion: { ...daemonHealth.ingestion }
|
|
147
1203
|
};
|
|
148
1204
|
if (isCrossEnv(params.callerEnv, getSiteUrl())) {
|
|
149
1205
|
return {
|
|
@@ -155,7 +1211,7 @@ function handleStatusSnapshot(params) {
|
|
|
155
1211
|
envMismatch: true
|
|
156
1212
|
};
|
|
157
1213
|
}
|
|
158
|
-
const presenceFresh = lastOkAtLocal !== void 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
|
|
1214
|
+
const presenceFresh = lastOkAtLocal !== void 0 && daemonHealth.heartbeat.consecutiveFailures === 0 && Date.now() - lastOkAtLocal < PRESENCE_FRESH_WINDOW_MS;
|
|
159
1215
|
const presenceStale = lastOkAtLocal !== void 0 && !presenceFresh;
|
|
160
1216
|
return {
|
|
161
1217
|
...base,
|
|
@@ -245,68 +1301,85 @@ function handleConnection(conn) {
|
|
|
245
1301
|
conn.on("error", () => {
|
|
246
1302
|
});
|
|
247
1303
|
}
|
|
248
|
-
function
|
|
1304
|
+
function shutdown(exitCode, reason) {
|
|
1305
|
+
if (shuttingDown) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
shuttingDown = true;
|
|
1309
|
+
process.stderr.write(`${reason}
|
|
1310
|
+
`);
|
|
1311
|
+
stopTimers();
|
|
249
1312
|
try {
|
|
250
|
-
|
|
1313
|
+
socketServer?.close();
|
|
251
1314
|
} catch {
|
|
252
1315
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
1316
|
+
cleanup();
|
|
1317
|
+
process.exit(exitCode);
|
|
1318
|
+
}
|
|
1319
|
+
function startSocketServer() {
|
|
1320
|
+
socketServer = createServer(handleConnection);
|
|
1321
|
+
socketServer.on("error", (err) => {
|
|
1322
|
+
shutdown(EXIT_CRASH, `[prim-daemon] fatal socket error: ${err.message}`);
|
|
257
1323
|
});
|
|
258
|
-
|
|
259
|
-
process.stderr.write(`[prim-daemon] listening on ${
|
|
1324
|
+
socketServer.listen(SOCK_PATH2, () => {
|
|
1325
|
+
process.stderr.write(`[prim-daemon] listening on ${SOCK_PATH2}
|
|
260
1326
|
`);
|
|
1327
|
+
startTimers();
|
|
1328
|
+
process.stderr.write(
|
|
1329
|
+
`[prim-daemon] started (pid=${process.pid}, session=${activeSessionId}, version=${runtimeVersion})
|
|
1330
|
+
`
|
|
1331
|
+
);
|
|
261
1332
|
});
|
|
262
1333
|
}
|
|
263
1334
|
function startTimers() {
|
|
1335
|
+
persistHealth();
|
|
264
1336
|
void sendHeartbeat();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
1337
|
+
void runIngestionLoop();
|
|
1338
|
+
void runTokenCheckLoop();
|
|
1339
|
+
}
|
|
1340
|
+
async function runTokenCheckLoop() {
|
|
1341
|
+
await ensureTokenFresh();
|
|
1342
|
+
if (!shuttingDown) {
|
|
1343
|
+
tokenCheckTimer = setTimeout(() => {
|
|
1344
|
+
tokenCheckTimer = void 0;
|
|
1345
|
+
void runTokenCheckLoop();
|
|
1346
|
+
}, TOKEN_CHECK_INTERVAL_MS);
|
|
1347
|
+
}
|
|
271
1348
|
}
|
|
272
1349
|
function stopTimers() {
|
|
273
1350
|
if (heartbeatTimer) {
|
|
274
|
-
|
|
1351
|
+
clearTimeout(heartbeatTimer);
|
|
1352
|
+
}
|
|
1353
|
+
if (ingestionTimer) {
|
|
1354
|
+
clearTimeout(ingestionTimer);
|
|
275
1355
|
}
|
|
276
1356
|
if (tokenCheckTimer) {
|
|
277
|
-
|
|
1357
|
+
clearTimeout(tokenCheckTimer);
|
|
278
1358
|
}
|
|
279
1359
|
}
|
|
280
1360
|
function installSignalHandlers() {
|
|
281
1361
|
for (const signal of ["SIGTERM", "SIGINT"]) {
|
|
282
1362
|
process.on(signal, () => {
|
|
283
|
-
|
|
284
|
-
`);
|
|
285
|
-
stopTimers();
|
|
286
|
-
cleanup();
|
|
287
|
-
process.exit(EXIT_OK);
|
|
1363
|
+
shutdown(EXIT_OK, `[prim-daemon] ${signal}, shutting down (pid=${process.pid})`);
|
|
288
1364
|
});
|
|
289
1365
|
}
|
|
290
1366
|
process.on("uncaughtException", (err) => {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
process.exit(EXIT_CRASH);
|
|
1367
|
+
shutdown(EXIT_CRASH, `[prim-daemon] uncaught: ${err.message}`);
|
|
1368
|
+
});
|
|
1369
|
+
process.on("unhandledRejection", (err) => {
|
|
1370
|
+
shutdown(EXIT_CRASH, `[prim-daemon] unhandled rejection: ${errorMessage(err)}`);
|
|
296
1371
|
});
|
|
297
1372
|
}
|
|
298
|
-
function main() {
|
|
1373
|
+
async function main() {
|
|
299
1374
|
try {
|
|
300
|
-
|
|
1375
|
+
await takeOwnership();
|
|
301
1376
|
} catch (err) {
|
|
302
|
-
process.stderr.write(`[prim-daemon] ${
|
|
1377
|
+
process.stderr.write(`[prim-daemon] ${errorMessage(err)}
|
|
303
1378
|
`);
|
|
1379
|
+
cleanup();
|
|
304
1380
|
process.exit(EXIT_CRASH);
|
|
305
1381
|
}
|
|
306
1382
|
installSignalHandlers();
|
|
307
1383
|
startSocketServer();
|
|
308
|
-
startTimers();
|
|
309
|
-
process.stderr.write(`[prim-daemon] started (pid=${process.pid}, session=${activeSessionId})
|
|
310
|
-
`);
|
|
311
1384
|
}
|
|
312
|
-
main();
|
|
1385
|
+
void main();
|