@tinycloud/operations 0.5.0-beta.5 → 0.5.0-beta.6
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/dist/artifacts.cjs +232 -40
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +34 -34
- package/dist/artifacts.d.ts +34 -34
- package/dist/artifacts.js +233 -40
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +230 -40
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +231 -40
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +232 -40
- package/dist/delegation-binding.cjs.map +1 -1
- package/dist/delegation-binding.js +233 -40
- package/dist/delegation-binding.js.map +1 -1
- package/dist/index.cjs +230 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +231 -40
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +8 -4
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +9 -4
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +232 -40
- package/dist/state.cjs.map +1 -1
- package/dist/state.js +233 -40
- package/dist/state.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -4235,9 +4235,161 @@ var init_authority = __esm({
|
|
|
4235
4235
|
|
|
4236
4236
|
// src/state.ts
|
|
4237
4237
|
function processWideContext(key) {
|
|
4238
|
-
var
|
|
4238
|
+
var _a4;
|
|
4239
4239
|
const registry = globalThis;
|
|
4240
|
-
return registry[
|
|
4240
|
+
return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
|
|
4241
|
+
}
|
|
4242
|
+
function processOrigin() {
|
|
4243
|
+
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
4244
|
+
if (process.platform !== "linux") return cachedProcessOrigin = null;
|
|
4245
|
+
try {
|
|
4246
|
+
const bootId = (0, import_node_fs.readFileSync)("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
4247
|
+
const pidNamespace = (0, import_node_fs.readlinkSync)("/proc/self/ns/pid");
|
|
4248
|
+
const processStat = (0, import_node_fs.readFileSync)("/proc/self/stat", "utf8");
|
|
4249
|
+
const closingParen = processStat.lastIndexOf(") ");
|
|
4250
|
+
const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
|
|
4251
|
+
if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
|
|
4252
|
+
return cachedProcessOrigin = null;
|
|
4253
|
+
}
|
|
4254
|
+
return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
|
|
4255
|
+
} catch {
|
|
4256
|
+
return cachedProcessOrigin = null;
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
function processOwner(liveness) {
|
|
4260
|
+
return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
|
|
4261
|
+
}
|
|
4262
|
+
function socketDirectory(name) {
|
|
4263
|
+
const match = LIVENESS_SOCKET.exec(name);
|
|
4264
|
+
return match ? `${match[1]}-${match[2]}` : null;
|
|
4265
|
+
}
|
|
4266
|
+
async function pinDirectory(directory) {
|
|
4267
|
+
const handle = await (0, import_promises.open)(directory, "r");
|
|
4268
|
+
try {
|
|
4269
|
+
const { dev, ino } = await handle.stat({ bigint: true });
|
|
4270
|
+
return {
|
|
4271
|
+
identity: `${dev.toString(16)}-${ino.toString(16)}`,
|
|
4272
|
+
path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
|
|
4273
|
+
close: () => handle.close()
|
|
4274
|
+
};
|
|
4275
|
+
} catch (error) {
|
|
4276
|
+
await handle.close();
|
|
4277
|
+
throw error;
|
|
4278
|
+
}
|
|
4279
|
+
}
|
|
4280
|
+
async function bindLiveness(directory) {
|
|
4281
|
+
if (!processOrigin()) return null;
|
|
4282
|
+
let pinned;
|
|
4283
|
+
try {
|
|
4284
|
+
pinned = await pinDirectory(directory);
|
|
4285
|
+
} catch {
|
|
4286
|
+
return null;
|
|
4287
|
+
}
|
|
4288
|
+
const socket = `.live-${pinned.identity}-${(0, import_node_crypto.randomUUID)()}`;
|
|
4289
|
+
const server = (0, import_node_net.createServer)((connection) => connection.destroy());
|
|
4290
|
+
let bound = false;
|
|
4291
|
+
try {
|
|
4292
|
+
await new Promise((resolve2, reject) => {
|
|
4293
|
+
server.once("error", reject);
|
|
4294
|
+
server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
|
|
4295
|
+
server.off("error", reject);
|
|
4296
|
+
resolve2();
|
|
4297
|
+
});
|
|
4298
|
+
});
|
|
4299
|
+
bound = true;
|
|
4300
|
+
server.on("error", () => void 0);
|
|
4301
|
+
server.unref();
|
|
4302
|
+
await (0, import_promises.rename)(pinned.path(`${socket}.new`), pinned.path(socket));
|
|
4303
|
+
const stats = await (0, import_promises.lstat)(pinned.path(socket), { bigint: true });
|
|
4304
|
+
if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
|
|
4305
|
+
await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, (0, import_node_path.basename)(directory));
|
|
4306
|
+
return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: (0, import_node_path.join)(directory, socket), server };
|
|
4307
|
+
} catch {
|
|
4308
|
+
if (bound) {
|
|
4309
|
+
server.close();
|
|
4310
|
+
for (const name of [`${socket}.new`, socket]) await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
|
|
4311
|
+
}
|
|
4312
|
+
return null;
|
|
4313
|
+
} finally {
|
|
4314
|
+
await pinned.close();
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
async function ensureLiveness(acquisition) {
|
|
4318
|
+
if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
|
|
4319
|
+
return acquisition.held;
|
|
4320
|
+
}
|
|
4321
|
+
async function closeLiveness(held) {
|
|
4322
|
+
held.server.close();
|
|
4323
|
+
await (0, import_promises.unlink)(held.path).catch(() => void 0);
|
|
4324
|
+
}
|
|
4325
|
+
function parseLiveness(value) {
|
|
4326
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4327
|
+
const { socket, dev, ino } = value;
|
|
4328
|
+
const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
|
|
4329
|
+
return name !== null && name[3] === void 0 && typeof dev === "string" && /^[0-9]+$/.test(dev) && typeof ino === "string" && /^[0-9]+$/.test(ino) ? { socket: name[0], dev, ino } : null;
|
|
4330
|
+
}
|
|
4331
|
+
async function controlIn(pinned, acquisition) {
|
|
4332
|
+
const own = await ensureLiveness(acquisition);
|
|
4333
|
+
if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
|
|
4334
|
+
const stats = await (0, import_promises.lstat)(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
|
|
4335
|
+
return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
|
|
4336
|
+
}
|
|
4337
|
+
async function tryConnect(path) {
|
|
4338
|
+
return new Promise((resolve2) => {
|
|
4339
|
+
const probe = (0, import_node_net.connect)({ path });
|
|
4340
|
+
const settle = (outcome) => {
|
|
4341
|
+
clearTimeout(timer);
|
|
4342
|
+
probe.destroy();
|
|
4343
|
+
resolve2(outcome);
|
|
4344
|
+
};
|
|
4345
|
+
const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
|
|
4346
|
+
probe.once("connect", () => settle("connected"));
|
|
4347
|
+
probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
async function kernelRefuses(pinned, name, control) {
|
|
4351
|
+
if (process.versions.bun !== void 0) return false;
|
|
4352
|
+
return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
|
|
4353
|
+
}
|
|
4354
|
+
async function probeLiveness(liveness, acquisition) {
|
|
4355
|
+
let pinned;
|
|
4356
|
+
try {
|
|
4357
|
+
pinned = await pinDirectory(acquisition.directory);
|
|
4358
|
+
} catch {
|
|
4359
|
+
return "unknown";
|
|
4360
|
+
}
|
|
4361
|
+
try {
|
|
4362
|
+
if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
|
|
4363
|
+
const control = await controlIn(pinned, acquisition);
|
|
4364
|
+
if (!control) return "unknown";
|
|
4365
|
+
let stats;
|
|
4366
|
+
try {
|
|
4367
|
+
stats = await (0, import_promises.lstat)(pinned.path(liveness.socket), { bigint: true });
|
|
4368
|
+
} catch (error) {
|
|
4369
|
+
return isErrno(error, "ENOENT") ? "gone" : "unknown";
|
|
4370
|
+
}
|
|
4371
|
+
if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
|
|
4372
|
+
return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
|
|
4373
|
+
} finally {
|
|
4374
|
+
await pinned.close();
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
async function sweepLiveness(acquisition) {
|
|
4378
|
+
if (process.versions.bun !== void 0 || !acquisition.held) return;
|
|
4379
|
+
const pinned = await pinDirectory(acquisition.directory);
|
|
4380
|
+
try {
|
|
4381
|
+
const control = await controlIn(pinned, acquisition);
|
|
4382
|
+
if (!control) return;
|
|
4383
|
+
for (const name of await (0, import_promises.readdir)(pinned.path("."))) {
|
|
4384
|
+
if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
|
|
4385
|
+
const stats = await (0, import_promises.lstat)(pinned.path(name)).catch(() => null);
|
|
4386
|
+
if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
|
|
4387
|
+
await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
4390
|
+
} finally {
|
|
4391
|
+
await pinned.close();
|
|
4392
|
+
}
|
|
4241
4393
|
}
|
|
4242
4394
|
function tinycloudHomePath() {
|
|
4243
4395
|
const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? (0, import_node_os.homedir)();
|
|
@@ -4414,7 +4566,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
4414
4566
|
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
4415
4567
|
});
|
|
4416
4568
|
}
|
|
4417
|
-
const
|
|
4569
|
+
const liveness = { directory: turnDirectory, held: void 0 };
|
|
4570
|
+
const closeAcquisitionLiveness = async () => {
|
|
4571
|
+
if (liveness.held) await closeLiveness(liveness.held);
|
|
4572
|
+
};
|
|
4573
|
+
let taken;
|
|
4574
|
+
try {
|
|
4575
|
+
taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
|
|
4576
|
+
} catch (error) {
|
|
4577
|
+
await closeAcquisitionLiveness();
|
|
4578
|
+
throw error;
|
|
4579
|
+
}
|
|
4580
|
+
const { turn, firstWaitedTurn } = taken;
|
|
4418
4581
|
try {
|
|
4419
4582
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) {
|
|
4420
4583
|
await (0, import_promises.rmdir)(directory).catch(() => void 0);
|
|
@@ -4424,7 +4587,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
4424
4587
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
4425
4588
|
while (true) {
|
|
4426
4589
|
const token = (0, import_node_crypto.randomUUID)();
|
|
4427
|
-
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
|
|
4590
|
+
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
4428
4591
|
return {
|
|
4429
4592
|
deletedWhileWaiting,
|
|
4430
4593
|
turnSlot: turn.slot,
|
|
@@ -4432,17 +4595,19 @@ async function acquireProfileLock(profile, options) {
|
|
|
4432
4595
|
try {
|
|
4433
4596
|
await releaseProfileLock(profile, lockPath, token);
|
|
4434
4597
|
} finally {
|
|
4435
|
-
await releaseTurn(profile, turn);
|
|
4598
|
+
await releaseTurn(profile, turn, liveness);
|
|
4599
|
+
await closeAcquisitionLiveness();
|
|
4436
4600
|
}
|
|
4437
4601
|
}
|
|
4438
4602
|
};
|
|
4439
4603
|
}
|
|
4440
4604
|
await signalTestLockContention(profile);
|
|
4441
|
-
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
4605
|
+
if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
|
|
4442
4606
|
await waitOrTimeOut(profile, deadline);
|
|
4443
4607
|
}
|
|
4444
4608
|
} catch (error) {
|
|
4445
|
-
await releaseTurn(profile, turn);
|
|
4609
|
+
await releaseTurn(profile, turn, liveness);
|
|
4610
|
+
await closeAcquisitionLiveness();
|
|
4446
4611
|
throw error;
|
|
4447
4612
|
}
|
|
4448
4613
|
}
|
|
@@ -4455,7 +4620,7 @@ async function waitOrTimeOut(profile, deadline, detail) {
|
|
|
4455
4620
|
checkDeadline(profile, deadline, detail);
|
|
4456
4621
|
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
4457
4622
|
}
|
|
4458
|
-
async function acquireTurn(profile, directory, deadline) {
|
|
4623
|
+
async function acquireTurn(profile, directory, deadline, liveness) {
|
|
4459
4624
|
let firstWaitedTurn;
|
|
4460
4625
|
let problem;
|
|
4461
4626
|
const damaged = (what) => `The turn lock ${directory} ${what}, so no process can take it. If no tc or MCP process is using profile "${profile}", remove that directory.`;
|
|
@@ -4480,13 +4645,13 @@ async function acquireTurn(profile, directory, deadline) {
|
|
|
4480
4645
|
}
|
|
4481
4646
|
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
4482
4647
|
if (latest.held && latest.done) {
|
|
4483
|
-
const turn = await takeTurn(profile, directory, latest);
|
|
4648
|
+
const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
|
|
4484
4649
|
if (turn !== null) return { turn, firstWaitedTurn };
|
|
4485
4650
|
await waitOrTimeOut(profile, deadline);
|
|
4486
4651
|
} else if (latest.done) {
|
|
4487
4652
|
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
4488
4653
|
await waitOrTimeOut(profile, deadline, problem);
|
|
4489
|
-
} else if (latest.pid === process.pid
|
|
4654
|
+
} else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
|
|
4490
4655
|
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
4491
4656
|
await settleAbandonedTurn(directory, latest);
|
|
4492
4657
|
relinquishedTurns.delete(latest.token);
|
|
@@ -4531,6 +4696,7 @@ async function readTurn(directory, slot) {
|
|
|
4531
4696
|
const done = await exists((0, import_node_path.join)(path, `${owner.token}.done`));
|
|
4532
4697
|
const held = await exists((0, import_node_path.join)(path, `${owner.token}.held`));
|
|
4533
4698
|
return {
|
|
4699
|
+
...owner,
|
|
4534
4700
|
slot,
|
|
4535
4701
|
token: owner.token,
|
|
4536
4702
|
pid: owner.pid,
|
|
@@ -4544,11 +4710,11 @@ async function isFinishedTurn(directory, slot, token) {
|
|
|
4544
4710
|
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
4545
4711
|
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
4546
4712
|
}
|
|
4547
|
-
async function takeTurn(profile, directory, latest) {
|
|
4713
|
+
async function takeTurn(profile, directory, latest, liveness) {
|
|
4548
4714
|
const token = (0, import_node_crypto.randomUUID)();
|
|
4549
4715
|
const slot = latest.slot + 1;
|
|
4550
4716
|
const path = (0, import_node_path.join)(directory, String(slot));
|
|
4551
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
4717
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
|
|
4552
4718
|
let published = false;
|
|
4553
4719
|
try {
|
|
4554
4720
|
published = await publishDirectory(
|
|
@@ -4602,7 +4768,7 @@ async function markTurn(path, token, kind) {
|
|
|
4602
4768
|
}
|
|
4603
4769
|
async function createTurnDirectory(directory) {
|
|
4604
4770
|
const token = (0, import_node_crypto.randomUUID)();
|
|
4605
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
4771
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
|
|
4606
4772
|
await publishDirectory((0, import_node_path.join)((0, import_node_path.dirname)(directory), `.stage-${(0, import_node_path.basename)(directory)}-${process.pid}-${token}`), directory, {
|
|
4607
4773
|
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
4608
4774
|
`,
|
|
@@ -4626,31 +4792,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
|
|
|
4626
4792
|
await (0, import_promises.rm)(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
4627
4793
|
}
|
|
4628
4794
|
}
|
|
4629
|
-
async function releaseTurn(profile, turn) {
|
|
4795
|
+
async function releaseTurn(profile, turn, liveness) {
|
|
4630
4796
|
try {
|
|
4631
4797
|
await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
|
|
4632
4798
|
} catch {
|
|
4633
4799
|
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
4634
4800
|
return;
|
|
4635
4801
|
}
|
|
4636
|
-
await collectTurns(profile, turn).catch(() => void 0);
|
|
4802
|
+
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
4637
4803
|
}
|
|
4638
|
-
function isAbandonedTurnEntry(name) {
|
|
4804
|
+
async function isAbandonedTurnEntry(directory, name, liveness) {
|
|
4639
4805
|
if (name.startsWith(".trash-")) return true;
|
|
4640
4806
|
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
4641
|
-
|
|
4807
|
+
if (writer === void 0) return false;
|
|
4808
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4809
|
+
return name.startsWith(".stage-") && await isStagedOwnerGone((0, import_node_path.join)(directory, name, "owner.json"), liveness);
|
|
4642
4810
|
}
|
|
4643
|
-
function isAbandonedTurnDirectoryStage(name) {
|
|
4811
|
+
async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
|
|
4644
4812
|
const writer = /^\.stage-.+-([0-9]+)-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.exec(name)?.[1];
|
|
4645
|
-
|
|
4813
|
+
if (writer === void 0) return false;
|
|
4814
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4815
|
+
return isStagedOwnerGone((0, import_node_path.join)(directory, name, "0", "owner.json"), liveness);
|
|
4646
4816
|
}
|
|
4647
|
-
async function
|
|
4817
|
+
async function isStagedOwnerGone(recordPath, liveness) {
|
|
4818
|
+
const owner = await readJson(recordPath).catch(() => null);
|
|
4819
|
+
return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
|
|
4820
|
+
}
|
|
4821
|
+
async function collectTurns(profile, turn, liveness) {
|
|
4648
4822
|
for (const [directory, abandoned] of [
|
|
4649
4823
|
[turn.directory, isAbandonedTurnEntry],
|
|
4650
4824
|
[(0, import_node_path.dirname)(turn.directory), isAbandonedTurnDirectoryStage]
|
|
4651
4825
|
]) {
|
|
4652
|
-
for (const name of
|
|
4653
|
-
await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4826
|
+
for (const name of await (0, import_promises.readdir)(directory)) {
|
|
4827
|
+
if (await abandoned(directory, name, liveness)) await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4654
4828
|
}
|
|
4655
4829
|
}
|
|
4656
4830
|
const older = (await (0, import_promises.readdir)(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
|
|
@@ -4668,8 +4842,9 @@ async function collectTurns(profile, turn) {
|
|
|
4668
4842
|
await (0, import_promises.rm)(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
4669
4843
|
}
|
|
4670
4844
|
}
|
|
4845
|
+
await sweepLiveness(liveness);
|
|
4671
4846
|
}
|
|
4672
|
-
async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
4847
|
+
async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
|
|
4673
4848
|
try {
|
|
4674
4849
|
await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
4675
4850
|
} catch (error) {
|
|
@@ -4682,7 +4857,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
|
4682
4857
|
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
4683
4858
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
4684
4859
|
const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
|
|
4685
|
-
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
4860
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
|
|
4686
4861
|
`;
|
|
4687
4862
|
try {
|
|
4688
4863
|
await (0, import_promises.writeFile)(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
@@ -4726,7 +4901,7 @@ async function signalTestLockContention(profile) {
|
|
|
4726
4901
|
await (0, import_promises.writeFile)(signalPath, `${profile}
|
|
4727
4902
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
4728
4903
|
}
|
|
4729
|
-
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
4904
|
+
async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4730
4905
|
let aged;
|
|
4731
4906
|
try {
|
|
4732
4907
|
const { mtimeMs } = await (0, import_promises.stat)(lockPath);
|
|
@@ -4743,7 +4918,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4743
4918
|
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
4744
4919
|
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
4745
4920
|
const claimPath = (0, import_node_path.join)(lockPath, name);
|
|
4746
|
-
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
4921
|
+
if (await hasLiveRecoveryOwner(claimPath, liveness)) {
|
|
4747
4922
|
if (!await exists((0, import_node_path.join)(lockPath, "owner.json"))) await (0, import_promises.rename)(claimPath, (0, import_node_path.join)(lockPath, "owner.json"));
|
|
4748
4923
|
return false;
|
|
4749
4924
|
}
|
|
@@ -4759,7 +4934,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4759
4934
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4760
4935
|
return true;
|
|
4761
4936
|
}
|
|
4762
|
-
async function hasLiveRecoveryOwner(claimPath) {
|
|
4937
|
+
async function hasLiveRecoveryOwner(claimPath, liveness) {
|
|
4763
4938
|
let contents;
|
|
4764
4939
|
try {
|
|
4765
4940
|
contents = await (0, import_promises.readFile)(claimPath, "utf8");
|
|
@@ -4773,8 +4948,8 @@ async function hasLiveRecoveryOwner(claimPath) {
|
|
|
4773
4948
|
} catch {
|
|
4774
4949
|
return false;
|
|
4775
4950
|
}
|
|
4776
|
-
const
|
|
4777
|
-
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 &&
|
|
4951
|
+
const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
|
|
4952
|
+
return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
|
|
4778
4953
|
}
|
|
4779
4954
|
function recoveryFenced(since, staleAfterMs) {
|
|
4780
4955
|
return performance.now() - since >= staleAfterMs / 2;
|
|
@@ -4788,11 +4963,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
|
4788
4963
|
if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
|
|
4789
4964
|
}
|
|
4790
4965
|
}
|
|
4791
|
-
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
4966
|
+
async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4792
4967
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
4793
4968
|
const observed = await (0, import_promises.readFile)(ownerPath, "utf8").catch(() => null);
|
|
4794
|
-
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
4795
|
-
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
4969
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
|
|
4970
|
+
if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
|
|
4796
4971
|
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
4797
4972
|
const claimPath = (0, import_node_path.join)(lockPath, `.recover-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
4798
4973
|
let moved = false;
|
|
@@ -4828,7 +5003,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
|
4828
5003
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4829
5004
|
return true;
|
|
4830
5005
|
}
|
|
4831
|
-
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
5006
|
+
async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
|
|
4832
5007
|
let owner;
|
|
4833
5008
|
try {
|
|
4834
5009
|
owner = JSON.parse(observed);
|
|
@@ -4836,7 +5011,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
|
4836
5011
|
owner = null;
|
|
4837
5012
|
}
|
|
4838
5013
|
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
4839
|
-
return isStaleOwner(owner, staleAfterMs);
|
|
5014
|
+
return isStaleOwner(owner, liveness, staleAfterMs);
|
|
4840
5015
|
}
|
|
4841
5016
|
const modifiedAt = await (0, import_promises.stat)(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
4842
5017
|
return Date.now() - modifiedAt >= staleAfterMs;
|
|
@@ -4858,14 +5033,14 @@ async function waitForTestBarrier(environmentName, profile) {
|
|
|
4858
5033
|
}
|
|
4859
5034
|
}
|
|
4860
5035
|
}
|
|
4861
|
-
function isStaleOwner(owner, staleAfterMs) {
|
|
5036
|
+
async function isStaleOwner(owner, liveness, staleAfterMs) {
|
|
4862
5037
|
const now = Date.now();
|
|
4863
5038
|
const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
|
|
4864
5039
|
if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
|
|
4865
5040
|
if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
4866
5041
|
return false;
|
|
4867
5042
|
}
|
|
4868
|
-
return !
|
|
5043
|
+
return !await isRecordedProcessAlive(owner, liveness);
|
|
4869
5044
|
}
|
|
4870
5045
|
async function exists(path) {
|
|
4871
5046
|
try {
|
|
@@ -4876,13 +5051,22 @@ async function exists(path) {
|
|
|
4876
5051
|
throw error;
|
|
4877
5052
|
}
|
|
4878
5053
|
}
|
|
4879
|
-
function
|
|
5054
|
+
async function isRecordedProcessAlive(owner, acquisition) {
|
|
5055
|
+
const pid = owner.pid;
|
|
5056
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
|
|
4880
5057
|
try {
|
|
4881
5058
|
process.kill(pid, 0);
|
|
4882
|
-
return true;
|
|
4883
5059
|
} catch (error) {
|
|
4884
5060
|
return !isErrno(error, "ESRCH");
|
|
4885
5061
|
}
|
|
5062
|
+
if (pid !== process.pid || typeof owner.instanceId !== "string" || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/.test(owner.instanceId) || owner.instanceId === processInstanceId) return true;
|
|
5063
|
+
const origin = processOrigin();
|
|
5064
|
+
if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
|
|
5065
|
+
if (owner.pidNamespace === origin.pidNamespace) {
|
|
5066
|
+
return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
|
|
5067
|
+
}
|
|
5068
|
+
const liveness = parseLiveness(owner.liveness);
|
|
5069
|
+
return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
|
|
4886
5070
|
}
|
|
4887
5071
|
function validateProfileName(profile) {
|
|
4888
5072
|
if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
|
|
@@ -4896,13 +5080,15 @@ function isErrno(error, code) {
|
|
|
4896
5080
|
function sleep(milliseconds) {
|
|
4897
5081
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
4898
5082
|
}
|
|
4899
|
-
var import_node_crypto, import_node_async_hooks, import_promises, import_node_os, import_node_path, DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, invocationStateRoot, heldProfileLocks, ProfileLockTimeoutError, ProfileDeletedError, TURN,
|
|
5083
|
+
var import_node_crypto, import_node_async_hooks, import_node_fs, import_promises, import_node_net, import_node_os, import_node_path, DEFAULT_PROFILE, DEFAULT_LOCK_TIMEOUT_MS, DEFAULT_LOCK_RETRY_MS, DEFAULT_STALE_LOCK_MS, TEST_LOCK_CONTENTION_SIGNAL_PATH, PRIVATE_FILE_MODE, PRIVATE_DIR_MODE, TEST_LOCK_RECOVERY_BARRIER_DIR, TEST_LOCK_OWNERLESS_BARRIER_DIR, TEST_LOCK_PUBLISH_BARRIER_DIR, TEST_LOCK_RELEASE_BARRIER_DIR, TEST_LOCK_CLAIM_BARRIER_DIR, TEST_LOCK_CLAIMED_BARRIER_DIR, TEST_LOCK_VERIFIED_BARRIER_DIR, TEST_LOCK_MOVED_BARRIER_DIR, TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, TEST_LOCK_TURN_SETTLE_BARRIER_DIR, TEST_LOCK_TURN_COLLECT_BARRIER_DIR, TEST_LOCK_LIVENESS_BARRIER_DIR, invocationStateRoot, heldProfileLocks, _a2, processInstanceId, cachedProcessOrigin, LIVENESS_SOCKET, LIVENESS_PROBE_TIMEOUT_MS, ProfileLockTimeoutError, ProfileDeletedError, TURN, _a3, relinquishedTurns, MARKER_RETRY_DELAYS_MS, SETTLE_RETRY_MS, SETTLE_RETRIES, NO_HARD_LINK_ERRORS, ABANDONED_CLAIM, RECOVERY_CLAIM, STAGED_OWNER;
|
|
4900
5084
|
var init_state = __esm({
|
|
4901
5085
|
"src/state.ts"() {
|
|
4902
5086
|
"use strict";
|
|
4903
5087
|
import_node_crypto = require("crypto");
|
|
4904
5088
|
import_node_async_hooks = require("async_hooks");
|
|
5089
|
+
import_node_fs = require("fs");
|
|
4905
5090
|
import_promises = require("fs/promises");
|
|
5091
|
+
import_node_net = require("net");
|
|
4906
5092
|
import_node_os = require("os");
|
|
4907
5093
|
import_node_path = require("path");
|
|
4908
5094
|
DEFAULT_PROFILE = "default";
|
|
@@ -4923,8 +5109,12 @@ var init_state = __esm({
|
|
|
4923
5109
|
TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
4924
5110
|
TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
4925
5111
|
TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
5112
|
+
TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
4926
5113
|
invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
4927
5114
|
heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
5115
|
+
processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
|
|
5116
|
+
LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
|
|
5117
|
+
LIVENESS_PROBE_TIMEOUT_MS = 1e3;
|
|
4928
5118
|
ProfileLockTimeoutError = class extends Error {
|
|
4929
5119
|
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
4930
5120
|
constructor(profile, timeoutMs, detail) {
|
|
@@ -4941,7 +5131,7 @@ var init_state = __esm({
|
|
|
4941
5131
|
}
|
|
4942
5132
|
};
|
|
4943
5133
|
TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
4944
|
-
relinquishedTurns = globalThis[
|
|
5134
|
+
relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
|
|
4945
5135
|
MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
4946
5136
|
SETTLE_RETRY_MS = 1e3;
|
|
4947
5137
|
SETTLE_RETRIES = 300;
|