@tinycloud/operations 0.5.0-beta.5 → 0.5.0-beta.7
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 +235 -43
- 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 +236 -43
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +233 -43
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +234 -43
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +235 -43
- package/dist/delegation-binding.cjs.map +1 -1
- package/dist/delegation-binding.js +236 -43
- package/dist/delegation-binding.js.map +1 -1
- package/dist/index.cjs +233 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +234 -43
- 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 +2 -2
package/dist/cli-runtime.js
CHANGED
|
@@ -4107,10 +4107,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace2) {
|
|
|
4107
4107
|
const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace2);
|
|
4108
4108
|
const missing = [];
|
|
4109
4109
|
for (const requested of canonicalRequired) {
|
|
4110
|
-
const
|
|
4111
|
-
(candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
|
|
4110
|
+
const sameScope = canonicalGranted.filter(
|
|
4111
|
+
(candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
|
|
4112
4112
|
);
|
|
4113
|
-
if (!
|
|
4113
|
+
if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
|
|
4114
4114
|
}
|
|
4115
4115
|
return {
|
|
4116
4116
|
satisfied: missing.length === 0,
|
|
@@ -4234,9 +4234,11 @@ var init_authority = __esm({
|
|
|
4234
4234
|
// src/state.ts
|
|
4235
4235
|
import { randomUUID } from "crypto";
|
|
4236
4236
|
import { AsyncLocalStorage } from "async_hooks";
|
|
4237
|
+
import { readFileSync, readlinkSync } from "fs";
|
|
4237
4238
|
import {
|
|
4238
4239
|
chmod,
|
|
4239
4240
|
link,
|
|
4241
|
+
lstat,
|
|
4240
4242
|
mkdir,
|
|
4241
4243
|
open,
|
|
4242
4244
|
readFile,
|
|
@@ -4248,12 +4250,165 @@ import {
|
|
|
4248
4250
|
unlink,
|
|
4249
4251
|
writeFile
|
|
4250
4252
|
} from "fs/promises";
|
|
4253
|
+
import { connect, createServer } from "net";
|
|
4251
4254
|
import { homedir } from "os";
|
|
4252
4255
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
4253
4256
|
function processWideContext(key) {
|
|
4254
|
-
var
|
|
4257
|
+
var _a4;
|
|
4255
4258
|
const registry = globalThis;
|
|
4256
|
-
return registry[
|
|
4259
|
+
return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
|
|
4260
|
+
}
|
|
4261
|
+
function processOrigin() {
|
|
4262
|
+
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
4263
|
+
if (process.platform !== "linux") return cachedProcessOrigin = null;
|
|
4264
|
+
try {
|
|
4265
|
+
const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
4266
|
+
const pidNamespace = readlinkSync("/proc/self/ns/pid");
|
|
4267
|
+
const processStat = readFileSync("/proc/self/stat", "utf8");
|
|
4268
|
+
const closingParen = processStat.lastIndexOf(") ");
|
|
4269
|
+
const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
|
|
4270
|
+
if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
|
|
4271
|
+
return cachedProcessOrigin = null;
|
|
4272
|
+
}
|
|
4273
|
+
return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
|
|
4274
|
+
} catch {
|
|
4275
|
+
return cachedProcessOrigin = null;
|
|
4276
|
+
}
|
|
4277
|
+
}
|
|
4278
|
+
function processOwner(liveness) {
|
|
4279
|
+
return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
|
|
4280
|
+
}
|
|
4281
|
+
function socketDirectory(name) {
|
|
4282
|
+
const match = LIVENESS_SOCKET.exec(name);
|
|
4283
|
+
return match ? `${match[1]}-${match[2]}` : null;
|
|
4284
|
+
}
|
|
4285
|
+
async function pinDirectory(directory) {
|
|
4286
|
+
const handle = await open(directory, "r");
|
|
4287
|
+
try {
|
|
4288
|
+
const { dev, ino } = await handle.stat({ bigint: true });
|
|
4289
|
+
return {
|
|
4290
|
+
identity: `${dev.toString(16)}-${ino.toString(16)}`,
|
|
4291
|
+
path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
|
|
4292
|
+
close: () => handle.close()
|
|
4293
|
+
};
|
|
4294
|
+
} catch (error) {
|
|
4295
|
+
await handle.close();
|
|
4296
|
+
throw error;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
async function bindLiveness(directory) {
|
|
4300
|
+
if (!processOrigin()) return null;
|
|
4301
|
+
let pinned;
|
|
4302
|
+
try {
|
|
4303
|
+
pinned = await pinDirectory(directory);
|
|
4304
|
+
} catch {
|
|
4305
|
+
return null;
|
|
4306
|
+
}
|
|
4307
|
+
const socket = `.live-${pinned.identity}-${randomUUID()}`;
|
|
4308
|
+
const server = createServer((connection) => connection.destroy());
|
|
4309
|
+
let bound = false;
|
|
4310
|
+
try {
|
|
4311
|
+
await new Promise((resolve2, reject) => {
|
|
4312
|
+
server.once("error", reject);
|
|
4313
|
+
server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
|
|
4314
|
+
server.off("error", reject);
|
|
4315
|
+
resolve2();
|
|
4316
|
+
});
|
|
4317
|
+
});
|
|
4318
|
+
bound = true;
|
|
4319
|
+
server.on("error", () => void 0);
|
|
4320
|
+
server.unref();
|
|
4321
|
+
await rename(pinned.path(`${socket}.new`), pinned.path(socket));
|
|
4322
|
+
const stats = await lstat(pinned.path(socket), { bigint: true });
|
|
4323
|
+
if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
|
|
4324
|
+
await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, basename(directory));
|
|
4325
|
+
return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: join(directory, socket), server };
|
|
4326
|
+
} catch {
|
|
4327
|
+
if (bound) {
|
|
4328
|
+
server.close();
|
|
4329
|
+
for (const name of [`${socket}.new`, socket]) await unlink(pinned.path(name)).catch(() => void 0);
|
|
4330
|
+
}
|
|
4331
|
+
return null;
|
|
4332
|
+
} finally {
|
|
4333
|
+
await pinned.close();
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
async function ensureLiveness(acquisition) {
|
|
4337
|
+
if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
|
|
4338
|
+
return acquisition.held;
|
|
4339
|
+
}
|
|
4340
|
+
async function closeLiveness(held) {
|
|
4341
|
+
held.server.close();
|
|
4342
|
+
await unlink(held.path).catch(() => void 0);
|
|
4343
|
+
}
|
|
4344
|
+
function parseLiveness(value) {
|
|
4345
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4346
|
+
const { socket, dev, ino } = value;
|
|
4347
|
+
const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
|
|
4348
|
+
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;
|
|
4349
|
+
}
|
|
4350
|
+
async function controlIn(pinned, acquisition) {
|
|
4351
|
+
const own = await ensureLiveness(acquisition);
|
|
4352
|
+
if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
|
|
4353
|
+
const stats = await lstat(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
|
|
4354
|
+
return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
|
|
4355
|
+
}
|
|
4356
|
+
async function tryConnect(path) {
|
|
4357
|
+
return new Promise((resolve2) => {
|
|
4358
|
+
const probe = connect({ path });
|
|
4359
|
+
const settle = (outcome) => {
|
|
4360
|
+
clearTimeout(timer);
|
|
4361
|
+
probe.destroy();
|
|
4362
|
+
resolve2(outcome);
|
|
4363
|
+
};
|
|
4364
|
+
const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
|
|
4365
|
+
probe.once("connect", () => settle("connected"));
|
|
4366
|
+
probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
async function kernelRefuses(pinned, name, control) {
|
|
4370
|
+
if (process.versions.bun !== void 0) return false;
|
|
4371
|
+
return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
|
|
4372
|
+
}
|
|
4373
|
+
async function probeLiveness(liveness, acquisition) {
|
|
4374
|
+
let pinned;
|
|
4375
|
+
try {
|
|
4376
|
+
pinned = await pinDirectory(acquisition.directory);
|
|
4377
|
+
} catch {
|
|
4378
|
+
return "unknown";
|
|
4379
|
+
}
|
|
4380
|
+
try {
|
|
4381
|
+
if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
|
|
4382
|
+
const control = await controlIn(pinned, acquisition);
|
|
4383
|
+
if (!control) return "unknown";
|
|
4384
|
+
let stats;
|
|
4385
|
+
try {
|
|
4386
|
+
stats = await lstat(pinned.path(liveness.socket), { bigint: true });
|
|
4387
|
+
} catch (error) {
|
|
4388
|
+
return isErrno(error, "ENOENT") ? "gone" : "unknown";
|
|
4389
|
+
}
|
|
4390
|
+
if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
|
|
4391
|
+
return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
|
|
4392
|
+
} finally {
|
|
4393
|
+
await pinned.close();
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
async function sweepLiveness(acquisition) {
|
|
4397
|
+
if (process.versions.bun !== void 0 || !acquisition.held) return;
|
|
4398
|
+
const pinned = await pinDirectory(acquisition.directory);
|
|
4399
|
+
try {
|
|
4400
|
+
const control = await controlIn(pinned, acquisition);
|
|
4401
|
+
if (!control) return;
|
|
4402
|
+
for (const name of await readdir(pinned.path("."))) {
|
|
4403
|
+
if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
|
|
4404
|
+
const stats = await lstat(pinned.path(name)).catch(() => null);
|
|
4405
|
+
if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
|
|
4406
|
+
await unlink(pinned.path(name)).catch(() => void 0);
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
} finally {
|
|
4410
|
+
await pinned.close();
|
|
4411
|
+
}
|
|
4257
4412
|
}
|
|
4258
4413
|
function tinycloudHomePath() {
|
|
4259
4414
|
const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
@@ -4430,7 +4585,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
4430
4585
|
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
4431
4586
|
});
|
|
4432
4587
|
}
|
|
4433
|
-
const
|
|
4588
|
+
const liveness = { directory: turnDirectory, held: void 0 };
|
|
4589
|
+
const closeAcquisitionLiveness = async () => {
|
|
4590
|
+
if (liveness.held) await closeLiveness(liveness.held);
|
|
4591
|
+
};
|
|
4592
|
+
let taken;
|
|
4593
|
+
try {
|
|
4594
|
+
taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
|
|
4595
|
+
} catch (error) {
|
|
4596
|
+
await closeAcquisitionLiveness();
|
|
4597
|
+
throw error;
|
|
4598
|
+
}
|
|
4599
|
+
const { turn, firstWaitedTurn } = taken;
|
|
4434
4600
|
try {
|
|
4435
4601
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) {
|
|
4436
4602
|
await rmdir(directory).catch(() => void 0);
|
|
@@ -4440,7 +4606,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
4440
4606
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
4441
4607
|
while (true) {
|
|
4442
4608
|
const token = randomUUID();
|
|
4443
|
-
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
|
|
4609
|
+
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
4444
4610
|
return {
|
|
4445
4611
|
deletedWhileWaiting,
|
|
4446
4612
|
turnSlot: turn.slot,
|
|
@@ -4448,17 +4614,19 @@ async function acquireProfileLock(profile, options) {
|
|
|
4448
4614
|
try {
|
|
4449
4615
|
await releaseProfileLock(profile, lockPath, token);
|
|
4450
4616
|
} finally {
|
|
4451
|
-
await releaseTurn(profile, turn);
|
|
4617
|
+
await releaseTurn(profile, turn, liveness);
|
|
4618
|
+
await closeAcquisitionLiveness();
|
|
4452
4619
|
}
|
|
4453
4620
|
}
|
|
4454
4621
|
};
|
|
4455
4622
|
}
|
|
4456
4623
|
await signalTestLockContention(profile);
|
|
4457
|
-
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
4624
|
+
if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
|
|
4458
4625
|
await waitOrTimeOut(profile, deadline);
|
|
4459
4626
|
}
|
|
4460
4627
|
} catch (error) {
|
|
4461
|
-
await releaseTurn(profile, turn);
|
|
4628
|
+
await releaseTurn(profile, turn, liveness);
|
|
4629
|
+
await closeAcquisitionLiveness();
|
|
4462
4630
|
throw error;
|
|
4463
4631
|
}
|
|
4464
4632
|
}
|
|
@@ -4471,7 +4639,7 @@ async function waitOrTimeOut(profile, deadline, detail) {
|
|
|
4471
4639
|
checkDeadline(profile, deadline, detail);
|
|
4472
4640
|
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
4473
4641
|
}
|
|
4474
|
-
async function acquireTurn(profile, directory, deadline) {
|
|
4642
|
+
async function acquireTurn(profile, directory, deadline, liveness) {
|
|
4475
4643
|
let firstWaitedTurn;
|
|
4476
4644
|
let problem;
|
|
4477
4645
|
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.`;
|
|
@@ -4496,13 +4664,13 @@ async function acquireTurn(profile, directory, deadline) {
|
|
|
4496
4664
|
}
|
|
4497
4665
|
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
4498
4666
|
if (latest.held && latest.done) {
|
|
4499
|
-
const turn = await takeTurn(profile, directory, latest);
|
|
4667
|
+
const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
|
|
4500
4668
|
if (turn !== null) return { turn, firstWaitedTurn };
|
|
4501
4669
|
await waitOrTimeOut(profile, deadline);
|
|
4502
4670
|
} else if (latest.done) {
|
|
4503
4671
|
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
4504
4672
|
await waitOrTimeOut(profile, deadline, problem);
|
|
4505
|
-
} else if (latest.pid === process.pid
|
|
4673
|
+
} else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
|
|
4506
4674
|
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
4507
4675
|
await settleAbandonedTurn(directory, latest);
|
|
4508
4676
|
relinquishedTurns.delete(latest.token);
|
|
@@ -4547,6 +4715,7 @@ async function readTurn(directory, slot) {
|
|
|
4547
4715
|
const done = await exists(join(path, `${owner.token}.done`));
|
|
4548
4716
|
const held = await exists(join(path, `${owner.token}.held`));
|
|
4549
4717
|
return {
|
|
4718
|
+
...owner,
|
|
4550
4719
|
slot,
|
|
4551
4720
|
token: owner.token,
|
|
4552
4721
|
pid: owner.pid,
|
|
@@ -4560,11 +4729,11 @@ async function isFinishedTurn(directory, slot, token) {
|
|
|
4560
4729
|
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
4561
4730
|
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
4562
4731
|
}
|
|
4563
|
-
async function takeTurn(profile, directory, latest) {
|
|
4732
|
+
async function takeTurn(profile, directory, latest, liveness) {
|
|
4564
4733
|
const token = randomUUID();
|
|
4565
4734
|
const slot = latest.slot + 1;
|
|
4566
4735
|
const path = join(directory, String(slot));
|
|
4567
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
4736
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
|
|
4568
4737
|
let published = false;
|
|
4569
4738
|
try {
|
|
4570
4739
|
published = await publishDirectory(
|
|
@@ -4618,7 +4787,7 @@ async function markTurn(path, token, kind) {
|
|
|
4618
4787
|
}
|
|
4619
4788
|
async function createTurnDirectory(directory) {
|
|
4620
4789
|
const token = randomUUID();
|
|
4621
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
4790
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
|
|
4622
4791
|
await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
|
|
4623
4792
|
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
4624
4793
|
`,
|
|
@@ -4642,31 +4811,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
|
|
|
4642
4811
|
await rm(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
4643
4812
|
}
|
|
4644
4813
|
}
|
|
4645
|
-
async function releaseTurn(profile, turn) {
|
|
4814
|
+
async function releaseTurn(profile, turn, liveness) {
|
|
4646
4815
|
try {
|
|
4647
4816
|
await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
|
|
4648
4817
|
} catch {
|
|
4649
4818
|
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
4650
4819
|
return;
|
|
4651
4820
|
}
|
|
4652
|
-
await collectTurns(profile, turn).catch(() => void 0);
|
|
4821
|
+
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
4653
4822
|
}
|
|
4654
|
-
function isAbandonedTurnEntry(name) {
|
|
4823
|
+
async function isAbandonedTurnEntry(directory, name, liveness) {
|
|
4655
4824
|
if (name.startsWith(".trash-")) return true;
|
|
4656
4825
|
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
4657
|
-
|
|
4826
|
+
if (writer === void 0) return false;
|
|
4827
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4828
|
+
return name.startsWith(".stage-") && await isStagedOwnerGone(join(directory, name, "owner.json"), liveness);
|
|
4658
4829
|
}
|
|
4659
|
-
function isAbandonedTurnDirectoryStage(name) {
|
|
4830
|
+
async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
|
|
4660
4831
|
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];
|
|
4661
|
-
|
|
4832
|
+
if (writer === void 0) return false;
|
|
4833
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4834
|
+
return isStagedOwnerGone(join(directory, name, "0", "owner.json"), liveness);
|
|
4662
4835
|
}
|
|
4663
|
-
async function
|
|
4836
|
+
async function isStagedOwnerGone(recordPath, liveness) {
|
|
4837
|
+
const owner = await readJson(recordPath).catch(() => null);
|
|
4838
|
+
return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
|
|
4839
|
+
}
|
|
4840
|
+
async function collectTurns(profile, turn, liveness) {
|
|
4664
4841
|
for (const [directory, abandoned] of [
|
|
4665
4842
|
[turn.directory, isAbandonedTurnEntry],
|
|
4666
4843
|
[dirname(turn.directory), isAbandonedTurnDirectoryStage]
|
|
4667
4844
|
]) {
|
|
4668
|
-
for (const name of
|
|
4669
|
-
await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4845
|
+
for (const name of await readdir(directory)) {
|
|
4846
|
+
if (await abandoned(directory, name, liveness)) await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4670
4847
|
}
|
|
4671
4848
|
}
|
|
4672
4849
|
const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
|
|
@@ -4684,8 +4861,9 @@ async function collectTurns(profile, turn) {
|
|
|
4684
4861
|
await rm(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
4685
4862
|
}
|
|
4686
4863
|
}
|
|
4864
|
+
await sweepLiveness(liveness);
|
|
4687
4865
|
}
|
|
4688
|
-
async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
4866
|
+
async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
|
|
4689
4867
|
try {
|
|
4690
4868
|
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
4691
4869
|
} catch (error) {
|
|
@@ -4698,7 +4876,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
|
4698
4876
|
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
4699
4877
|
const ownerPath = join(lockPath, "owner.json");
|
|
4700
4878
|
const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
|
|
4701
|
-
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
4879
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
|
|
4702
4880
|
`;
|
|
4703
4881
|
try {
|
|
4704
4882
|
await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
@@ -4742,7 +4920,7 @@ async function signalTestLockContention(profile) {
|
|
|
4742
4920
|
await writeFile(signalPath, `${profile}
|
|
4743
4921
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
4744
4922
|
}
|
|
4745
|
-
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
4923
|
+
async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4746
4924
|
let aged;
|
|
4747
4925
|
try {
|
|
4748
4926
|
const { mtimeMs } = await stat(lockPath);
|
|
@@ -4759,7 +4937,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4759
4937
|
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
4760
4938
|
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
4761
4939
|
const claimPath = join(lockPath, name);
|
|
4762
|
-
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
4940
|
+
if (await hasLiveRecoveryOwner(claimPath, liveness)) {
|
|
4763
4941
|
if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
|
|
4764
4942
|
return false;
|
|
4765
4943
|
}
|
|
@@ -4775,7 +4953,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4775
4953
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4776
4954
|
return true;
|
|
4777
4955
|
}
|
|
4778
|
-
async function hasLiveRecoveryOwner(claimPath) {
|
|
4956
|
+
async function hasLiveRecoveryOwner(claimPath, liveness) {
|
|
4779
4957
|
let contents;
|
|
4780
4958
|
try {
|
|
4781
4959
|
contents = await readFile(claimPath, "utf8");
|
|
@@ -4789,8 +4967,8 @@ async function hasLiveRecoveryOwner(claimPath) {
|
|
|
4789
4967
|
} catch {
|
|
4790
4968
|
return false;
|
|
4791
4969
|
}
|
|
4792
|
-
const
|
|
4793
|
-
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 &&
|
|
4970
|
+
const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
|
|
4971
|
+
return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
|
|
4794
4972
|
}
|
|
4795
4973
|
function recoveryFenced(since, staleAfterMs) {
|
|
4796
4974
|
return performance.now() - since >= staleAfterMs / 2;
|
|
@@ -4804,11 +4982,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
|
4804
4982
|
if (aged) await rm(path, { force: true }).catch(() => void 0);
|
|
4805
4983
|
}
|
|
4806
4984
|
}
|
|
4807
|
-
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
4985
|
+
async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4808
4986
|
const ownerPath = join(lockPath, "owner.json");
|
|
4809
4987
|
const observed = await readFile(ownerPath, "utf8").catch(() => null);
|
|
4810
|
-
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
4811
|
-
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
4988
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
|
|
4989
|
+
if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
|
|
4812
4990
|
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
4813
4991
|
const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
|
|
4814
4992
|
let moved = false;
|
|
@@ -4844,7 +5022,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
|
4844
5022
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4845
5023
|
return true;
|
|
4846
5024
|
}
|
|
4847
|
-
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
5025
|
+
async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
|
|
4848
5026
|
let owner;
|
|
4849
5027
|
try {
|
|
4850
5028
|
owner = JSON.parse(observed);
|
|
@@ -4852,7 +5030,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
|
4852
5030
|
owner = null;
|
|
4853
5031
|
}
|
|
4854
5032
|
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
4855
|
-
return isStaleOwner(owner, staleAfterMs);
|
|
5033
|
+
return isStaleOwner(owner, liveness, staleAfterMs);
|
|
4856
5034
|
}
|
|
4857
5035
|
const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
4858
5036
|
return Date.now() - modifiedAt >= staleAfterMs;
|
|
@@ -4874,14 +5052,14 @@ async function waitForTestBarrier(environmentName, profile) {
|
|
|
4874
5052
|
}
|
|
4875
5053
|
}
|
|
4876
5054
|
}
|
|
4877
|
-
function isStaleOwner(owner, staleAfterMs) {
|
|
5055
|
+
async function isStaleOwner(owner, liveness, staleAfterMs) {
|
|
4878
5056
|
const now = Date.now();
|
|
4879
5057
|
const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
|
|
4880
5058
|
if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
|
|
4881
5059
|
if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
4882
5060
|
return false;
|
|
4883
5061
|
}
|
|
4884
|
-
return !
|
|
5062
|
+
return !await isRecordedProcessAlive(owner, liveness);
|
|
4885
5063
|
}
|
|
4886
5064
|
async function exists(path) {
|
|
4887
5065
|
try {
|
|
@@ -4892,13 +5070,22 @@ async function exists(path) {
|
|
|
4892
5070
|
throw error;
|
|
4893
5071
|
}
|
|
4894
5072
|
}
|
|
4895
|
-
function
|
|
5073
|
+
async function isRecordedProcessAlive(owner, acquisition) {
|
|
5074
|
+
const pid = owner.pid;
|
|
5075
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
|
|
4896
5076
|
try {
|
|
4897
5077
|
process.kill(pid, 0);
|
|
4898
|
-
return true;
|
|
4899
5078
|
} catch (error) {
|
|
4900
5079
|
return !isErrno(error, "ESRCH");
|
|
4901
5080
|
}
|
|
5081
|
+
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;
|
|
5082
|
+
const origin = processOrigin();
|
|
5083
|
+
if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
|
|
5084
|
+
if (owner.pidNamespace === origin.pidNamespace) {
|
|
5085
|
+
return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
|
|
5086
|
+
}
|
|
5087
|
+
const liveness = parseLiveness(owner.liveness);
|
|
5088
|
+
return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
|
|
4902
5089
|
}
|
|
4903
5090
|
function validateProfileName(profile) {
|
|
4904
5091
|
if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
|
|
@@ -4912,7 +5099,7 @@ function isErrno(error, code) {
|
|
|
4912
5099
|
function sleep(milliseconds) {
|
|
4913
5100
|
return new Promise((resolve2) => setTimeout(resolve2, milliseconds));
|
|
4914
5101
|
}
|
|
4915
|
-
var 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,
|
|
5102
|
+
var 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;
|
|
4916
5103
|
var init_state = __esm({
|
|
4917
5104
|
"src/state.ts"() {
|
|
4918
5105
|
"use strict";
|
|
@@ -4934,8 +5121,12 @@ var init_state = __esm({
|
|
|
4934
5121
|
TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
4935
5122
|
TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
4936
5123
|
TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
5124
|
+
TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
4937
5125
|
invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
4938
5126
|
heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
5127
|
+
processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
|
|
5128
|
+
LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
|
|
5129
|
+
LIVENESS_PROBE_TIMEOUT_MS = 1e3;
|
|
4939
5130
|
ProfileLockTimeoutError = class extends Error {
|
|
4940
5131
|
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
4941
5132
|
constructor(profile, timeoutMs, detail) {
|
|
@@ -4952,7 +5143,7 @@ var init_state = __esm({
|
|
|
4952
5143
|
}
|
|
4953
5144
|
};
|
|
4954
5145
|
TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
4955
|
-
relinquishedTurns = globalThis[
|
|
5146
|
+
relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
|
|
4956
5147
|
MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
4957
5148
|
SETTLE_RETRY_MS = 1e3;
|
|
4958
5149
|
SETTLE_RETRIES = 300;
|