@fml-inc/fml 0.8.6 → 0.8.7-rc.1
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/.claude-plugin/plugin.json +1 -1
- package/README.md +16 -5
- package/dist/{chunk-5TOOBX7I.js → chunk-4YLSKFDU.js} +1140 -111
- package/dist/{chunk-V2XC7JTB.js → chunk-5JJQWV4G.js} +381 -95
- package/dist/{chunk-YFHZJPTC.js → chunk-6BTC3S7C.js} +4 -2
- package/dist/chunk-F57IFY3K.js +21 -0
- package/dist/{chunk-2DQQXBWM.js → chunk-LPSZCTTJ.js} +37 -1
- package/dist/chunk-ZC2ZXSKC.js +277 -0
- package/dist/cli.js +168 -126
- package/dist/hook-handler.js +214 -22
- package/dist/observer-server.js +108 -22
- package/dist/sync-cycle.js +3 -2
- package/package.json +3 -3
- package/dist/chunk-E2MATWOU.js +0 -1225
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
FML_HOOK_INBOX_DIR
|
|
2
|
+
FML_HOOK_INBOX_DIR,
|
|
3
|
+
FML_OBSERVER_LOCK_FILE,
|
|
4
|
+
FML_OBSERVER_PORT,
|
|
5
|
+
FML_OBSERVER_STATE_FILE
|
|
3
6
|
} from "./chunk-B3H2YWPJ.js";
|
|
4
7
|
|
|
5
8
|
// ../../packages/panopticon/src/observer/auth.ts
|
|
@@ -2922,6 +2925,7 @@ function openObserverDatabase(databaseFile, options = {}) {
|
|
|
2922
2925
|
// ../../packages/panopticon/src/observer/protocol.ts
|
|
2923
2926
|
var OBSERVER_PROTOCOL_VERSION = 1;
|
|
2924
2927
|
var OBSERVER_SERVICE_NAME = "fml-observer";
|
|
2928
|
+
var OBSERVER_IDENTITY_HEADER = "x-fml-observer";
|
|
2925
2929
|
function isObserverHealth(value) {
|
|
2926
2930
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
2927
2931
|
const record = value;
|
|
@@ -2959,7 +2963,12 @@ function isAllowedObserverHost(host) {
|
|
|
2959
2963
|
}
|
|
2960
2964
|
function writeJson(res, status, body) {
|
|
2961
2965
|
const payload = JSON.stringify(body);
|
|
2962
|
-
res.writeHead(status, {
|
|
2966
|
+
res.writeHead(status, {
|
|
2967
|
+
"Content-Type": "application/json",
|
|
2968
|
+
// Identify every reply, including rejections, so a hook never mistakes
|
|
2969
|
+
// another localhost service on a recorded port for the observer.
|
|
2970
|
+
[OBSERVER_IDENTITY_HEADER]: OBSERVER_SERVICE_NAME
|
|
2971
|
+
});
|
|
2963
2972
|
res.end(payload);
|
|
2964
2973
|
}
|
|
2965
2974
|
function readHookRequestBody(req) {
|
|
@@ -3296,10 +3305,941 @@ function createObserverHttpServer(opts) {
|
|
|
3296
3305
|
});
|
|
3297
3306
|
}
|
|
3298
3307
|
|
|
3299
|
-
// src/
|
|
3308
|
+
// src/observer/state.ts
|
|
3300
3309
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3310
|
+
import { execFileSync } from "child_process";
|
|
3301
3311
|
import fs3 from "fs";
|
|
3302
3312
|
import path4 from "path";
|
|
3313
|
+
var OBSERVER_STATE_SCHEMA_VERSION = 1;
|
|
3314
|
+
var OBSERVER_HEARTBEAT_STALE_MS = 2 * 60 * 1e3;
|
|
3315
|
+
var OBSERVER_RECLAIM_GUARD_STALE_MS = 3e4;
|
|
3316
|
+
var OBSERVER_RECLAIM_GUARD_UNVERIFIED_TTL_MS = 5 * 6e4;
|
|
3317
|
+
var MAX_RECLAIM_GUARD_RECOVERIES = 2;
|
|
3318
|
+
var MAX_RECLAIM_GUARD_RACE_RETRIES = 16;
|
|
3319
|
+
var MAX_INITIAL_STATE_PUBLICATION_RETRIES = 2;
|
|
3320
|
+
var EXCLUSIVE_LINK_UNSUPPORTED_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3321
|
+
"ENOSYS",
|
|
3322
|
+
"ENOTSUP",
|
|
3323
|
+
"EOPNOTSUPP",
|
|
3324
|
+
"EPERM",
|
|
3325
|
+
"EXDEV"
|
|
3326
|
+
]);
|
|
3327
|
+
var TRANSIENT_OBSERVER_FILE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3328
|
+
"EACCES",
|
|
3329
|
+
"EAGAIN",
|
|
3330
|
+
"EBUSY",
|
|
3331
|
+
"EPERM"
|
|
3332
|
+
]);
|
|
3333
|
+
var reclaimProcessNonce = randomUUID2();
|
|
3334
|
+
var activeReclaimGuardIds = /* @__PURE__ */ new Set();
|
|
3335
|
+
function isRecord2(value) {
|
|
3336
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3337
|
+
}
|
|
3338
|
+
function isObserverPhase(value) {
|
|
3339
|
+
return value === "starting" || value === "syncing" || value === "idle" || value === "waiting_for_auth" || value === "backing_off" || value === "stopping";
|
|
3340
|
+
}
|
|
3341
|
+
function optionalNumber(record, key) {
|
|
3342
|
+
const value = record[key];
|
|
3343
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
3344
|
+
}
|
|
3345
|
+
function optionalString2(record, key) {
|
|
3346
|
+
const value = record[key];
|
|
3347
|
+
return typeof value === "string" ? value : void 0;
|
|
3348
|
+
}
|
|
3349
|
+
function parseObserverState(value) {
|
|
3350
|
+
if (!isRecord2(value)) return null;
|
|
3351
|
+
if (value.schemaVersion !== OBSERVER_STATE_SCHEMA_VERSION || value.service !== OBSERVER_SERVICE_NAME || typeof value.pid !== "number" || !Number.isInteger(value.pid) || value.pid <= 0 || typeof value.port !== "number" || !Number.isInteger(value.port) || value.port <= 0 || value.port > 65535 || typeof value.instanceId !== "string" || value.instanceId.length === 0 || typeof value.startedAtMs !== "number" || typeof value.heartbeatAtMs !== "number" || !isObserverPhase(value.phase)) {
|
|
3352
|
+
return null;
|
|
3353
|
+
}
|
|
3354
|
+
return {
|
|
3355
|
+
schemaVersion: OBSERVER_STATE_SCHEMA_VERSION,
|
|
3356
|
+
service: OBSERVER_SERVICE_NAME,
|
|
3357
|
+
pid: value.pid,
|
|
3358
|
+
port: value.port,
|
|
3359
|
+
instanceId: value.instanceId,
|
|
3360
|
+
startedAtMs: value.startedAtMs,
|
|
3361
|
+
heartbeatAtMs: value.heartbeatAtMs,
|
|
3362
|
+
phase: value.phase,
|
|
3363
|
+
processIdentity: optionalString2(value, "processIdentity"),
|
|
3364
|
+
environment: optionalString2(value, "environment"),
|
|
3365
|
+
lastAttemptAtMs: optionalNumber(value, "lastAttemptAtMs"),
|
|
3366
|
+
lastSuccessAtMs: optionalNumber(value, "lastSuccessAtMs"),
|
|
3367
|
+
nextAttemptAtMs: optionalNumber(value, "nextAttemptAtMs"),
|
|
3368
|
+
lastOutcome: optionalString2(value, "lastOutcome"),
|
|
3369
|
+
lastError: optionalString2(value, "lastError")
|
|
3370
|
+
};
|
|
3371
|
+
}
|
|
3372
|
+
function isPidAlive(pid) {
|
|
3373
|
+
try {
|
|
3374
|
+
process.kill(pid, 0);
|
|
3375
|
+
return true;
|
|
3376
|
+
} catch (error) {
|
|
3377
|
+
return error.code === "EPERM";
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
function readObserverState(stateFile = FML_OBSERVER_STATE_FILE) {
|
|
3381
|
+
try {
|
|
3382
|
+
return parseObserverState(
|
|
3383
|
+
JSON.parse(fs3.readFileSync(stateFile, "utf8"))
|
|
3384
|
+
);
|
|
3385
|
+
} catch {
|
|
3386
|
+
return null;
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
function readObserverStateStatus(opts = {}) {
|
|
3390
|
+
const stateFile = opts.stateFile ?? FML_OBSERVER_STATE_FILE;
|
|
3391
|
+
const exists = fs3.existsSync(stateFile);
|
|
3392
|
+
const state = readObserverState(stateFile);
|
|
3393
|
+
if (!state) {
|
|
3394
|
+
return { exists, valid: false, running: false, responsive: false, state };
|
|
3395
|
+
}
|
|
3396
|
+
const running = (opts.pidAlive ?? isPidAlive)(state.pid);
|
|
3397
|
+
return {
|
|
3398
|
+
exists,
|
|
3399
|
+
valid: true,
|
|
3400
|
+
running,
|
|
3401
|
+
responsive: running && (opts.nowMs ?? Date.now()) - state.heartbeatAtMs <= OBSERVER_HEARTBEAT_STALE_MS,
|
|
3402
|
+
state
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
function createInitialObserverState(opts = {}) {
|
|
3406
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
3407
|
+
return {
|
|
3408
|
+
schemaVersion: OBSERVER_STATE_SCHEMA_VERSION,
|
|
3409
|
+
service: OBSERVER_SERVICE_NAME,
|
|
3410
|
+
pid: process.pid,
|
|
3411
|
+
port: opts.port ?? FML_OBSERVER_PORT,
|
|
3412
|
+
instanceId: randomUUID2(),
|
|
3413
|
+
startedAtMs: nowMs,
|
|
3414
|
+
heartbeatAtMs: nowMs,
|
|
3415
|
+
phase: "starting",
|
|
3416
|
+
processIdentity: getCurrentProcessIdentity() ?? void 0
|
|
3417
|
+
};
|
|
3418
|
+
}
|
|
3419
|
+
function serializeState(state) {
|
|
3420
|
+
return `${JSON.stringify(state, null, 2)}
|
|
3421
|
+
`;
|
|
3422
|
+
}
|
|
3423
|
+
var ObserverStateMutationBusyError = class extends Error {
|
|
3424
|
+
constructor() {
|
|
3425
|
+
super("observer lease mutation is already in progress");
|
|
3426
|
+
this.name = "ObserverStateMutationBusyError";
|
|
3427
|
+
}
|
|
3428
|
+
};
|
|
3429
|
+
function parseObserverLeaseOwner(value) {
|
|
3430
|
+
try {
|
|
3431
|
+
const parsed = JSON.parse(value);
|
|
3432
|
+
return typeof parsed.pid === "number" && Number.isInteger(parsed.pid) && parsed.pid > 0 && typeof parsed.instanceId === "string" && parsed.instanceId.length > 0 ? { pid: parsed.pid, instanceId: parsed.instanceId } : null;
|
|
3433
|
+
} catch {
|
|
3434
|
+
return null;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
function readObserverLeaseOwner(lockFile = FML_OBSERVER_LOCK_FILE) {
|
|
3438
|
+
try {
|
|
3439
|
+
return parseObserverLeaseOwner(fs3.readFileSync(lockFile, "utf8"));
|
|
3440
|
+
} catch {
|
|
3441
|
+
return null;
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
function inspectObserverFile(file) {
|
|
3445
|
+
try {
|
|
3446
|
+
return {
|
|
3447
|
+
raw: fs3.readFileSync(file, "utf8"),
|
|
3448
|
+
exists: true,
|
|
3449
|
+
busy: false
|
|
3450
|
+
};
|
|
3451
|
+
} catch (error) {
|
|
3452
|
+
if (error.code === "ENOENT") {
|
|
3453
|
+
return { raw: null, exists: false, busy: false };
|
|
3454
|
+
}
|
|
3455
|
+
if (isTransientObserverFileError(error)) {
|
|
3456
|
+
return { raw: null, exists: true, busy: true };
|
|
3457
|
+
}
|
|
3458
|
+
throw error;
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
function inspectObserverLease(opts = {}) {
|
|
3462
|
+
const stateResult = inspectObserverFile(
|
|
3463
|
+
opts.stateFile ?? FML_OBSERVER_STATE_FILE
|
|
3464
|
+
);
|
|
3465
|
+
const lockResult = inspectObserverFile(
|
|
3466
|
+
opts.lockFile ?? FML_OBSERVER_LOCK_FILE
|
|
3467
|
+
);
|
|
3468
|
+
let state = null;
|
|
3469
|
+
if (stateResult.raw !== null) {
|
|
3470
|
+
try {
|
|
3471
|
+
state = parseObserverState(JSON.parse(stateResult.raw));
|
|
3472
|
+
} catch {
|
|
3473
|
+
state = null;
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
return {
|
|
3477
|
+
state,
|
|
3478
|
+
owner: lockResult.raw === null ? null : parseObserverLeaseOwner(lockResult.raw),
|
|
3479
|
+
stateExists: stateResult.exists,
|
|
3480
|
+
lockExists: lockResult.exists,
|
|
3481
|
+
stateBusy: stateResult.busy,
|
|
3482
|
+
lockBusy: lockResult.busy,
|
|
3483
|
+
busy: stateResult.busy || lockResult.busy
|
|
3484
|
+
};
|
|
3485
|
+
}
|
|
3486
|
+
function observerLeaseIsOwned(instanceId, lockFile = FML_OBSERVER_LOCK_FILE) {
|
|
3487
|
+
return readObserverLeaseOwner(lockFile)?.instanceId === instanceId;
|
|
3488
|
+
}
|
|
3489
|
+
function getProcessIdentity(pid) {
|
|
3490
|
+
try {
|
|
3491
|
+
if (process.platform === "linux") {
|
|
3492
|
+
const stat = fs3.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
3493
|
+
const fieldsAfterCommand = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
3494
|
+
const startTimeTicks = fieldsAfterCommand[19];
|
|
3495
|
+
return startTimeTicks ? `linux:${startTimeTicks}` : null;
|
|
3496
|
+
}
|
|
3497
|
+
if (process.platform === "darwin") {
|
|
3498
|
+
const startedAt = execFileSync(
|
|
3499
|
+
"ps",
|
|
3500
|
+
["-p", String(pid), "-o", "lstart="],
|
|
3501
|
+
{
|
|
3502
|
+
encoding: "utf8",
|
|
3503
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3504
|
+
timeout: 1e3
|
|
3505
|
+
}
|
|
3506
|
+
).trim();
|
|
3507
|
+
return startedAt ? `darwin:${startedAt}` : null;
|
|
3508
|
+
}
|
|
3509
|
+
if (process.platform === "win32") {
|
|
3510
|
+
const startedAt = execFileSync(
|
|
3511
|
+
"powershell.exe",
|
|
3512
|
+
[
|
|
3513
|
+
"-NoProfile",
|
|
3514
|
+
"-NonInteractive",
|
|
3515
|
+
"-Command",
|
|
3516
|
+
`(Get-Process -Id ${pid}).StartTime.ToUniversalTime().Ticks`
|
|
3517
|
+
],
|
|
3518
|
+
{
|
|
3519
|
+
encoding: "utf8",
|
|
3520
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3521
|
+
timeout: 1e3
|
|
3522
|
+
}
|
|
3523
|
+
).trim();
|
|
3524
|
+
return startedAt ? `win32:${startedAt}` : null;
|
|
3525
|
+
}
|
|
3526
|
+
} catch {
|
|
3527
|
+
}
|
|
3528
|
+
return null;
|
|
3529
|
+
}
|
|
3530
|
+
var currentProcessIdentityCache;
|
|
3531
|
+
function getCurrentProcessIdentity() {
|
|
3532
|
+
if (currentProcessIdentityCache === void 0) {
|
|
3533
|
+
currentProcessIdentityCache = getProcessIdentity(process.pid);
|
|
3534
|
+
}
|
|
3535
|
+
return currentProcessIdentityCache;
|
|
3536
|
+
}
|
|
3537
|
+
function parseObserverReclaimGuardOwner(value) {
|
|
3538
|
+
try {
|
|
3539
|
+
const parsed = JSON.parse(value);
|
|
3540
|
+
if (typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.instanceId !== "string" || parsed.instanceId.length === 0 || typeof parsed.guardId !== "string" || parsed.guardId.length === 0 || typeof parsed.createdAtMs !== "number" || parsed.processIdentity !== void 0 && typeof parsed.processIdentity !== "string" || parsed.processNonce !== void 0 && typeof parsed.processNonce !== "string") {
|
|
3541
|
+
return null;
|
|
3542
|
+
}
|
|
3543
|
+
return parsed;
|
|
3544
|
+
} catch {
|
|
3545
|
+
return null;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3548
|
+
function isTransientObserverFileError(error) {
|
|
3549
|
+
const code = error?.code;
|
|
3550
|
+
return code !== void 0 && TRANSIENT_OBSERVER_FILE_ERROR_CODES.has(code);
|
|
3551
|
+
}
|
|
3552
|
+
function readObserverMutationFile(file) {
|
|
3553
|
+
try {
|
|
3554
|
+
return fs3.readFileSync(file, "utf8");
|
|
3555
|
+
} catch (error) {
|
|
3556
|
+
if (error.code === "ENOENT") return null;
|
|
3557
|
+
if (isTransientObserverFileError(error)) {
|
|
3558
|
+
throw new ObserverStateMutationBusyError();
|
|
3559
|
+
}
|
|
3560
|
+
throw error;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
function readObserverStateForMutation(stateFile) {
|
|
3564
|
+
const raw = readObserverMutationFile(stateFile);
|
|
3565
|
+
if (raw === null) return null;
|
|
3566
|
+
try {
|
|
3567
|
+
return parseObserverState(JSON.parse(raw));
|
|
3568
|
+
} catch {
|
|
3569
|
+
return null;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
function assertObserverStateMutationOwnership(instanceId, stateFile, lockFile, errorMessage) {
|
|
3573
|
+
const leaseOwner = readObserverMutationFile(lockFile);
|
|
3574
|
+
if (leaseOwner === null || parseObserverLeaseOwner(leaseOwner)?.instanceId !== instanceId) {
|
|
3575
|
+
throw new Error(errorMessage);
|
|
3576
|
+
}
|
|
3577
|
+
if (readObserverStateForMutation(stateFile)?.instanceId !== instanceId) {
|
|
3578
|
+
throw new Error(errorMessage);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
function isRetryableReclaimGuardRace(error) {
|
|
3582
|
+
return error.code === "ENOENT" || isTransientObserverFileError(error);
|
|
3583
|
+
}
|
|
3584
|
+
function removeReclaimTombstone(tombstone) {
|
|
3585
|
+
try {
|
|
3586
|
+
fs3.rmSync(tombstone, { force: true });
|
|
3587
|
+
} catch (error) {
|
|
3588
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
3589
|
+
}
|
|
3590
|
+
}
|
|
3591
|
+
function restoreDisplacedReclaimGuard(tombstone, reclaimFile, fallbackRaw) {
|
|
3592
|
+
let canonicalReserved = false;
|
|
3593
|
+
let tombstoneRestored = false;
|
|
3594
|
+
let fallbackPublished = false;
|
|
3595
|
+
try {
|
|
3596
|
+
fs3.linkSync(tombstone, reclaimFile);
|
|
3597
|
+
canonicalReserved = true;
|
|
3598
|
+
tombstoneRestored = true;
|
|
3599
|
+
} catch (error) {
|
|
3600
|
+
if (error.code === "EEXIST") {
|
|
3601
|
+
canonicalReserved = true;
|
|
3602
|
+
} else if (!isRetryableReclaimGuardRace(error)) {
|
|
3603
|
+
throw error;
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3606
|
+
if (!canonicalReserved) {
|
|
3607
|
+
try {
|
|
3608
|
+
fs3.copyFileSync(tombstone, reclaimFile, fs3.constants.COPYFILE_EXCL);
|
|
3609
|
+
canonicalReserved = true;
|
|
3610
|
+
tombstoneRestored = true;
|
|
3611
|
+
} catch (error) {
|
|
3612
|
+
if (error.code === "EEXIST") {
|
|
3613
|
+
canonicalReserved = true;
|
|
3614
|
+
} else if (!isRetryableReclaimGuardRace(error)) {
|
|
3615
|
+
throw error;
|
|
3616
|
+
}
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
if (!canonicalReserved) {
|
|
3620
|
+
let fd;
|
|
3621
|
+
try {
|
|
3622
|
+
fd = fs3.openSync(reclaimFile, "wx", 384);
|
|
3623
|
+
canonicalReserved = true;
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
if (error.code === "EEXIST") {
|
|
3626
|
+
canonicalReserved = true;
|
|
3627
|
+
} else if (isTransientObserverFileError(error)) {
|
|
3628
|
+
return false;
|
|
3629
|
+
} else {
|
|
3630
|
+
throw error;
|
|
3631
|
+
}
|
|
3632
|
+
fd = -1;
|
|
3633
|
+
}
|
|
3634
|
+
if (fd >= 0) {
|
|
3635
|
+
let publicationError;
|
|
3636
|
+
try {
|
|
3637
|
+
fs3.writeFileSync(fd, fallbackRaw ?? "");
|
|
3638
|
+
} catch (error) {
|
|
3639
|
+
publicationError = error;
|
|
3640
|
+
}
|
|
3641
|
+
try {
|
|
3642
|
+
fs3.closeSync(fd);
|
|
3643
|
+
} catch (error) {
|
|
3644
|
+
if (publicationError === void 0 || !isTransientObserverFileError(error)) {
|
|
3645
|
+
publicationError = error;
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
if (publicationError !== void 0 && !isTransientObserverFileError(publicationError)) {
|
|
3649
|
+
throw publicationError;
|
|
3650
|
+
}
|
|
3651
|
+
fallbackPublished = publicationError === void 0 && fallbackRaw !== void 0;
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
if (canonicalReserved && (fallbackPublished || tombstoneRestored)) {
|
|
3655
|
+
removeReclaimTombstone(tombstone);
|
|
3656
|
+
}
|
|
3657
|
+
return canonicalReserved;
|
|
3658
|
+
}
|
|
3659
|
+
function sameFileIdentity(left, right) {
|
|
3660
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
3661
|
+
}
|
|
3662
|
+
function removeCreatedFileByIdentity(file, created) {
|
|
3663
|
+
let observed;
|
|
3664
|
+
try {
|
|
3665
|
+
observed = fs3.statSync(file);
|
|
3666
|
+
} catch (error) {
|
|
3667
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
3668
|
+
return error.code === "ENOENT";
|
|
3669
|
+
}
|
|
3670
|
+
if (!sameFileIdentity(observed, created)) return true;
|
|
3671
|
+
const tombstone = `${file}.failed-${process.pid}-${randomUUID2()}`;
|
|
3672
|
+
try {
|
|
3673
|
+
fs3.renameSync(file, tombstone);
|
|
3674
|
+
} catch (error) {
|
|
3675
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
3676
|
+
return error.code === "ENOENT";
|
|
3677
|
+
}
|
|
3678
|
+
let displaced;
|
|
3679
|
+
try {
|
|
3680
|
+
displaced = fs3.statSync(tombstone);
|
|
3681
|
+
} catch (error) {
|
|
3682
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
3683
|
+
return restoreDisplacedReclaimGuard(tombstone, file);
|
|
3684
|
+
}
|
|
3685
|
+
let displacedRaw;
|
|
3686
|
+
try {
|
|
3687
|
+
displacedRaw = fs3.readFileSync(tombstone, "utf8");
|
|
3688
|
+
} catch (error) {
|
|
3689
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
3690
|
+
}
|
|
3691
|
+
if (sameFileIdentity(displaced, created)) {
|
|
3692
|
+
removeReclaimTombstone(tombstone);
|
|
3693
|
+
return true;
|
|
3694
|
+
}
|
|
3695
|
+
return restoreDisplacedReclaimGuard(tombstone, file, displacedRaw);
|
|
3696
|
+
}
|
|
3697
|
+
function rewriteExclusiveFileBody(fd, contents) {
|
|
3698
|
+
const body = Buffer.from(contents, "utf8");
|
|
3699
|
+
fs3.ftruncateSync(fd, 0);
|
|
3700
|
+
let offset = 0;
|
|
3701
|
+
while (offset < body.length) {
|
|
3702
|
+
const written = fs3.writeSync(
|
|
3703
|
+
fd,
|
|
3704
|
+
body,
|
|
3705
|
+
offset,
|
|
3706
|
+
body.length - offset,
|
|
3707
|
+
offset
|
|
3708
|
+
);
|
|
3709
|
+
if (written === 0) {
|
|
3710
|
+
throw Object.assign(
|
|
3711
|
+
new Error("observer guard rewrite made no progress"),
|
|
3712
|
+
{
|
|
3713
|
+
code: "EBUSY"
|
|
3714
|
+
}
|
|
3715
|
+
);
|
|
3716
|
+
}
|
|
3717
|
+
offset += written;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
function publishExclusiveLease(lockFile, serializedOwner) {
|
|
3721
|
+
const stagingFile = `${lockFile}.${process.pid}.${randomUUID2()}.tmp`;
|
|
3722
|
+
let fd = null;
|
|
3723
|
+
let staged = null;
|
|
3724
|
+
let publicationError;
|
|
3725
|
+
try {
|
|
3726
|
+
fd = fs3.openSync(stagingFile, "wx", 384);
|
|
3727
|
+
fs3.writeFileSync(fd, serializedOwner);
|
|
3728
|
+
fs3.fsyncSync(fd);
|
|
3729
|
+
staged = fs3.fstatSync(fd);
|
|
3730
|
+
const stagedFd = fd;
|
|
3731
|
+
fd = null;
|
|
3732
|
+
fs3.closeSync(stagedFd);
|
|
3733
|
+
try {
|
|
3734
|
+
fs3.linkSync(stagingFile, lockFile);
|
|
3735
|
+
} catch (error) {
|
|
3736
|
+
if (error.code === "EEXIST") return null;
|
|
3737
|
+
let linkedDespiteError = false;
|
|
3738
|
+
try {
|
|
3739
|
+
const published2 = fs3.statSync(lockFile);
|
|
3740
|
+
linkedDespiteError = sameFileIdentity(published2, staged) && fs3.readFileSync(lockFile, "utf8") === serializedOwner;
|
|
3741
|
+
} catch {
|
|
3742
|
+
}
|
|
3743
|
+
if (!linkedDespiteError) {
|
|
3744
|
+
const code = error.code;
|
|
3745
|
+
if (code !== void 0 && EXCLUSIVE_LINK_UNSUPPORTED_ERROR_CODES.has(code)) {
|
|
3746
|
+
throw new Error(
|
|
3747
|
+
`The filesystem containing ${lockFile} does not support atomic FML observer leases; choose an FML state or observer directory on a filesystem with hard-link support`,
|
|
3748
|
+
{ cause: error }
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
throw error;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
const published = fs3.statSync(lockFile);
|
|
3755
|
+
if (fs3.readFileSync(lockFile, "utf8") !== serializedOwner) {
|
|
3756
|
+
try {
|
|
3757
|
+
removeCreatedFileByIdentity(lockFile, published);
|
|
3758
|
+
} catch {
|
|
3759
|
+
}
|
|
3760
|
+
throw new Error(`FML observer lease publication was incomplete`);
|
|
3761
|
+
}
|
|
3762
|
+
return published;
|
|
3763
|
+
} catch (error) {
|
|
3764
|
+
publicationError = error;
|
|
3765
|
+
throw error;
|
|
3766
|
+
} finally {
|
|
3767
|
+
if (fd !== null) {
|
|
3768
|
+
try {
|
|
3769
|
+
fs3.closeSync(fd);
|
|
3770
|
+
} catch (closeError) {
|
|
3771
|
+
if (publicationError === void 0) throw closeError;
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
try {
|
|
3775
|
+
fs3.rmSync(stagingFile, { force: true });
|
|
3776
|
+
} catch {
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
function ownsReclaimGuard(reclaimFile, guardId) {
|
|
3781
|
+
try {
|
|
3782
|
+
return parseObserverReclaimGuardOwner(fs3.readFileSync(reclaimFile, "utf8"))?.guardId === guardId;
|
|
3783
|
+
} catch {
|
|
3784
|
+
return false;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
function ownsReclaimGuardForMutation(reclaimFile, guardId) {
|
|
3788
|
+
const raw = readObserverMutationFile(reclaimFile);
|
|
3789
|
+
return raw !== null && parseObserverReclaimGuardOwner(raw)?.guardId === guardId;
|
|
3790
|
+
}
|
|
3791
|
+
function writeLeaseAndState(state, stateFile, lockFile, reclaimFile, reclaimGuardId) {
|
|
3792
|
+
const serializedOwner = `${JSON.stringify({
|
|
3793
|
+
pid: state.pid,
|
|
3794
|
+
instanceId: state.instanceId
|
|
3795
|
+
})}
|
|
3796
|
+
`;
|
|
3797
|
+
const created = publishExclusiveLease(lockFile, serializedOwner);
|
|
3798
|
+
if (!created) return false;
|
|
3799
|
+
if (!ownsReclaimGuard(reclaimFile, reclaimGuardId) || !observerLeaseIsOwned(state.instanceId, lockFile)) {
|
|
3800
|
+
removeCreatedFileByIdentity(lockFile, created);
|
|
3801
|
+
return false;
|
|
3802
|
+
}
|
|
3803
|
+
try {
|
|
3804
|
+
writeObserverState(state, stateFile);
|
|
3805
|
+
} catch (error) {
|
|
3806
|
+
try {
|
|
3807
|
+
removeCreatedFileByIdentity(lockFile, created);
|
|
3808
|
+
} catch {
|
|
3809
|
+
}
|
|
3810
|
+
throw error;
|
|
3811
|
+
}
|
|
3812
|
+
const coherent = ownsReclaimGuard(reclaimFile, reclaimGuardId) && observerLeaseIsOwned(state.instanceId, lockFile) && readObserverState(stateFile)?.instanceId === state.instanceId;
|
|
3813
|
+
if (!coherent) {
|
|
3814
|
+
try {
|
|
3815
|
+
removeCreatedFileByIdentity(lockFile, created);
|
|
3816
|
+
} catch {
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return coherent;
|
|
3820
|
+
}
|
|
3821
|
+
function leaseCanBeReclaimed(opts) {
|
|
3822
|
+
const owner = parseObserverLeaseOwner(opts.rawOwner);
|
|
3823
|
+
if (!owner) {
|
|
3824
|
+
if (opts.liveOwnerPolicy === "process_identity") {
|
|
3825
|
+
return false;
|
|
3826
|
+
}
|
|
3827
|
+
return opts.nowMs - opts.lockMtimeMs >= OBSERVER_RECLAIM_GUARD_STALE_MS;
|
|
3828
|
+
}
|
|
3829
|
+
if (!opts.pidAlive(owner.pid)) return true;
|
|
3830
|
+
const ownerState = readObserverState(opts.stateFile);
|
|
3831
|
+
if (opts.liveOwnerPolicy === "process_identity") {
|
|
3832
|
+
if (ownerState?.pid !== owner.pid || ownerState.instanceId !== owner.instanceId) {
|
|
3833
|
+
return false;
|
|
3834
|
+
}
|
|
3835
|
+
if (ownerState.processIdentity === void 0) {
|
|
3836
|
+
return false;
|
|
3837
|
+
}
|
|
3838
|
+
const currentIdentity = opts.processIdentity(owner.pid);
|
|
3839
|
+
return currentIdentity !== null && currentIdentity !== ownerState.processIdentity;
|
|
3840
|
+
}
|
|
3841
|
+
if (ownerState?.pid === owner.pid && ownerState.instanceId === owner.instanceId) {
|
|
3842
|
+
return opts.nowMs - ownerState.heartbeatAtMs > OBSERVER_HEARTBEAT_STALE_MS;
|
|
3843
|
+
}
|
|
3844
|
+
return opts.nowMs - opts.lockMtimeMs > OBSERVER_HEARTBEAT_STALE_MS;
|
|
3845
|
+
}
|
|
3846
|
+
function acquireReclaimGuard(owner, reclaimFile, nowMs, opts = {}) {
|
|
3847
|
+
const guardId = randomUUID2();
|
|
3848
|
+
const processIdentity = opts.processIdentity ?? ((pid) => pid === process.pid ? getCurrentProcessIdentity() : getProcessIdentity(pid));
|
|
3849
|
+
let recoveryAttempts = 0;
|
|
3850
|
+
let raceRetries = 0;
|
|
3851
|
+
while (recoveryAttempts < MAX_RECLAIM_GUARD_RECOVERIES && raceRetries <= MAX_RECLAIM_GUARD_RACE_RETRIES) {
|
|
3852
|
+
try {
|
|
3853
|
+
const fd = fs3.openSync(reclaimFile, "wx", 384);
|
|
3854
|
+
let created = null;
|
|
3855
|
+
const serializedGuard = `${JSON.stringify({
|
|
3856
|
+
pid: process.pid,
|
|
3857
|
+
instanceId: owner.instanceId,
|
|
3858
|
+
guardId,
|
|
3859
|
+
createdAtMs: nowMs,
|
|
3860
|
+
processIdentity: processIdentity(process.pid) ?? void 0,
|
|
3861
|
+
processNonce: reclaimProcessNonce
|
|
3862
|
+
})}
|
|
3863
|
+
`;
|
|
3864
|
+
let publicationError;
|
|
3865
|
+
let publicationFailed = false;
|
|
3866
|
+
let cleanupBlocked = false;
|
|
3867
|
+
try {
|
|
3868
|
+
created = fs3.fstatSync(fd);
|
|
3869
|
+
fs3.writeFileSync(fd, serializedGuard);
|
|
3870
|
+
} catch (error) {
|
|
3871
|
+
if (isTransientObserverFileError(error)) {
|
|
3872
|
+
try {
|
|
3873
|
+
rewriteExclusiveFileBody(fd, serializedGuard);
|
|
3874
|
+
} catch (retryError) {
|
|
3875
|
+
publicationFailed = true;
|
|
3876
|
+
publicationError = retryError;
|
|
3877
|
+
}
|
|
3878
|
+
} else {
|
|
3879
|
+
publicationFailed = true;
|
|
3880
|
+
publicationError = error;
|
|
3881
|
+
}
|
|
3882
|
+
if (publicationFailed && created) {
|
|
3883
|
+
try {
|
|
3884
|
+
cleanupBlocked = !removeCreatedFileByIdentity(reclaimFile, created);
|
|
3885
|
+
} catch (cleanupError) {
|
|
3886
|
+
publicationError = cleanupError;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3890
|
+
let displacedAfterPublication = false;
|
|
3891
|
+
if (!publicationFailed) {
|
|
3892
|
+
try {
|
|
3893
|
+
displacedAfterPublication = parseObserverReclaimGuardOwner(fs3.readFileSync(reclaimFile, "utf8"))?.guardId !== guardId;
|
|
3894
|
+
} catch (error) {
|
|
3895
|
+
if (error.code === "ENOENT") {
|
|
3896
|
+
displacedAfterPublication = true;
|
|
3897
|
+
} else {
|
|
3898
|
+
publicationFailed = true;
|
|
3899
|
+
publicationError = error;
|
|
3900
|
+
if (isTransientObserverFileError(error) && created) {
|
|
3901
|
+
try {
|
|
3902
|
+
cleanupBlocked = !removeCreatedFileByIdentity(
|
|
3903
|
+
reclaimFile,
|
|
3904
|
+
created
|
|
3905
|
+
);
|
|
3906
|
+
} catch (cleanupError) {
|
|
3907
|
+
publicationError = cleanupError;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
try {
|
|
3914
|
+
fs3.closeSync(fd);
|
|
3915
|
+
} catch (error) {
|
|
3916
|
+
if (!publicationFailed) publicationError = error;
|
|
3917
|
+
publicationFailed = true;
|
|
3918
|
+
}
|
|
3919
|
+
if (publicationFailed) {
|
|
3920
|
+
if (cleanupBlocked) return null;
|
|
3921
|
+
if (isTransientObserverFileError(publicationError)) {
|
|
3922
|
+
if (ownsReclaimGuard(reclaimFile, guardId)) {
|
|
3923
|
+
activeReclaimGuardIds.add(guardId);
|
|
3924
|
+
return guardId;
|
|
3925
|
+
}
|
|
3926
|
+
raceRetries += 1;
|
|
3927
|
+
continue;
|
|
3928
|
+
}
|
|
3929
|
+
throw publicationError;
|
|
3930
|
+
}
|
|
3931
|
+
if (displacedAfterPublication) return null;
|
|
3932
|
+
activeReclaimGuardIds.add(guardId);
|
|
3933
|
+
return guardId;
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
if (error.code !== "EEXIST") {
|
|
3936
|
+
if (isTransientObserverFileError(error)) return null;
|
|
3937
|
+
throw error;
|
|
3938
|
+
}
|
|
3939
|
+
let observedRaw;
|
|
3940
|
+
let observed;
|
|
3941
|
+
try {
|
|
3942
|
+
observedRaw = fs3.readFileSync(reclaimFile, "utf8");
|
|
3943
|
+
observed = fs3.statSync(reclaimFile);
|
|
3944
|
+
} catch (readError) {
|
|
3945
|
+
if (readError.code === "ENOENT") {
|
|
3946
|
+
raceRetries += 1;
|
|
3947
|
+
continue;
|
|
3948
|
+
}
|
|
3949
|
+
if (isTransientObserverFileError(readError)) return null;
|
|
3950
|
+
throw readError;
|
|
3951
|
+
}
|
|
3952
|
+
const observedOwner = parseObserverReclaimGuardOwner(observedRaw);
|
|
3953
|
+
if (observedOwner) {
|
|
3954
|
+
const alive = (opts.pidAlive ?? isPidAlive)(observedOwner.pid);
|
|
3955
|
+
const abandonedByThisProcess = observedOwner.processNonce === reclaimProcessNonce && !activeReclaimGuardIds.has(observedOwner.guardId);
|
|
3956
|
+
if (alive && !abandonedByThisProcess) {
|
|
3957
|
+
const currentIdentity = observedOwner.processIdentity === void 0 ? null : processIdentity(observedOwner.pid);
|
|
3958
|
+
if (currentIdentity !== null && currentIdentity === observedOwner.processIdentity) {
|
|
3959
|
+
return null;
|
|
3960
|
+
}
|
|
3961
|
+
if (currentIdentity === null && nowMs - observedOwner.createdAtMs < OBSERVER_RECLAIM_GUARD_UNVERIFIED_TTL_MS) {
|
|
3962
|
+
return null;
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
} else if (nowMs - observed.mtimeMs < OBSERVER_RECLAIM_GUARD_STALE_MS) {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
const tombstone = `${reclaimFile}.stale-${process.pid}-${randomUUID2()}`;
|
|
3969
|
+
try {
|
|
3970
|
+
fs3.renameSync(reclaimFile, tombstone);
|
|
3971
|
+
} catch (renameError) {
|
|
3972
|
+
if (renameError.code === "ENOENT") {
|
|
3973
|
+
raceRetries += 1;
|
|
3974
|
+
continue;
|
|
3975
|
+
}
|
|
3976
|
+
if (isTransientObserverFileError(renameError)) return null;
|
|
3977
|
+
throw renameError;
|
|
3978
|
+
}
|
|
3979
|
+
recoveryAttempts += 1;
|
|
3980
|
+
let displacedRaw;
|
|
3981
|
+
try {
|
|
3982
|
+
displacedRaw = fs3.readFileSync(tombstone, "utf8");
|
|
3983
|
+
} catch (error2) {
|
|
3984
|
+
if (!isRetryableReclaimGuardRace(error2)) throw error2;
|
|
3985
|
+
restoreDisplacedReclaimGuard(tombstone, reclaimFile, observedRaw);
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
let displaced;
|
|
3989
|
+
try {
|
|
3990
|
+
displaced = fs3.statSync(tombstone);
|
|
3991
|
+
} catch (error2) {
|
|
3992
|
+
if (!isRetryableReclaimGuardRace(error2)) throw error2;
|
|
3993
|
+
restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
|
|
3994
|
+
return null;
|
|
3995
|
+
}
|
|
3996
|
+
if (displacedRaw !== observedRaw || displaced.dev !== observed.dev || displaced.ino !== observed.ino || displaced.mtimeMs !== observed.mtimeMs || !observedOwner && nowMs - displaced.mtimeMs < OBSERVER_RECLAIM_GUARD_STALE_MS) {
|
|
3997
|
+
restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
|
|
3998
|
+
return null;
|
|
3999
|
+
}
|
|
4000
|
+
removeReclaimTombstone(tombstone);
|
|
4001
|
+
}
|
|
4002
|
+
}
|
|
4003
|
+
return null;
|
|
4004
|
+
}
|
|
4005
|
+
function releaseReclaimGuard(reclaimFile, guardId) {
|
|
4006
|
+
try {
|
|
4007
|
+
let observedRaw;
|
|
4008
|
+
let observed;
|
|
4009
|
+
try {
|
|
4010
|
+
observedRaw = fs3.readFileSync(reclaimFile, "utf8");
|
|
4011
|
+
observed = fs3.statSync(reclaimFile);
|
|
4012
|
+
} catch (error) {
|
|
4013
|
+
if (error.code === "ENOENT" || isTransientObserverFileError(error)) {
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
throw error;
|
|
4017
|
+
}
|
|
4018
|
+
if (parseObserverReclaimGuardOwner(observedRaw)?.guardId !== guardId) {
|
|
4019
|
+
return;
|
|
4020
|
+
}
|
|
4021
|
+
const tombstone = `${reclaimFile}.release-${process.pid}-${randomUUID2()}`;
|
|
4022
|
+
fs3.renameSync(reclaimFile, tombstone);
|
|
4023
|
+
let displacedRaw;
|
|
4024
|
+
try {
|
|
4025
|
+
displacedRaw = fs3.readFileSync(tombstone, "utf8");
|
|
4026
|
+
} catch (error) {
|
|
4027
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
4028
|
+
restoreDisplacedReclaimGuard(tombstone, reclaimFile, observedRaw);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
let displaced;
|
|
4032
|
+
try {
|
|
4033
|
+
displaced = fs3.statSync(tombstone);
|
|
4034
|
+
} catch (error) {
|
|
4035
|
+
if (!isRetryableReclaimGuardRace(error)) throw error;
|
|
4036
|
+
restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
4039
|
+
if (displacedRaw !== observedRaw || displaced.dev !== observed.dev || displaced.ino !== observed.ino || displaced.mtimeMs !== observed.mtimeMs) {
|
|
4040
|
+
restoreDisplacedReclaimGuard(tombstone, reclaimFile, displacedRaw);
|
|
4041
|
+
return;
|
|
4042
|
+
}
|
|
4043
|
+
removeReclaimTombstone(tombstone);
|
|
4044
|
+
} catch (error) {
|
|
4045
|
+
if (error.code !== "ENOENT" && !isTransientObserverFileError(error)) {
|
|
4046
|
+
throw error;
|
|
4047
|
+
}
|
|
4048
|
+
} finally {
|
|
4049
|
+
activeReclaimGuardIds.delete(guardId);
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
function claimObserverState(state, opts = {}) {
|
|
4053
|
+
const stateFile = opts.stateFile ?? FML_OBSERVER_STATE_FILE;
|
|
4054
|
+
const lockFile = opts.lockFile ?? FML_OBSERVER_LOCK_FILE;
|
|
4055
|
+
fs3.mkdirSync(path4.dirname(lockFile), { recursive: true, mode: 448 });
|
|
4056
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
4057
|
+
const reclaimFile = `${lockFile}.reclaim`;
|
|
4058
|
+
let initialPublicationRetries = 0;
|
|
4059
|
+
while (!fs3.existsSync(lockFile)) {
|
|
4060
|
+
const initialGuardId = acquireReclaimGuard(state, reclaimFile, nowMs, {
|
|
4061
|
+
pidAlive: opts.pidAlive,
|
|
4062
|
+
processIdentity: opts.processIdentity
|
|
4063
|
+
});
|
|
4064
|
+
if (!initialGuardId) return false;
|
|
4065
|
+
let retryPublication = false;
|
|
4066
|
+
try {
|
|
4067
|
+
try {
|
|
4068
|
+
if (writeLeaseAndState(
|
|
4069
|
+
state,
|
|
4070
|
+
stateFile,
|
|
4071
|
+
lockFile,
|
|
4072
|
+
reclaimFile,
|
|
4073
|
+
initialGuardId
|
|
4074
|
+
)) {
|
|
4075
|
+
return true;
|
|
4076
|
+
}
|
|
4077
|
+
} catch (error) {
|
|
4078
|
+
if (!isTransientObserverFileError(error) || initialPublicationRetries >= MAX_INITIAL_STATE_PUBLICATION_RETRIES) {
|
|
4079
|
+
throw error;
|
|
4080
|
+
}
|
|
4081
|
+
initialPublicationRetries += 1;
|
|
4082
|
+
retryPublication = true;
|
|
4083
|
+
}
|
|
4084
|
+
} finally {
|
|
4085
|
+
releaseReclaimGuard(reclaimFile, initialGuardId);
|
|
4086
|
+
}
|
|
4087
|
+
if (!retryPublication) break;
|
|
4088
|
+
}
|
|
4089
|
+
let observedRaw;
|
|
4090
|
+
let observedStat;
|
|
4091
|
+
try {
|
|
4092
|
+
observedRaw = fs3.readFileSync(lockFile, "utf8");
|
|
4093
|
+
observedStat = fs3.statSync(lockFile);
|
|
4094
|
+
} catch (error) {
|
|
4095
|
+
if (error.code === "ENOENT") return false;
|
|
4096
|
+
throw error;
|
|
4097
|
+
}
|
|
4098
|
+
const reclaimable = leaseCanBeReclaimed({
|
|
4099
|
+
rawOwner: observedRaw,
|
|
4100
|
+
lockMtimeMs: observedStat.mtimeMs,
|
|
4101
|
+
stateFile,
|
|
4102
|
+
nowMs,
|
|
4103
|
+
pidAlive: opts.pidAlive ?? isPidAlive,
|
|
4104
|
+
processIdentity: opts.processIdentity ?? getProcessIdentity,
|
|
4105
|
+
liveOwnerPolicy: opts.liveOwnerPolicy ?? "heartbeat"
|
|
4106
|
+
});
|
|
4107
|
+
if (!reclaimable) return false;
|
|
4108
|
+
const reclaimGuardId = acquireReclaimGuard(state, reclaimFile, nowMs, {
|
|
4109
|
+
pidAlive: opts.pidAlive,
|
|
4110
|
+
processIdentity: opts.processIdentity
|
|
4111
|
+
});
|
|
4112
|
+
if (!reclaimGuardId) return false;
|
|
4113
|
+
try {
|
|
4114
|
+
const currentRaw = fs3.readFileSync(lockFile, "utf8");
|
|
4115
|
+
const currentStat = fs3.statSync(lockFile);
|
|
4116
|
+
if (!ownsReclaimGuard(reclaimFile, reclaimGuardId) || currentRaw !== observedRaw || currentStat.dev !== observedStat.dev || currentStat.ino !== observedStat.ino || currentStat.mtimeMs !== observedStat.mtimeMs || !leaseCanBeReclaimed({
|
|
4117
|
+
rawOwner: currentRaw,
|
|
4118
|
+
lockMtimeMs: currentStat.mtimeMs,
|
|
4119
|
+
stateFile,
|
|
4120
|
+
nowMs,
|
|
4121
|
+
pidAlive: opts.pidAlive ?? isPidAlive,
|
|
4122
|
+
processIdentity: opts.processIdentity ?? getProcessIdentity,
|
|
4123
|
+
liveOwnerPolicy: opts.liveOwnerPolicy ?? "heartbeat"
|
|
4124
|
+
})) {
|
|
4125
|
+
return false;
|
|
4126
|
+
}
|
|
4127
|
+
fs3.unlinkSync(lockFile);
|
|
4128
|
+
return writeLeaseAndState(
|
|
4129
|
+
state,
|
|
4130
|
+
stateFile,
|
|
4131
|
+
lockFile,
|
|
4132
|
+
reclaimFile,
|
|
4133
|
+
reclaimGuardId
|
|
4134
|
+
);
|
|
4135
|
+
} catch (error) {
|
|
4136
|
+
if (error.code === "ENOENT") return false;
|
|
4137
|
+
throw error;
|
|
4138
|
+
} finally {
|
|
4139
|
+
releaseReclaimGuard(reclaimFile, reclaimGuardId);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
function writeObserverState(state, stateFile = FML_OBSERVER_STATE_FILE) {
|
|
4143
|
+
fs3.mkdirSync(path4.dirname(stateFile), { recursive: true, mode: 448 });
|
|
4144
|
+
const tempFile = `${stateFile}.${process.pid}.${randomUUID2()}.tmp`;
|
|
4145
|
+
try {
|
|
4146
|
+
fs3.writeFileSync(tempFile, serializeState(state), { mode: 384 });
|
|
4147
|
+
fs3.renameSync(tempFile, stateFile);
|
|
4148
|
+
} catch (error) {
|
|
4149
|
+
try {
|
|
4150
|
+
fs3.rmSync(tempFile, { force: true });
|
|
4151
|
+
} catch {
|
|
4152
|
+
}
|
|
4153
|
+
throw error;
|
|
4154
|
+
}
|
|
4155
|
+
try {
|
|
4156
|
+
fs3.chmodSync(stateFile, 384);
|
|
4157
|
+
} catch {
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
function updateObserverState(state, update, stateFile = FML_OBSERVER_STATE_FILE, lockFile = FML_OBSERVER_LOCK_FILE) {
|
|
4161
|
+
const reclaimFile = `${lockFile}.reclaim`;
|
|
4162
|
+
const reclaimGuardId = acquireReclaimGuard(state, reclaimFile, Date.now());
|
|
4163
|
+
if (!reclaimGuardId) {
|
|
4164
|
+
assertObserverStateMutationOwnership(
|
|
4165
|
+
state.instanceId,
|
|
4166
|
+
stateFile,
|
|
4167
|
+
lockFile,
|
|
4168
|
+
"observer lease ownership was lost"
|
|
4169
|
+
);
|
|
4170
|
+
throw new ObserverStateMutationBusyError();
|
|
4171
|
+
}
|
|
4172
|
+
try {
|
|
4173
|
+
assertObserverStateMutationOwnership(
|
|
4174
|
+
state.instanceId,
|
|
4175
|
+
stateFile,
|
|
4176
|
+
lockFile,
|
|
4177
|
+
"observer lease ownership was lost"
|
|
4178
|
+
);
|
|
4179
|
+
if (!ownsReclaimGuardForMutation(reclaimFile, reclaimGuardId)) {
|
|
4180
|
+
throw new ObserverStateMutationBusyError();
|
|
4181
|
+
}
|
|
4182
|
+
const next = { ...state, ...update };
|
|
4183
|
+
try {
|
|
4184
|
+
writeObserverState(next, stateFile);
|
|
4185
|
+
} catch (error) {
|
|
4186
|
+
if (isTransientObserverFileError(error)) {
|
|
4187
|
+
assertObserverStateMutationOwnership(
|
|
4188
|
+
state.instanceId,
|
|
4189
|
+
stateFile,
|
|
4190
|
+
lockFile,
|
|
4191
|
+
"observer lease ownership was lost during state update"
|
|
4192
|
+
);
|
|
4193
|
+
if (!ownsReclaimGuardForMutation(reclaimFile, reclaimGuardId)) {
|
|
4194
|
+
throw new ObserverStateMutationBusyError();
|
|
4195
|
+
}
|
|
4196
|
+
throw new ObserverStateMutationBusyError();
|
|
4197
|
+
}
|
|
4198
|
+
throw error;
|
|
4199
|
+
}
|
|
4200
|
+
assertObserverStateMutationOwnership(
|
|
4201
|
+
state.instanceId,
|
|
4202
|
+
stateFile,
|
|
4203
|
+
lockFile,
|
|
4204
|
+
"observer lease ownership was lost during state update"
|
|
4205
|
+
);
|
|
4206
|
+
if (!ownsReclaimGuardForMutation(reclaimFile, reclaimGuardId)) {
|
|
4207
|
+
throw new ObserverStateMutationBusyError();
|
|
4208
|
+
}
|
|
4209
|
+
return next;
|
|
4210
|
+
} finally {
|
|
4211
|
+
releaseReclaimGuard(reclaimFile, reclaimGuardId);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
function removeObserverStateIfOwned(instanceId, opts = {}) {
|
|
4215
|
+
const stateFile = opts.stateFile ?? FML_OBSERVER_STATE_FILE;
|
|
4216
|
+
const lockFile = opts.lockFile ?? FML_OBSERVER_LOCK_FILE;
|
|
4217
|
+
const reclaimFile = `${lockFile}.reclaim`;
|
|
4218
|
+
const reclaimGuardId = acquireReclaimGuard(
|
|
4219
|
+
{ pid: process.pid, instanceId },
|
|
4220
|
+
reclaimFile,
|
|
4221
|
+
Date.now()
|
|
4222
|
+
);
|
|
4223
|
+
if (!reclaimGuardId) {
|
|
4224
|
+
return;
|
|
4225
|
+
}
|
|
4226
|
+
try {
|
|
4227
|
+
if (!ownsReclaimGuard(reclaimFile, reclaimGuardId) || !observerLeaseIsOwned(instanceId, lockFile) || readObserverState(stateFile)?.instanceId !== instanceId) {
|
|
4228
|
+
return;
|
|
4229
|
+
}
|
|
4230
|
+
fs3.rmSync(stateFile, { force: true });
|
|
4231
|
+
if (ownsReclaimGuard(reclaimFile, reclaimGuardId) && observerLeaseIsOwned(instanceId, lockFile)) {
|
|
4232
|
+
fs3.rmSync(lockFile, { force: true });
|
|
4233
|
+
}
|
|
4234
|
+
} finally {
|
|
4235
|
+
releaseReclaimGuard(reclaimFile, reclaimGuardId);
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4239
|
+
// src/hooks/inbox.ts
|
|
4240
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4241
|
+
import fs4 from "fs";
|
|
4242
|
+
import path5 from "path";
|
|
3303
4243
|
|
|
3304
4244
|
// src/hooks/inbox-config.ts
|
|
3305
4245
|
var BYTE_SUFFIXES = {
|
|
@@ -3311,7 +4251,8 @@ var BYTE_SUFFIXES = {
|
|
|
3311
4251
|
var MAX_BYTES_PATTERN = /^([1-9]\d*)(b|kib|mib|gib)?$/i;
|
|
3312
4252
|
var POSITIVE_INTEGER_PATTERN = /^[1-9]\d*$/;
|
|
3313
4253
|
var DEFAULT_HOOK_INBOX_MAX_BYTES = 512 * 1024 * 1024;
|
|
3314
|
-
var
|
|
4254
|
+
var SMALLEST_REALISTIC_HOOK_EVENT_BYTES = 2 * 1024;
|
|
4255
|
+
var DEFAULT_HOOK_INBOX_MAX_ENTRIES = DEFAULT_HOOK_INBOX_MAX_BYTES / SMALLEST_REALISTIC_HOOK_EVENT_BYTES;
|
|
3315
4256
|
function parseHookInboxMaxBytes(configured) {
|
|
3316
4257
|
if (configured === void 0) return DEFAULT_HOOK_INBOX_MAX_BYTES;
|
|
3317
4258
|
const value = configured.trim();
|
|
@@ -3347,9 +4288,12 @@ function parseHookInboxMaxEntries(configured) {
|
|
|
3347
4288
|
}
|
|
3348
4289
|
return entries;
|
|
3349
4290
|
}
|
|
4291
|
+
function hookInboxEntriesForBytes(maxBytes) {
|
|
4292
|
+
return Math.ceil(maxBytes / SMALLEST_REALISTIC_HOOK_EVENT_BYTES);
|
|
4293
|
+
}
|
|
3350
4294
|
function hookInboxLimitsFromEnv(env = process.env) {
|
|
3351
4295
|
const maxBytes = parseHookInboxMaxBytes(env.FML_HOOK_INBOX_MAX_BYTES);
|
|
3352
|
-
const maxEntries = parseHookInboxMaxEntries(env.FML_HOOK_INBOX_MAX_ENTRIES);
|
|
4296
|
+
const maxEntries = env.FML_HOOK_INBOX_MAX_ENTRIES === void 0 ? maxBytes === void 0 ? void 0 : hookInboxEntriesForBytes(maxBytes) : parseHookInboxMaxEntries(env.FML_HOOK_INBOX_MAX_ENTRIES);
|
|
3353
4297
|
return {
|
|
3354
4298
|
...maxBytes === void 0 ? {} : { maxBytes },
|
|
3355
4299
|
...maxEntries === void 0 ? {} : { maxEntries }
|
|
@@ -3360,6 +4304,7 @@ function hookInboxLimitsFromEnv(env = process.env) {
|
|
|
3360
4304
|
var HOOK_INBOX_SCHEMA_VERSION = 1;
|
|
3361
4305
|
var MAX_HOOK_INBOX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
3362
4306
|
var MAX_HOOK_INBOX_DRAIN_ENTRIES = 100;
|
|
4307
|
+
var DRAIN_YIELD_EVERY_ENTRIES = 25;
|
|
3363
4308
|
var MAX_HOOK_INBOX_DRAIN_CURSORS = 8;
|
|
3364
4309
|
var INBOX_ID_PATTERN = /^\d{1,16}-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3365
4310
|
var INBOX_TEMP_PATTERN = /^\.(\d{1,16}-[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.\d+\.[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.tmp$/i;
|
|
@@ -3397,7 +4342,7 @@ var HookInboxCapacityError = class extends Error {
|
|
|
3397
4342
|
this.reason = reason;
|
|
3398
4343
|
}
|
|
3399
4344
|
};
|
|
3400
|
-
function
|
|
4345
|
+
function isRecord3(value) {
|
|
3401
4346
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
3402
4347
|
}
|
|
3403
4348
|
function validTimestamp(value) {
|
|
@@ -3408,7 +4353,7 @@ function validSyncId(value) {
|
|
|
3408
4353
|
}
|
|
3409
4354
|
function parseDestination(value) {
|
|
3410
4355
|
if (value === void 0) return void 0;
|
|
3411
|
-
if (!
|
|
4356
|
+
if (!isRecord3(value)) return null;
|
|
3412
4357
|
const { targetUrl, destinationIdentity } = value;
|
|
3413
4358
|
if (typeof targetUrl !== "string" || targetUrl.length === 0 || Buffer.byteLength(targetUrl, "utf8") > 4096 || typeof destinationIdentity !== "string" || destinationIdentity.length === 0 || Buffer.byteLength(destinationIdentity, "utf8") > 4096) {
|
|
3414
4359
|
return null;
|
|
@@ -3416,7 +4361,7 @@ function parseDestination(value) {
|
|
|
3416
4361
|
return { targetUrl, destinationIdentity };
|
|
3417
4362
|
}
|
|
3418
4363
|
function parseMetadata(value) {
|
|
3419
|
-
if (!
|
|
4364
|
+
if (!isRecord3(value)) return null;
|
|
3420
4365
|
if (typeof value.id !== "string" || !INBOX_ID_PATTERN.test(value.id) || !validSyncId(value.syncId) || !validTimestamp(value.capturedAtMs)) {
|
|
3421
4366
|
return null;
|
|
3422
4367
|
}
|
|
@@ -3441,12 +4386,12 @@ function entryFileName(id) {
|
|
|
3441
4386
|
return `${id}.json`;
|
|
3442
4387
|
}
|
|
3443
4388
|
function entryPath(inboxDir, id) {
|
|
3444
|
-
return
|
|
4389
|
+
return path5.join(inboxDir, entryFileName(id));
|
|
3445
4390
|
}
|
|
3446
4391
|
function enforceOwnerOnlyDirectory(inboxDir) {
|
|
3447
|
-
|
|
4392
|
+
fs4.mkdirSync(inboxDir, { recursive: true, mode: 448 });
|
|
3448
4393
|
try {
|
|
3449
|
-
|
|
4394
|
+
fs4.chmodSync(inboxDir, 448);
|
|
3450
4395
|
} catch {
|
|
3451
4396
|
}
|
|
3452
4397
|
}
|
|
@@ -3454,22 +4399,22 @@ function fsyncDirectory(inboxDir) {
|
|
|
3454
4399
|
if (process.platform === "win32") return;
|
|
3455
4400
|
let descriptor;
|
|
3456
4401
|
try {
|
|
3457
|
-
descriptor =
|
|
3458
|
-
|
|
4402
|
+
descriptor = fs4.openSync(inboxDir, "r");
|
|
4403
|
+
fs4.fsyncSync(descriptor);
|
|
3459
4404
|
} catch (error) {
|
|
3460
4405
|
const code = error.code;
|
|
3461
4406
|
if (code === void 0 || !UNSUPPORTED_DIRECTORY_FSYNC_CODES.has(code)) {
|
|
3462
4407
|
throw error;
|
|
3463
4408
|
}
|
|
3464
4409
|
} finally {
|
|
3465
|
-
if (descriptor !== void 0)
|
|
4410
|
+
if (descriptor !== void 0) fs4.closeSync(descriptor);
|
|
3466
4411
|
}
|
|
3467
4412
|
}
|
|
3468
4413
|
async function fsyncDirectoryAsync(inboxDir) {
|
|
3469
4414
|
if (process.platform === "win32") return;
|
|
3470
4415
|
let descriptor;
|
|
3471
4416
|
try {
|
|
3472
|
-
descriptor = await
|
|
4417
|
+
descriptor = await fs4.promises.open(inboxDir, "r");
|
|
3473
4418
|
await descriptor.sync();
|
|
3474
4419
|
} catch (error) {
|
|
3475
4420
|
const code = error.code;
|
|
@@ -3502,7 +4447,7 @@ function parseNullablePositiveInteger(value) {
|
|
|
3502
4447
|
}
|
|
3503
4448
|
function parsePendingCapacityMutation(value) {
|
|
3504
4449
|
if (value === void 0) return void 0;
|
|
3505
|
-
if (!
|
|
4450
|
+
if (!isRecord3(value)) return null;
|
|
3506
4451
|
if (typeof value.fileName !== "string" || !value.fileName.endsWith(".json") || !INBOX_ID_PATTERN.test(value.fileName.slice(0, -5)) || !nonNegativeInteger(value.bytes) || !validTimestamp(value.startedAtMs)) {
|
|
3507
4452
|
return null;
|
|
3508
4453
|
}
|
|
@@ -3523,7 +4468,7 @@ function parsePendingCapacityMutation(value) {
|
|
|
3523
4468
|
};
|
|
3524
4469
|
}
|
|
3525
4470
|
function parseCapacityState(value) {
|
|
3526
|
-
if (!
|
|
4471
|
+
if (!isRecord3(value) || value.schemaVersion !== CAPACITY_STATE_SCHEMA_VERSION || !nonNegativeInteger(value.pendingEntries) || !nonNegativeInteger(value.pendingBytes) || !nonNegativeInteger(value.rejectedEntries) || !nonNegativeInteger(value.rejectedBytes)) {
|
|
3527
4472
|
return null;
|
|
3528
4473
|
}
|
|
3529
4474
|
const maxEntries = parseNullablePositiveInteger(value.maxEntries);
|
|
@@ -3536,28 +4481,33 @@ function parseCapacityState(value) {
|
|
|
3536
4481
|
const lastRejectionReason = parseCapacityRejectionReason(
|
|
3537
4482
|
value.lastRejectionReason
|
|
3538
4483
|
);
|
|
4484
|
+
const lostEntries = nonNegativeInteger(value.lostEntries) ? value.lostEntries : 0;
|
|
4485
|
+
const lostBytes = nonNegativeInteger(value.lostBytes) ? value.lostBytes : 0;
|
|
3539
4486
|
return {
|
|
3540
4487
|
schemaVersion: CAPACITY_STATE_SCHEMA_VERSION,
|
|
3541
4488
|
pendingEntries: value.pendingEntries,
|
|
3542
4489
|
pendingBytes: value.pendingBytes,
|
|
3543
4490
|
rejectedEntries: value.rejectedEntries,
|
|
3544
4491
|
rejectedBytes: value.rejectedBytes,
|
|
4492
|
+
lostEntries,
|
|
4493
|
+
lostBytes,
|
|
3545
4494
|
maxEntries,
|
|
3546
4495
|
maxBytes,
|
|
3547
4496
|
...validTimestamp(value.lastRejectedAtMs) ? { lastRejectedAtMs: value.lastRejectedAtMs } : {},
|
|
3548
4497
|
...lastRejectionReason === void 0 ? {} : { lastRejectionReason },
|
|
4498
|
+
...validTimestamp(value.lastLostAtMs) ? { lastLostAtMs: value.lastLostAtMs } : {},
|
|
3549
4499
|
...pendingPersist === void 0 ? {} : { pendingPersist },
|
|
3550
4500
|
...pendingRemoval === void 0 ? {} : { pendingRemoval }
|
|
3551
4501
|
};
|
|
3552
4502
|
}
|
|
3553
4503
|
function capacityStatePath(inboxDir) {
|
|
3554
|
-
return
|
|
4504
|
+
return path5.join(inboxDir, CAPACITY_STATE_FILE);
|
|
3555
4505
|
}
|
|
3556
4506
|
function readCapacityStateFile(inboxDir) {
|
|
3557
4507
|
try {
|
|
3558
4508
|
return parseCapacityState(
|
|
3559
4509
|
JSON.parse(
|
|
3560
|
-
|
|
4510
|
+
fs4.readFileSync(capacityStatePath(inboxDir), "utf8")
|
|
3561
4511
|
)
|
|
3562
4512
|
);
|
|
3563
4513
|
} catch {
|
|
@@ -3571,6 +4521,8 @@ function defaultCapacityState(limits, pendingEntries, pendingBytes) {
|
|
|
3571
4521
|
pendingBytes,
|
|
3572
4522
|
rejectedEntries: 0,
|
|
3573
4523
|
rejectedBytes: 0,
|
|
4524
|
+
lostEntries: 0,
|
|
4525
|
+
lostBytes: 0,
|
|
3574
4526
|
maxEntries: limits.maxEntries ?? null,
|
|
3575
4527
|
maxBytes: limits.maxBytes ?? null
|
|
3576
4528
|
};
|
|
@@ -3579,26 +4531,26 @@ function writeCapacityState(inboxDir, state, capacityLock) {
|
|
|
3579
4531
|
enforceOwnerOnlyDirectory(inboxDir);
|
|
3580
4532
|
capacityLock.assertOwned();
|
|
3581
4533
|
const destination = capacityStatePath(inboxDir);
|
|
3582
|
-
const temporary =
|
|
4534
|
+
const temporary = path5.join(
|
|
3583
4535
|
inboxDir,
|
|
3584
|
-
`${CAPACITY_STATE_FILE}.${process.pid}.${
|
|
4536
|
+
`${CAPACITY_STATE_FILE}.${process.pid}.${randomUUID3()}.tmp`
|
|
3585
4537
|
);
|
|
3586
4538
|
let descriptor;
|
|
3587
4539
|
try {
|
|
3588
|
-
descriptor =
|
|
3589
|
-
|
|
4540
|
+
descriptor = fs4.openSync(temporary, "wx", 384);
|
|
4541
|
+
fs4.writeFileSync(descriptor, `${JSON.stringify(state)}
|
|
3590
4542
|
`, "utf8");
|
|
3591
|
-
|
|
4543
|
+
fs4.fsyncSync(descriptor);
|
|
3592
4544
|
capacityLock.assertOwned();
|
|
3593
|
-
|
|
4545
|
+
fs4.closeSync(descriptor);
|
|
3594
4546
|
descriptor = void 0;
|
|
3595
|
-
|
|
4547
|
+
fs4.renameSync(temporary, destination);
|
|
3596
4548
|
fsyncDirectory(inboxDir);
|
|
3597
4549
|
capacityLock.assertOwned();
|
|
3598
4550
|
} catch (error) {
|
|
3599
|
-
if (descriptor !== void 0)
|
|
4551
|
+
if (descriptor !== void 0) fs4.closeSync(descriptor);
|
|
3600
4552
|
try {
|
|
3601
|
-
|
|
4553
|
+
fs4.rmSync(temporary, { force: true });
|
|
3602
4554
|
} catch {
|
|
3603
4555
|
}
|
|
3604
4556
|
throw error;
|
|
@@ -3606,19 +4558,19 @@ function writeCapacityState(inboxDir, state, capacityLock) {
|
|
|
3606
4558
|
}
|
|
3607
4559
|
function capacityLockIsOwned(lockPath, descriptor, ownerToken) {
|
|
3608
4560
|
try {
|
|
3609
|
-
const openStat =
|
|
3610
|
-
const pathStat =
|
|
4561
|
+
const openStat = fs4.fstatSync(descriptor);
|
|
4562
|
+
const pathStat = fs4.statSync(lockPath);
|
|
3611
4563
|
if (openStat.dev !== pathStat.dev || openStat.ino !== pathStat.ino) {
|
|
3612
4564
|
return false;
|
|
3613
4565
|
}
|
|
3614
|
-
const value = JSON.parse(
|
|
3615
|
-
return
|
|
4566
|
+
const value = JSON.parse(fs4.readFileSync(lockPath, "utf8"));
|
|
4567
|
+
return isRecord3(value) && value.ownerToken === ownerToken;
|
|
3616
4568
|
} catch {
|
|
3617
4569
|
return false;
|
|
3618
4570
|
}
|
|
3619
4571
|
}
|
|
3620
4572
|
function parseCapacityLockOwner(value) {
|
|
3621
|
-
if (!
|
|
4573
|
+
if (!isRecord3(value) || typeof value.ownerToken !== "string" || value.ownerToken.length === 0 || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0 || !validTimestamp(value.createdAtMs)) {
|
|
3622
4574
|
return null;
|
|
3623
4575
|
}
|
|
3624
4576
|
return {
|
|
@@ -3630,7 +4582,7 @@ function parseCapacityLockOwner(value) {
|
|
|
3630
4582
|
function readCapacityLockOwner(lockPath) {
|
|
3631
4583
|
try {
|
|
3632
4584
|
return parseCapacityLockOwner(
|
|
3633
|
-
JSON.parse(
|
|
4585
|
+
JSON.parse(fs4.readFileSync(lockPath, "utf8"))
|
|
3634
4586
|
);
|
|
3635
4587
|
} catch {
|
|
3636
4588
|
return null;
|
|
@@ -3648,7 +4600,7 @@ function capacityLockOwnerIsAlive(pid) {
|
|
|
3648
4600
|
}
|
|
3649
4601
|
function restoreMovedCapacityLock(abandonedPath, lockPath) {
|
|
3650
4602
|
try {
|
|
3651
|
-
|
|
4603
|
+
fs4.renameSync(abandonedPath, lockPath);
|
|
3652
4604
|
} catch (error) {
|
|
3653
4605
|
if (!capacityLockRaceError(error)) throw error;
|
|
3654
4606
|
}
|
|
@@ -3660,7 +4612,7 @@ function capacityLockRaceError(error) {
|
|
|
3660
4612
|
function recoverStaleCapacityLock(lockPath, nowMs) {
|
|
3661
4613
|
let observed;
|
|
3662
4614
|
try {
|
|
3663
|
-
observed =
|
|
4615
|
+
observed = fs4.statSync(lockPath);
|
|
3664
4616
|
} catch (error) {
|
|
3665
4617
|
if (capacityLockRaceError(error)) return;
|
|
3666
4618
|
throw error;
|
|
@@ -3672,10 +4624,10 @@ function recoverStaleCapacityLock(lockPath, nowMs) {
|
|
|
3672
4624
|
if (!beyondStaleHorizon && (!observedOwner || capacityLockOwnerIsAlive(observedOwner.pid))) {
|
|
3673
4625
|
return;
|
|
3674
4626
|
}
|
|
3675
|
-
const abandoned = `${lockPath}.stale.${nowMs}.${
|
|
4627
|
+
const abandoned = `${lockPath}.stale.${nowMs}.${randomUUID3()}`;
|
|
3676
4628
|
try {
|
|
3677
|
-
|
|
3678
|
-
const moved =
|
|
4629
|
+
fs4.renameSync(lockPath, abandoned);
|
|
4630
|
+
const moved = fs4.statSync(abandoned);
|
|
3679
4631
|
if (moved.dev !== observed.dev || moved.ino !== observed.ino) {
|
|
3680
4632
|
restoreMovedCapacityLock(abandoned, lockPath);
|
|
3681
4633
|
return;
|
|
@@ -3687,8 +4639,8 @@ function recoverStaleCapacityLock(lockPath, nowMs) {
|
|
|
3687
4639
|
restoreMovedCapacityLock(abandoned, lockPath);
|
|
3688
4640
|
return;
|
|
3689
4641
|
}
|
|
3690
|
-
|
|
3691
|
-
fsyncDirectory(
|
|
4642
|
+
fs4.rmSync(abandoned, { force: true });
|
|
4643
|
+
fsyncDirectory(path5.dirname(lockPath));
|
|
3692
4644
|
} catch (error) {
|
|
3693
4645
|
if (!capacityLockRaceError(error)) throw error;
|
|
3694
4646
|
}
|
|
@@ -3699,21 +4651,21 @@ function sleepSync(milliseconds) {
|
|
|
3699
4651
|
}
|
|
3700
4652
|
function acquireCapacityLock(inboxDir, waitMs = CAPACITY_LOCK_WAIT_MS) {
|
|
3701
4653
|
enforceOwnerOnlyDirectory(inboxDir);
|
|
3702
|
-
const lockPath =
|
|
4654
|
+
const lockPath = path5.join(inboxDir, CAPACITY_LOCK_FILE);
|
|
3703
4655
|
const deadlineMs = Date.now() + Math.max(0, waitMs);
|
|
3704
4656
|
while (true) {
|
|
3705
4657
|
let descriptor;
|
|
3706
4658
|
let ownerToken;
|
|
3707
4659
|
try {
|
|
3708
|
-
descriptor =
|
|
3709
|
-
ownerToken =
|
|
3710
|
-
|
|
4660
|
+
descriptor = fs4.openSync(lockPath, "wx+", 384);
|
|
4661
|
+
ownerToken = randomUUID3();
|
|
4662
|
+
fs4.writeFileSync(
|
|
3711
4663
|
descriptor,
|
|
3712
4664
|
`${JSON.stringify({ ownerToken, pid: process.pid, createdAtMs: Date.now() })}
|
|
3713
4665
|
`,
|
|
3714
4666
|
"utf8"
|
|
3715
4667
|
);
|
|
3716
|
-
|
|
4668
|
+
fs4.fsyncSync(descriptor);
|
|
3717
4669
|
let released = false;
|
|
3718
4670
|
const capacityLock = {
|
|
3719
4671
|
assertOwned() {
|
|
@@ -3727,14 +4679,14 @@ function acquireCapacityLock(inboxDir, waitMs = CAPACITY_LOCK_WAIT_MS) {
|
|
|
3727
4679
|
let releaseError;
|
|
3728
4680
|
try {
|
|
3729
4681
|
if (capacityLockIsOwned(lockPath, descriptor, ownerToken)) {
|
|
3730
|
-
|
|
4682
|
+
fs4.unlinkSync(lockPath);
|
|
3731
4683
|
fsyncDirectory(inboxDir);
|
|
3732
4684
|
}
|
|
3733
4685
|
} catch (error) {
|
|
3734
4686
|
releaseError = error;
|
|
3735
4687
|
}
|
|
3736
4688
|
try {
|
|
3737
|
-
|
|
4689
|
+
fs4.closeSync(descriptor);
|
|
3738
4690
|
} catch (error) {
|
|
3739
4691
|
releaseError ??= error;
|
|
3740
4692
|
}
|
|
@@ -3747,12 +4699,12 @@ function acquireCapacityLock(inboxDir, waitMs = CAPACITY_LOCK_WAIT_MS) {
|
|
|
3747
4699
|
if (descriptor !== void 0) {
|
|
3748
4700
|
const removeOwnedLock = ownerToken !== void 0 && capacityLockIsOwned(lockPath, descriptor, ownerToken);
|
|
3749
4701
|
try {
|
|
3750
|
-
|
|
4702
|
+
fs4.closeSync(descriptor);
|
|
3751
4703
|
} catch {
|
|
3752
4704
|
}
|
|
3753
4705
|
if (removeOwnedLock) {
|
|
3754
4706
|
try {
|
|
3755
|
-
|
|
4707
|
+
fs4.rmSync(lockPath, { force: true });
|
|
3756
4708
|
fsyncDirectory(inboxDir);
|
|
3757
4709
|
} catch {
|
|
3758
4710
|
}
|
|
@@ -3772,23 +4724,23 @@ function capacityMutationEvidence(inboxDir, pending, includeTemporary) {
|
|
|
3772
4724
|
const prefix = `.${id}.`;
|
|
3773
4725
|
const candidates = [];
|
|
3774
4726
|
if (includeTemporary && pending.temporaryFileName) {
|
|
3775
|
-
candidates.push(
|
|
4727
|
+
candidates.push(path5.join(inboxDir, pending.temporaryFileName));
|
|
3776
4728
|
} else if (includeTemporary) {
|
|
3777
4729
|
try {
|
|
3778
|
-
for (const name of
|
|
4730
|
+
for (const name of fs4.readdirSync(inboxDir)) {
|
|
3779
4731
|
if (name.startsWith(prefix) && name.endsWith(".tmp")) {
|
|
3780
|
-
candidates.push(
|
|
4732
|
+
candidates.push(path5.join(inboxDir, name));
|
|
3781
4733
|
}
|
|
3782
4734
|
}
|
|
3783
4735
|
} catch (error) {
|
|
3784
4736
|
return error.code === "ENOENT" ? { kind: "missing" } : { kind: "unavailable" };
|
|
3785
4737
|
}
|
|
3786
4738
|
}
|
|
3787
|
-
candidates.push(
|
|
4739
|
+
candidates.push(path5.join(inboxDir, fileName));
|
|
3788
4740
|
let presentBytes;
|
|
3789
4741
|
for (const candidate of candidates) {
|
|
3790
4742
|
try {
|
|
3791
|
-
const stat =
|
|
4743
|
+
const stat = fs4.lstatSync(candidate);
|
|
3792
4744
|
if (!stat.isFile() || presentBytes !== void 0) {
|
|
3793
4745
|
return { kind: "unavailable" };
|
|
3794
4746
|
}
|
|
@@ -3887,7 +4839,7 @@ function removeCapacityTrackedEntry(filePath, inboxDir, expectedBytes, missingIs
|
|
|
3887
4839
|
);
|
|
3888
4840
|
let bytes = expectedBytes;
|
|
3889
4841
|
try {
|
|
3890
|
-
const stat =
|
|
4842
|
+
const stat = fs4.lstatSync(filePath);
|
|
3891
4843
|
if (!stat.isFile()) return false;
|
|
3892
4844
|
bytes = stat.size;
|
|
3893
4845
|
} catch (error) {
|
|
@@ -3896,7 +4848,7 @@ function removeCapacityTrackedEntry(filePath, inboxDir, expectedBytes, missingIs
|
|
|
3896
4848
|
}
|
|
3897
4849
|
if (bytes === void 0) return false;
|
|
3898
4850
|
const pendingRemoval = {
|
|
3899
|
-
fileName:
|
|
4851
|
+
fileName: path5.basename(filePath),
|
|
3900
4852
|
bytes,
|
|
3901
4853
|
startedAtMs: Date.now()
|
|
3902
4854
|
};
|
|
@@ -3909,7 +4861,7 @@ function removeCapacityTrackedEntry(filePath, inboxDir, expectedBytes, missingIs
|
|
|
3909
4861
|
writeCapacityState(inboxDir, state, capacityLock);
|
|
3910
4862
|
capacityLock.assertOwned();
|
|
3911
4863
|
try {
|
|
3912
|
-
|
|
4864
|
+
fs4.unlinkSync(filePath);
|
|
3913
4865
|
} catch (error) {
|
|
3914
4866
|
if (error.code !== "ENOENT") {
|
|
3915
4867
|
try {
|
|
@@ -3944,12 +4896,12 @@ function parseRetirementReason(value) {
|
|
|
3944
4896
|
return value === "malformed" || value === "terminal_rejected" ? value : void 0;
|
|
3945
4897
|
}
|
|
3946
4898
|
function parseRetirementState(value) {
|
|
3947
|
-
if (!
|
|
4899
|
+
if (!isRecord3(value) || value.schemaVersion !== RETIREMENT_STATE_SCHEMA_VERSION || !nonNegativeInteger(value.retiredEntries) || !nonNegativeInteger(value.retiredBytes)) {
|
|
3948
4900
|
return null;
|
|
3949
4901
|
}
|
|
3950
4902
|
let pending;
|
|
3951
4903
|
if (value.pending !== void 0) {
|
|
3952
|
-
if (!
|
|
4904
|
+
if (!isRecord3(value.pending)) return null;
|
|
3953
4905
|
const reason = parseRetirementReason(value.pending.reason);
|
|
3954
4906
|
if (typeof value.pending.fileName !== "string" || !value.pending.fileName.endsWith(".json") || !INBOX_ID_PATTERN.test(value.pending.fileName.slice(0, -5)) || !nonNegativeInteger(value.pending.bytes) || !validTimestamp(value.pending.retiredAtMs) || reason === void 0) {
|
|
3955
4907
|
return null;
|
|
@@ -3977,7 +4929,7 @@ function readRetirementState(inboxDir) {
|
|
|
3977
4929
|
try {
|
|
3978
4930
|
return parseRetirementState(
|
|
3979
4931
|
JSON.parse(
|
|
3980
|
-
|
|
4932
|
+
fs4.readFileSync(path5.join(inboxDir, RETIREMENT_STATE_FILE), "utf8")
|
|
3981
4933
|
)
|
|
3982
4934
|
) ?? defaultRetirementState();
|
|
3983
4935
|
} catch {
|
|
@@ -3986,25 +4938,25 @@ function readRetirementState(inboxDir) {
|
|
|
3986
4938
|
}
|
|
3987
4939
|
function writeRetirementState(inboxDir, state) {
|
|
3988
4940
|
enforceOwnerOnlyDirectory(inboxDir);
|
|
3989
|
-
const destination =
|
|
3990
|
-
const temporary =
|
|
4941
|
+
const destination = path5.join(inboxDir, RETIREMENT_STATE_FILE);
|
|
4942
|
+
const temporary = path5.join(
|
|
3991
4943
|
inboxDir,
|
|
3992
|
-
`${RETIREMENT_STATE_FILE}.${process.pid}.${
|
|
4944
|
+
`${RETIREMENT_STATE_FILE}.${process.pid}.${randomUUID3()}.tmp`
|
|
3993
4945
|
);
|
|
3994
4946
|
let descriptor;
|
|
3995
4947
|
try {
|
|
3996
|
-
descriptor =
|
|
3997
|
-
|
|
4948
|
+
descriptor = fs4.openSync(temporary, "wx", 384);
|
|
4949
|
+
fs4.writeFileSync(descriptor, `${JSON.stringify(state)}
|
|
3998
4950
|
`, "utf8");
|
|
3999
|
-
|
|
4000
|
-
|
|
4951
|
+
fs4.fsyncSync(descriptor);
|
|
4952
|
+
fs4.closeSync(descriptor);
|
|
4001
4953
|
descriptor = void 0;
|
|
4002
|
-
|
|
4954
|
+
fs4.renameSync(temporary, destination);
|
|
4003
4955
|
fsyncDirectory(inboxDir);
|
|
4004
4956
|
} catch (error) {
|
|
4005
|
-
if (descriptor !== void 0)
|
|
4957
|
+
if (descriptor !== void 0) fs4.closeSync(descriptor);
|
|
4006
4958
|
try {
|
|
4007
|
-
|
|
4959
|
+
fs4.rmSync(temporary, { force: true });
|
|
4008
4960
|
} catch {
|
|
4009
4961
|
}
|
|
4010
4962
|
throw error;
|
|
@@ -4014,7 +4966,7 @@ function completePendingRetirement(inboxDir, state) {
|
|
|
4014
4966
|
const pending = state.pending;
|
|
4015
4967
|
if (!pending) return state;
|
|
4016
4968
|
if (!removeCapacityTrackedEntry(
|
|
4017
|
-
|
|
4969
|
+
path5.join(inboxDir, pending.fileName),
|
|
4018
4970
|
inboxDir,
|
|
4019
4971
|
pending.bytes,
|
|
4020
4972
|
true
|
|
@@ -4043,14 +4995,14 @@ function retireEntry(filePath, reason, inboxDir) {
|
|
|
4043
4995
|
if (!current) return false;
|
|
4044
4996
|
let bytes;
|
|
4045
4997
|
try {
|
|
4046
|
-
bytes =
|
|
4998
|
+
bytes = fs4.lstatSync(filePath).size;
|
|
4047
4999
|
} catch {
|
|
4048
5000
|
return false;
|
|
4049
5001
|
}
|
|
4050
5002
|
const pending = {
|
|
4051
5003
|
...current,
|
|
4052
5004
|
pending: {
|
|
4053
|
-
fileName:
|
|
5005
|
+
fileName: path5.basename(filePath),
|
|
4054
5006
|
bytes,
|
|
4055
5007
|
reason,
|
|
4056
5008
|
retiredAtMs: Date.now()
|
|
@@ -4069,7 +5021,7 @@ function createHookInboxEntry(request, event, destination, persistedAtMs = Date.
|
|
|
4069
5021
|
schemaVersion: HOOK_INBOX_SCHEMA_VERSION,
|
|
4070
5022
|
// File ordering follows the local durable write, not an agent-supplied
|
|
4071
5023
|
// event timestamp that may be delayed or backdated behind a drain cursor.
|
|
4072
|
-
id: `${persistedAtMs}-${
|
|
5024
|
+
id: `${persistedAtMs}-${randomUUID3()}`,
|
|
4073
5025
|
syncId: event.syncId,
|
|
4074
5026
|
capturedAtMs,
|
|
4075
5027
|
...event.repository ? { repository: event.repository } : {},
|
|
@@ -4101,9 +5053,9 @@ function persistHookInboxEntry(entry, inboxDir = FML_HOOK_INBOX_DIR, limits = ho
|
|
|
4101
5053
|
throw new Error("FML hook inbox admission is busy");
|
|
4102
5054
|
}
|
|
4103
5055
|
const destination = entryPath(inboxDir, entry.id);
|
|
4104
|
-
const temporary =
|
|
5056
|
+
const temporary = path5.join(
|
|
4105
5057
|
inboxDir,
|
|
4106
|
-
`.${entry.id}.${process.pid}.${
|
|
5058
|
+
`.${entry.id}.${process.pid}.${randomUUID3()}.tmp`
|
|
4107
5059
|
);
|
|
4108
5060
|
let descriptor;
|
|
4109
5061
|
let state;
|
|
@@ -4131,8 +5083,8 @@ function persistHookInboxEntry(entry, inboxDir = FML_HOOK_INBOX_DIR, limits = ho
|
|
|
4131
5083
|
}
|
|
4132
5084
|
try {
|
|
4133
5085
|
const pendingPersist = {
|
|
4134
|
-
fileName:
|
|
4135
|
-
temporaryFileName:
|
|
5086
|
+
fileName: path5.basename(destination),
|
|
5087
|
+
temporaryFileName: path5.basename(temporary),
|
|
4136
5088
|
bytes: serializedBytes,
|
|
4137
5089
|
startedAtMs: Date.now()
|
|
4138
5090
|
};
|
|
@@ -4144,23 +5096,23 @@ function persistHookInboxEntry(entry, inboxDir = FML_HOOK_INBOX_DIR, limits = ho
|
|
|
4144
5096
|
};
|
|
4145
5097
|
writeCapacityState(inboxDir, state, capacityLock);
|
|
4146
5098
|
capacityLock.assertOwned();
|
|
4147
|
-
descriptor =
|
|
4148
|
-
|
|
4149
|
-
|
|
5099
|
+
descriptor = fs4.openSync(temporary, "wx", 384);
|
|
5100
|
+
fs4.writeFileSync(descriptor, serialized, "utf8");
|
|
5101
|
+
fs4.fsyncSync(descriptor);
|
|
4150
5102
|
capacityLock.assertOwned();
|
|
4151
|
-
|
|
5103
|
+
fs4.closeSync(descriptor);
|
|
4152
5104
|
descriptor = void 0;
|
|
4153
|
-
|
|
5105
|
+
fs4.renameSync(temporary, destination);
|
|
4154
5106
|
try {
|
|
4155
|
-
|
|
5107
|
+
fs4.chmodSync(destination, 384);
|
|
4156
5108
|
} catch {
|
|
4157
5109
|
}
|
|
4158
5110
|
fsyncDirectory(inboxDir);
|
|
4159
5111
|
capacityLock.assertOwned();
|
|
4160
5112
|
} catch (error) {
|
|
4161
|
-
if (descriptor !== void 0)
|
|
5113
|
+
if (descriptor !== void 0) fs4.closeSync(descriptor);
|
|
4162
5114
|
try {
|
|
4163
|
-
|
|
5115
|
+
fs4.rmSync(temporary, { force: true });
|
|
4164
5116
|
} catch {
|
|
4165
5117
|
}
|
|
4166
5118
|
if (state.pendingPersist) {
|
|
@@ -4188,10 +5140,10 @@ function hookInboxDeliveryPayload(entry) {
|
|
|
4188
5140
|
};
|
|
4189
5141
|
}
|
|
4190
5142
|
function parseHookInboxMetadata(value) {
|
|
4191
|
-
return
|
|
5143
|
+
return isRecord3(value) ? parseMetadata(value[INBOX_METADATA_KEY]) : null;
|
|
4192
5144
|
}
|
|
4193
5145
|
function parseHookInboxEntry(value) {
|
|
4194
|
-
if (!
|
|
5146
|
+
if (!isRecord3(value) || value.schemaVersion !== HOOK_INBOX_SCHEMA_VERSION) {
|
|
4195
5147
|
return null;
|
|
4196
5148
|
}
|
|
4197
5149
|
const metadata = parseMetadata(value);
|
|
@@ -4211,21 +5163,21 @@ function sourceDeclaresForwardSchema(source) {
|
|
|
4211
5163
|
}
|
|
4212
5164
|
function readEntryResult(filePath) {
|
|
4213
5165
|
try {
|
|
4214
|
-
const stat =
|
|
5166
|
+
const stat = fs4.lstatSync(filePath);
|
|
4215
5167
|
if (!stat.isFile()) {
|
|
4216
5168
|
return { kind: "malformed" };
|
|
4217
5169
|
}
|
|
4218
5170
|
if (stat.size > MAX_HOOK_INBOX_ENTRY_BYTES) {
|
|
4219
5171
|
return { kind: "unsupported_schema" };
|
|
4220
5172
|
}
|
|
4221
|
-
const source =
|
|
5173
|
+
const source = fs4.readFileSync(filePath, "utf8");
|
|
4222
5174
|
let value;
|
|
4223
5175
|
try {
|
|
4224
5176
|
value = JSON.parse(source);
|
|
4225
5177
|
} catch {
|
|
4226
5178
|
return sourceDeclaresForwardSchema(source) ? { kind: "unsupported_schema" } : { kind: "malformed" };
|
|
4227
5179
|
}
|
|
4228
|
-
if (
|
|
5180
|
+
if (isRecord3(value) && typeof value.schemaVersion === "number" && Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > HOOK_INBOX_SCHEMA_VERSION) {
|
|
4229
5181
|
return { kind: "unsupported_schema" };
|
|
4230
5182
|
}
|
|
4231
5183
|
const entry = parseHookInboxEntry(value);
|
|
@@ -4283,19 +5235,19 @@ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, exclud
|
|
|
4283
5235
|
let blockCursorAdvance = false;
|
|
4284
5236
|
let removedOrphan = false;
|
|
4285
5237
|
const cutoffMs = nowMs - STALE_INBOX_TEMP_AGE_MS;
|
|
4286
|
-
const directory = await
|
|
5238
|
+
const directory = await fs4.promises.opendir(inboxDir);
|
|
4287
5239
|
try {
|
|
4288
5240
|
for (let dirent = await directory.read(); dirent !== null; dirent = await directory.read()) {
|
|
4289
5241
|
const name = dirent.name;
|
|
4290
5242
|
const abandonedLockMatch = ABANDONED_CAPACITY_LOCK_PATTERN.exec(name);
|
|
4291
5243
|
if (INBOX_STATE_TEMP_PATTERN.test(name) || abandonedLockMatch !== null) {
|
|
4292
5244
|
try {
|
|
4293
|
-
const orphanPath =
|
|
4294
|
-
const stat = await
|
|
5245
|
+
const orphanPath = path5.join(inboxDir, name);
|
|
5246
|
+
const stat = await fs4.promises.lstat(orphanPath);
|
|
4295
5247
|
const abandonedAtMs = abandonedLockMatch ? Number(abandonedLockMatch[1]) : void 0;
|
|
4296
5248
|
const isStale = abandonedLockMatch ? validTimestamp(abandonedAtMs) && abandonedAtMs <= cutoffMs : stat.mtimeMs <= cutoffMs;
|
|
4297
5249
|
if (stat.isFile() && isStale) {
|
|
4298
|
-
await
|
|
5250
|
+
await fs4.promises.unlink(orphanPath);
|
|
4299
5251
|
removedOrphan = true;
|
|
4300
5252
|
}
|
|
4301
5253
|
} catch (error) {
|
|
@@ -4313,27 +5265,27 @@ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, exclud
|
|
|
4313
5265
|
const id = match?.[1];
|
|
4314
5266
|
if (!id) continue;
|
|
4315
5267
|
const destination = entryPath(inboxDir, id);
|
|
4316
|
-
const destinationName =
|
|
4317
|
-
const temporary =
|
|
5268
|
+
const destinationName = path5.basename(destination);
|
|
5269
|
+
const temporary = path5.join(inboxDir, name);
|
|
4318
5270
|
try {
|
|
4319
|
-
const stat = await
|
|
5271
|
+
const stat = await fs4.promises.lstat(temporary);
|
|
4320
5272
|
if (!stat.isFile()) continue;
|
|
4321
5273
|
blockCursorAdvance = true;
|
|
4322
5274
|
if (skipThrough && destinationName <= skipThrough) resetCursor = true;
|
|
4323
5275
|
if (stat.mtimeMs > cutoffMs) continue;
|
|
4324
5276
|
let destinationExists = false;
|
|
4325
5277
|
try {
|
|
4326
|
-
await
|
|
5278
|
+
await fs4.promises.lstat(destination);
|
|
4327
5279
|
destinationExists = true;
|
|
4328
5280
|
} catch (error) {
|
|
4329
5281
|
if (error.code !== "ENOENT") throw error;
|
|
4330
5282
|
}
|
|
4331
5283
|
if (destinationExists) {
|
|
4332
|
-
await
|
|
5284
|
+
await fs4.promises.unlink(temporary);
|
|
4333
5285
|
} else {
|
|
4334
|
-
await
|
|
5286
|
+
await fs4.promises.rename(temporary, destination);
|
|
4335
5287
|
try {
|
|
4336
|
-
await
|
|
5288
|
+
await fs4.promises.chmod(destination, 384);
|
|
4337
5289
|
} catch {
|
|
4338
5290
|
}
|
|
4339
5291
|
}
|
|
@@ -4367,7 +5319,7 @@ async function scanDrainEntryNames(inboxDir, maximumEntries, skipThrough, exclud
|
|
|
4367
5319
|
return { names, failed, resetCursor, blockCursorAdvance };
|
|
4368
5320
|
}
|
|
4369
5321
|
function drainCursor(inboxDir) {
|
|
4370
|
-
const key =
|
|
5322
|
+
const key = path5.resolve(inboxDir);
|
|
4371
5323
|
const existing = HOOK_INBOX_DRAIN_CURSORS.get(key);
|
|
4372
5324
|
if (existing) {
|
|
4373
5325
|
HOOK_INBOX_DRAIN_CURSORS.delete(key);
|
|
@@ -4399,12 +5351,12 @@ function safeDrainCursor(cursor, nowMs) {
|
|
|
4399
5351
|
async function pendingInboxSizeAsync(inboxDir) {
|
|
4400
5352
|
let pendingEntries = 0;
|
|
4401
5353
|
let pendingBytes = 0;
|
|
4402
|
-
const directory = await
|
|
5354
|
+
const directory = await fs4.promises.opendir(inboxDir);
|
|
4403
5355
|
try {
|
|
4404
5356
|
for (let dirent = await directory.read(); dirent !== null; dirent = await directory.read()) {
|
|
4405
5357
|
if (!isPendingEntryName(dirent.name)) continue;
|
|
4406
5358
|
try {
|
|
4407
|
-
const stat = await
|
|
5359
|
+
const stat = await fs4.promises.lstat(path5.join(inboxDir, dirent.name));
|
|
4408
5360
|
if (stat.isFile()) {
|
|
4409
5361
|
pendingEntries += 1;
|
|
4410
5362
|
pendingBytes += stat.size;
|
|
@@ -4421,14 +5373,14 @@ async function pendingInboxSizeAsync(inboxDir) {
|
|
|
4421
5373
|
function pendingInboxSize(inboxDir, includeTemporary = false) {
|
|
4422
5374
|
let pendingEntries = 0;
|
|
4423
5375
|
let pendingBytes = 0;
|
|
4424
|
-
const directory =
|
|
5376
|
+
const directory = fs4.opendirSync(inboxDir);
|
|
4425
5377
|
try {
|
|
4426
5378
|
for (let dirent = directory.readSync(); dirent !== null; dirent = directory.readSync()) {
|
|
4427
5379
|
if (!isPendingEntryName(dirent.name) && !(includeTemporary && INBOX_TEMP_PATTERN.test(dirent.name))) {
|
|
4428
5380
|
continue;
|
|
4429
5381
|
}
|
|
4430
5382
|
try {
|
|
4431
|
-
const stat =
|
|
5383
|
+
const stat = fs4.lstatSync(path5.join(inboxDir, dirent.name));
|
|
4432
5384
|
if (stat.isFile()) {
|
|
4433
5385
|
pendingEntries += 1;
|
|
4434
5386
|
pendingBytes += stat.size;
|
|
@@ -4463,6 +5415,8 @@ function emptyHookInboxStatus(limits, unreadable) {
|
|
|
4463
5415
|
maxBytes: limits.maxBytes ?? null,
|
|
4464
5416
|
capacityRejectedEntries: 0,
|
|
4465
5417
|
capacityRejectedBytes: 0,
|
|
5418
|
+
lostEntries: 0,
|
|
5419
|
+
lostBytes: 0,
|
|
4466
5420
|
retiredEntries: 0,
|
|
4467
5421
|
retiredBytes: 0,
|
|
4468
5422
|
unreadable
|
|
@@ -4488,6 +5442,9 @@ function readHookInboxStatus(inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4488
5442
|
capacityRejectedBytes: capacity.rejectedBytes,
|
|
4489
5443
|
...capacity.lastRejectedAtMs === void 0 ? {} : { lastCapacityRejectedAtMs: capacity.lastRejectedAtMs },
|
|
4490
5444
|
...capacity.lastRejectionReason === void 0 ? {} : { lastCapacityRejectionReason: capacity.lastRejectionReason },
|
|
5445
|
+
lostEntries: capacity.lostEntries,
|
|
5446
|
+
lostBytes: capacity.lostBytes,
|
|
5447
|
+
...capacity.lastLostAtMs === void 0 ? {} : { lastLostAtMs: capacity.lastLostAtMs },
|
|
4491
5448
|
retiredEntries: retirement.retiredEntries,
|
|
4492
5449
|
retiredBytes: retirement.retiredBytes,
|
|
4493
5450
|
unreadable: false
|
|
@@ -4499,6 +5456,50 @@ function readHookInboxStatus(inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4499
5456
|
return emptyHookInboxStatus(limits, true);
|
|
4500
5457
|
}
|
|
4501
5458
|
}
|
|
5459
|
+
function readHookInboxCapacityCounters(inboxDir = FML_HOOK_INBOX_DIR) {
|
|
5460
|
+
const capacity = readCapacityStateFile(inboxDir);
|
|
5461
|
+
if (!capacity) return null;
|
|
5462
|
+
return {
|
|
5463
|
+
rejectedEntries: capacity.rejectedEntries,
|
|
5464
|
+
rejectedBytes: capacity.rejectedBytes,
|
|
5465
|
+
...capacity.lastRejectedAtMs === void 0 ? {} : { lastRejectedAtMs: capacity.lastRejectedAtMs },
|
|
5466
|
+
...capacity.lastRejectionReason === void 0 ? {} : { lastRejectionReason: capacity.lastRejectionReason },
|
|
5467
|
+
lostEntries: capacity.lostEntries,
|
|
5468
|
+
lostBytes: capacity.lostBytes,
|
|
5469
|
+
...capacity.lastLostAtMs === void 0 ? {} : { lastLostAtMs: capacity.lastLostAtMs }
|
|
5470
|
+
};
|
|
5471
|
+
}
|
|
5472
|
+
function recordHookInboxLoss(inboxDir = FML_HOOK_INBOX_DIR, bytes, capacityLockWaitMs = CAPACITY_LOCK_WAIT_MS) {
|
|
5473
|
+
let capacityLock;
|
|
5474
|
+
try {
|
|
5475
|
+
enforceOwnerOnlyDirectory(inboxDir);
|
|
5476
|
+
capacityLock = acquireCapacityLock(inboxDir, capacityLockWaitMs);
|
|
5477
|
+
} catch {
|
|
5478
|
+
return false;
|
|
5479
|
+
}
|
|
5480
|
+
if (!capacityLock) return false;
|
|
5481
|
+
try {
|
|
5482
|
+
const state = loadCapacityState(
|
|
5483
|
+
inboxDir,
|
|
5484
|
+
maintenanceCapacityLimits(inboxDir)
|
|
5485
|
+
);
|
|
5486
|
+
writeCapacityState(
|
|
5487
|
+
inboxDir,
|
|
5488
|
+
{
|
|
5489
|
+
...state,
|
|
5490
|
+
lostEntries: saturatingAdd(state.lostEntries, 1),
|
|
5491
|
+
lostBytes: saturatingAdd(state.lostBytes, Math.max(0, bytes)),
|
|
5492
|
+
lastLostAtMs: Date.now()
|
|
5493
|
+
},
|
|
5494
|
+
capacityLock
|
|
5495
|
+
);
|
|
5496
|
+
return true;
|
|
5497
|
+
} catch {
|
|
5498
|
+
return false;
|
|
5499
|
+
} finally {
|
|
5500
|
+
capacityLock.release();
|
|
5501
|
+
}
|
|
5502
|
+
}
|
|
4502
5503
|
async function readHookInboxStatusAsync(inboxDir = FML_HOOK_INBOX_DIR) {
|
|
4503
5504
|
let limits;
|
|
4504
5505
|
try {
|
|
@@ -4519,6 +5520,9 @@ async function readHookInboxStatusAsync(inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4519
5520
|
capacityRejectedBytes: capacity.rejectedBytes,
|
|
4520
5521
|
...capacity.lastRejectedAtMs === void 0 ? {} : { lastCapacityRejectedAtMs: capacity.lastRejectedAtMs },
|
|
4521
5522
|
...capacity.lastRejectionReason === void 0 ? {} : { lastCapacityRejectionReason: capacity.lastRejectionReason },
|
|
5523
|
+
lostEntries: capacity.lostEntries,
|
|
5524
|
+
lostBytes: capacity.lostBytes,
|
|
5525
|
+
...capacity.lastLostAtMs === void 0 ? {} : { lastLostAtMs: capacity.lastLostAtMs },
|
|
4522
5526
|
retiredEntries: retirement.retiredEntries,
|
|
4523
5527
|
retiredBytes: retirement.retiredBytes,
|
|
4524
5528
|
unreadable: false
|
|
@@ -4530,7 +5534,10 @@ async function readHookInboxStatusAsync(inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4530
5534
|
return emptyHookInboxStatus(limits, true);
|
|
4531
5535
|
}
|
|
4532
5536
|
}
|
|
4533
|
-
|
|
5537
|
+
var defaultYieldToEventLoop = () => new Promise((resolve) => setImmediate(resolve));
|
|
5538
|
+
async function drainHookInbox(capture, inboxDir = FML_HOOK_INBOX_DIR, options = {}) {
|
|
5539
|
+
const maximumEntries = options.maximumEntries ?? MAX_HOOK_INBOX_DRAIN_ENTRIES;
|
|
5540
|
+
const yieldToEventLoop = options.yieldToEventLoop ?? defaultYieldToEventLoop;
|
|
4534
5541
|
const retirement = readRetirementState(inboxDir);
|
|
4535
5542
|
const completedRetirement = completePendingRetirement(inboxDir, retirement);
|
|
4536
5543
|
let failed = completedRetirement === null ? 1 : 0;
|
|
@@ -4548,7 +5555,7 @@ async function drainHookInbox(capture, inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4548
5555
|
try {
|
|
4549
5556
|
const scan = await scanDrainEntryNames(
|
|
4550
5557
|
inboxDir,
|
|
4551
|
-
|
|
5558
|
+
Math.max(1, Math.floor(maximumEntries)),
|
|
4552
5559
|
skipThrough,
|
|
4553
5560
|
pendingRetirementName
|
|
4554
5561
|
);
|
|
@@ -4582,8 +5589,14 @@ async function drainHookInbox(capture, inboxDir = FML_HOOK_INBOX_DIR) {
|
|
|
4582
5589
|
}
|
|
4583
5590
|
safeThrough = name;
|
|
4584
5591
|
};
|
|
5592
|
+
let processedSinceYield = 0;
|
|
4585
5593
|
for (const name of names) {
|
|
4586
|
-
|
|
5594
|
+
if (processedSinceYield >= DRAIN_YIELD_EVERY_ENTRIES) {
|
|
5595
|
+
processedSinceYield = 0;
|
|
5596
|
+
await yieldToEventLoop();
|
|
5597
|
+
}
|
|
5598
|
+
processedSinceYield += 1;
|
|
5599
|
+
const filePath = path5.join(inboxDir, name);
|
|
4587
5600
|
const readResult = readEntryResult(filePath);
|
|
4588
5601
|
if (readResult.kind === "missing") {
|
|
4589
5602
|
markResolved(name, false);
|
|
@@ -4659,8 +5672,22 @@ export {
|
|
|
4659
5672
|
openObserverDatabase,
|
|
4660
5673
|
OBSERVER_PROTOCOL_VERSION,
|
|
4661
5674
|
OBSERVER_SERVICE_NAME,
|
|
5675
|
+
OBSERVER_IDENTITY_HEADER,
|
|
4662
5676
|
isObserverHealth,
|
|
4663
5677
|
createObserverHttpServer,
|
|
5678
|
+
OBSERVER_HEARTBEAT_STALE_MS,
|
|
5679
|
+
isPidAlive,
|
|
5680
|
+
readObserverState,
|
|
5681
|
+
readObserverStateStatus,
|
|
5682
|
+
createInitialObserverState,
|
|
5683
|
+
ObserverStateMutationBusyError,
|
|
5684
|
+
inspectObserverLease,
|
|
5685
|
+
observerLeaseIsOwned,
|
|
5686
|
+
getProcessIdentity,
|
|
5687
|
+
claimObserverState,
|
|
5688
|
+
updateObserverState,
|
|
5689
|
+
removeObserverStateIfOwned,
|
|
5690
|
+
HookInboxCapacityError,
|
|
4664
5691
|
createHookInboxEntry,
|
|
4665
5692
|
persistHookInboxEntry,
|
|
4666
5693
|
hookInboxDeliveryPayload,
|
|
@@ -4668,6 +5695,8 @@ export {
|
|
|
4668
5695
|
acknowledgeHookInboxEntry,
|
|
4669
5696
|
retireHookInboxEntry,
|
|
4670
5697
|
readHookInboxStatus,
|
|
5698
|
+
readHookInboxCapacityCounters,
|
|
5699
|
+
recordHookInboxLoss,
|
|
4671
5700
|
readHookInboxStatusAsync,
|
|
4672
5701
|
drainHookInbox
|
|
4673
5702
|
};
|