@tinycloud/operations 0.5.0-beta.4 → 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/coverage.json +1 -0
- package/dist/artifacts.cjs +232 -40
- package/dist/artifacts.cjs.map +1 -1
- 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 +4 -4
|
@@ -3704,9 +3704,11 @@ function canonicalizeCaveats(caveats) {
|
|
|
3704
3704
|
// src/state.ts
|
|
3705
3705
|
import { randomUUID } from "crypto";
|
|
3706
3706
|
import { AsyncLocalStorage } from "async_hooks";
|
|
3707
|
+
import { readFileSync, readlinkSync } from "fs";
|
|
3707
3708
|
import {
|
|
3708
3709
|
chmod,
|
|
3709
3710
|
link,
|
|
3711
|
+
lstat,
|
|
3710
3712
|
mkdir,
|
|
3711
3713
|
open,
|
|
3712
3714
|
readFile,
|
|
@@ -3718,6 +3720,7 @@ import {
|
|
|
3718
3720
|
unlink,
|
|
3719
3721
|
writeFile
|
|
3720
3722
|
} from "fs/promises";
|
|
3723
|
+
import { connect, createServer } from "net";
|
|
3721
3724
|
import { homedir } from "os";
|
|
3722
3725
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "path";
|
|
3723
3726
|
var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
@@ -3737,13 +3740,171 @@ var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
|
|
|
3737
3740
|
var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
3738
3741
|
var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
3739
3742
|
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
3743
|
+
var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
3740
3744
|
function processWideContext(key) {
|
|
3741
|
-
var
|
|
3745
|
+
var _a4;
|
|
3742
3746
|
const registry = globalThis;
|
|
3743
|
-
return registry[
|
|
3747
|
+
return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
|
|
3744
3748
|
}
|
|
3745
3749
|
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
3746
3750
|
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
3751
|
+
var _a2;
|
|
3752
|
+
var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
|
|
3753
|
+
var cachedProcessOrigin;
|
|
3754
|
+
function processOrigin() {
|
|
3755
|
+
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
3756
|
+
if (process.platform !== "linux") return cachedProcessOrigin = null;
|
|
3757
|
+
try {
|
|
3758
|
+
const bootId = readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
3759
|
+
const pidNamespace = readlinkSync("/proc/self/ns/pid");
|
|
3760
|
+
const processStat = readFileSync("/proc/self/stat", "utf8");
|
|
3761
|
+
const closingParen = processStat.lastIndexOf(") ");
|
|
3762
|
+
const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
|
|
3763
|
+
if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
|
|
3764
|
+
return cachedProcessOrigin = null;
|
|
3765
|
+
}
|
|
3766
|
+
return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
|
|
3767
|
+
} catch {
|
|
3768
|
+
return cachedProcessOrigin = null;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
function processOwner(liveness) {
|
|
3772
|
+
return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
|
|
3773
|
+
}
|
|
3774
|
+
var LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
|
|
3775
|
+
var LIVENESS_PROBE_TIMEOUT_MS = 1e3;
|
|
3776
|
+
function socketDirectory(name) {
|
|
3777
|
+
const match = LIVENESS_SOCKET.exec(name);
|
|
3778
|
+
return match ? `${match[1]}-${match[2]}` : null;
|
|
3779
|
+
}
|
|
3780
|
+
async function pinDirectory(directory) {
|
|
3781
|
+
const handle = await open(directory, "r");
|
|
3782
|
+
try {
|
|
3783
|
+
const { dev, ino } = await handle.stat({ bigint: true });
|
|
3784
|
+
return {
|
|
3785
|
+
identity: `${dev.toString(16)}-${ino.toString(16)}`,
|
|
3786
|
+
path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
|
|
3787
|
+
close: () => handle.close()
|
|
3788
|
+
};
|
|
3789
|
+
} catch (error) {
|
|
3790
|
+
await handle.close();
|
|
3791
|
+
throw error;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
async function bindLiveness(directory) {
|
|
3795
|
+
if (!processOrigin()) return null;
|
|
3796
|
+
let pinned;
|
|
3797
|
+
try {
|
|
3798
|
+
pinned = await pinDirectory(directory);
|
|
3799
|
+
} catch {
|
|
3800
|
+
return null;
|
|
3801
|
+
}
|
|
3802
|
+
const socket = `.live-${pinned.identity}-${randomUUID()}`;
|
|
3803
|
+
const server = createServer((connection) => connection.destroy());
|
|
3804
|
+
let bound = false;
|
|
3805
|
+
try {
|
|
3806
|
+
await new Promise((resolve2, reject) => {
|
|
3807
|
+
server.once("error", reject);
|
|
3808
|
+
server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
|
|
3809
|
+
server.off("error", reject);
|
|
3810
|
+
resolve2();
|
|
3811
|
+
});
|
|
3812
|
+
});
|
|
3813
|
+
bound = true;
|
|
3814
|
+
server.on("error", () => void 0);
|
|
3815
|
+
server.unref();
|
|
3816
|
+
await rename(pinned.path(`${socket}.new`), pinned.path(socket));
|
|
3817
|
+
const stats = await lstat(pinned.path(socket), { bigint: true });
|
|
3818
|
+
if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
|
|
3819
|
+
await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, basename(directory));
|
|
3820
|
+
return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: join(directory, socket), server };
|
|
3821
|
+
} catch {
|
|
3822
|
+
if (bound) {
|
|
3823
|
+
server.close();
|
|
3824
|
+
for (const name of [`${socket}.new`, socket]) await unlink(pinned.path(name)).catch(() => void 0);
|
|
3825
|
+
}
|
|
3826
|
+
return null;
|
|
3827
|
+
} finally {
|
|
3828
|
+
await pinned.close();
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
async function ensureLiveness(acquisition) {
|
|
3832
|
+
if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
|
|
3833
|
+
return acquisition.held;
|
|
3834
|
+
}
|
|
3835
|
+
async function closeLiveness(held) {
|
|
3836
|
+
held.server.close();
|
|
3837
|
+
await unlink(held.path).catch(() => void 0);
|
|
3838
|
+
}
|
|
3839
|
+
function parseLiveness(value) {
|
|
3840
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3841
|
+
const { socket, dev, ino } = value;
|
|
3842
|
+
const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
|
|
3843
|
+
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;
|
|
3844
|
+
}
|
|
3845
|
+
async function controlIn(pinned, acquisition) {
|
|
3846
|
+
const own = await ensureLiveness(acquisition);
|
|
3847
|
+
if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
|
|
3848
|
+
const stats = await lstat(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
|
|
3849
|
+
return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
|
|
3850
|
+
}
|
|
3851
|
+
async function tryConnect(path) {
|
|
3852
|
+
return new Promise((resolve2) => {
|
|
3853
|
+
const probe = connect({ path });
|
|
3854
|
+
const settle = (outcome) => {
|
|
3855
|
+
clearTimeout(timer);
|
|
3856
|
+
probe.destroy();
|
|
3857
|
+
resolve2(outcome);
|
|
3858
|
+
};
|
|
3859
|
+
const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
|
|
3860
|
+
probe.once("connect", () => settle("connected"));
|
|
3861
|
+
probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
|
|
3862
|
+
});
|
|
3863
|
+
}
|
|
3864
|
+
async function kernelRefuses(pinned, name, control) {
|
|
3865
|
+
if (process.versions.bun !== void 0) return false;
|
|
3866
|
+
return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
|
|
3867
|
+
}
|
|
3868
|
+
async function probeLiveness(liveness, acquisition) {
|
|
3869
|
+
let pinned;
|
|
3870
|
+
try {
|
|
3871
|
+
pinned = await pinDirectory(acquisition.directory);
|
|
3872
|
+
} catch {
|
|
3873
|
+
return "unknown";
|
|
3874
|
+
}
|
|
3875
|
+
try {
|
|
3876
|
+
if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
|
|
3877
|
+
const control = await controlIn(pinned, acquisition);
|
|
3878
|
+
if (!control) return "unknown";
|
|
3879
|
+
let stats;
|
|
3880
|
+
try {
|
|
3881
|
+
stats = await lstat(pinned.path(liveness.socket), { bigint: true });
|
|
3882
|
+
} catch (error) {
|
|
3883
|
+
return isErrno(error, "ENOENT") ? "gone" : "unknown";
|
|
3884
|
+
}
|
|
3885
|
+
if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
|
|
3886
|
+
return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
|
|
3887
|
+
} finally {
|
|
3888
|
+
await pinned.close();
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
async function sweepLiveness(acquisition) {
|
|
3892
|
+
if (process.versions.bun !== void 0 || !acquisition.held) return;
|
|
3893
|
+
const pinned = await pinDirectory(acquisition.directory);
|
|
3894
|
+
try {
|
|
3895
|
+
const control = await controlIn(pinned, acquisition);
|
|
3896
|
+
if (!control) return;
|
|
3897
|
+
for (const name of await readdir(pinned.path("."))) {
|
|
3898
|
+
if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
|
|
3899
|
+
const stats = await lstat(pinned.path(name)).catch(() => null);
|
|
3900
|
+
if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
|
|
3901
|
+
await unlink(pinned.path(name)).catch(() => void 0);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
} finally {
|
|
3905
|
+
await pinned.close();
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3747
3908
|
var ProfileLockTimeoutError = class extends Error {
|
|
3748
3909
|
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
3749
3910
|
constructor(profile, timeoutMs, detail) {
|
|
@@ -3915,7 +4076,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
3915
4076
|
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
3916
4077
|
});
|
|
3917
4078
|
}
|
|
3918
|
-
const
|
|
4079
|
+
const liveness = { directory: turnDirectory, held: void 0 };
|
|
4080
|
+
const closeAcquisitionLiveness = async () => {
|
|
4081
|
+
if (liveness.held) await closeLiveness(liveness.held);
|
|
4082
|
+
};
|
|
4083
|
+
let taken;
|
|
4084
|
+
try {
|
|
4085
|
+
taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
|
|
4086
|
+
} catch (error) {
|
|
4087
|
+
await closeAcquisitionLiveness();
|
|
4088
|
+
throw error;
|
|
4089
|
+
}
|
|
4090
|
+
const { turn, firstWaitedTurn } = taken;
|
|
3919
4091
|
try {
|
|
3920
4092
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) {
|
|
3921
4093
|
await rmdir(directory).catch(() => void 0);
|
|
@@ -3925,7 +4097,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
3925
4097
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
3926
4098
|
while (true) {
|
|
3927
4099
|
const token = randomUUID();
|
|
3928
|
-
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
|
|
4100
|
+
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
3929
4101
|
return {
|
|
3930
4102
|
deletedWhileWaiting,
|
|
3931
4103
|
turnSlot: turn.slot,
|
|
@@ -3933,17 +4105,19 @@ async function acquireProfileLock(profile, options) {
|
|
|
3933
4105
|
try {
|
|
3934
4106
|
await releaseProfileLock(profile, lockPath, token);
|
|
3935
4107
|
} finally {
|
|
3936
|
-
await releaseTurn(profile, turn);
|
|
4108
|
+
await releaseTurn(profile, turn, liveness);
|
|
4109
|
+
await closeAcquisitionLiveness();
|
|
3937
4110
|
}
|
|
3938
4111
|
}
|
|
3939
4112
|
};
|
|
3940
4113
|
}
|
|
3941
4114
|
await signalTestLockContention(profile);
|
|
3942
|
-
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
4115
|
+
if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
|
|
3943
4116
|
await waitOrTimeOut(profile, deadline);
|
|
3944
4117
|
}
|
|
3945
4118
|
} catch (error) {
|
|
3946
|
-
await releaseTurn(profile, turn);
|
|
4119
|
+
await releaseTurn(profile, turn, liveness);
|
|
4120
|
+
await closeAcquisitionLiveness();
|
|
3947
4121
|
throw error;
|
|
3948
4122
|
}
|
|
3949
4123
|
}
|
|
@@ -3957,12 +4131,12 @@ async function waitOrTimeOut(profile, deadline, detail) {
|
|
|
3957
4131
|
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
3958
4132
|
}
|
|
3959
4133
|
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
3960
|
-
var
|
|
3961
|
-
var relinquishedTurns = globalThis[
|
|
4134
|
+
var _a3;
|
|
4135
|
+
var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
|
|
3962
4136
|
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
3963
4137
|
var SETTLE_RETRY_MS = 1e3;
|
|
3964
4138
|
var SETTLE_RETRIES = 300;
|
|
3965
|
-
async function acquireTurn(profile, directory, deadline) {
|
|
4139
|
+
async function acquireTurn(profile, directory, deadline, liveness) {
|
|
3966
4140
|
let firstWaitedTurn;
|
|
3967
4141
|
let problem;
|
|
3968
4142
|
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.`;
|
|
@@ -3987,13 +4161,13 @@ async function acquireTurn(profile, directory, deadline) {
|
|
|
3987
4161
|
}
|
|
3988
4162
|
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
3989
4163
|
if (latest.held && latest.done) {
|
|
3990
|
-
const turn = await takeTurn(profile, directory, latest);
|
|
4164
|
+
const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
|
|
3991
4165
|
if (turn !== null) return { turn, firstWaitedTurn };
|
|
3992
4166
|
await waitOrTimeOut(profile, deadline);
|
|
3993
4167
|
} else if (latest.done) {
|
|
3994
4168
|
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
3995
4169
|
await waitOrTimeOut(profile, deadline, problem);
|
|
3996
|
-
} else if (latest.pid === process.pid
|
|
4170
|
+
} else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
|
|
3997
4171
|
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
3998
4172
|
await settleAbandonedTurn(directory, latest);
|
|
3999
4173
|
relinquishedTurns.delete(latest.token);
|
|
@@ -4038,6 +4212,7 @@ async function readTurn(directory, slot) {
|
|
|
4038
4212
|
const done = await exists(join(path, `${owner.token}.done`));
|
|
4039
4213
|
const held = await exists(join(path, `${owner.token}.held`));
|
|
4040
4214
|
return {
|
|
4215
|
+
...owner,
|
|
4041
4216
|
slot,
|
|
4042
4217
|
token: owner.token,
|
|
4043
4218
|
pid: owner.pid,
|
|
@@ -4051,11 +4226,11 @@ async function isFinishedTurn(directory, slot, token) {
|
|
|
4051
4226
|
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
4052
4227
|
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
4053
4228
|
}
|
|
4054
|
-
async function takeTurn(profile, directory, latest) {
|
|
4229
|
+
async function takeTurn(profile, directory, latest, liveness) {
|
|
4055
4230
|
const token = randomUUID();
|
|
4056
4231
|
const slot = latest.slot + 1;
|
|
4057
4232
|
const path = join(directory, String(slot));
|
|
4058
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
4233
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
|
|
4059
4234
|
let published = false;
|
|
4060
4235
|
try {
|
|
4061
4236
|
published = await publishDirectory(
|
|
@@ -4109,7 +4284,7 @@ async function markTurn(path, token, kind) {
|
|
|
4109
4284
|
}
|
|
4110
4285
|
async function createTurnDirectory(directory) {
|
|
4111
4286
|
const token = randomUUID();
|
|
4112
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
4287
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
|
|
4113
4288
|
await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
|
|
4114
4289
|
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
4115
4290
|
`,
|
|
@@ -4133,31 +4308,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
|
|
|
4133
4308
|
await rm(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
4134
4309
|
}
|
|
4135
4310
|
}
|
|
4136
|
-
async function releaseTurn(profile, turn) {
|
|
4311
|
+
async function releaseTurn(profile, turn, liveness) {
|
|
4137
4312
|
try {
|
|
4138
4313
|
await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
|
|
4139
4314
|
} catch {
|
|
4140
4315
|
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
4141
4316
|
return;
|
|
4142
4317
|
}
|
|
4143
|
-
await collectTurns(profile, turn).catch(() => void 0);
|
|
4318
|
+
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
4144
4319
|
}
|
|
4145
|
-
function isAbandonedTurnEntry(name) {
|
|
4320
|
+
async function isAbandonedTurnEntry(directory, name, liveness) {
|
|
4146
4321
|
if (name.startsWith(".trash-")) return true;
|
|
4147
4322
|
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
4148
|
-
|
|
4323
|
+
if (writer === void 0) return false;
|
|
4324
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4325
|
+
return name.startsWith(".stage-") && await isStagedOwnerGone(join(directory, name, "owner.json"), liveness);
|
|
4149
4326
|
}
|
|
4150
|
-
function isAbandonedTurnDirectoryStage(name) {
|
|
4327
|
+
async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
|
|
4151
4328
|
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];
|
|
4152
|
-
|
|
4329
|
+
if (writer === void 0) return false;
|
|
4330
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4331
|
+
return isStagedOwnerGone(join(directory, name, "0", "owner.json"), liveness);
|
|
4153
4332
|
}
|
|
4154
|
-
async function
|
|
4333
|
+
async function isStagedOwnerGone(recordPath, liveness) {
|
|
4334
|
+
const owner = await readJson(recordPath).catch(() => null);
|
|
4335
|
+
return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
|
|
4336
|
+
}
|
|
4337
|
+
async function collectTurns(profile, turn, liveness) {
|
|
4155
4338
|
for (const [directory, abandoned] of [
|
|
4156
4339
|
[turn.directory, isAbandonedTurnEntry],
|
|
4157
4340
|
[dirname(turn.directory), isAbandonedTurnDirectoryStage]
|
|
4158
4341
|
]) {
|
|
4159
|
-
for (const name of
|
|
4160
|
-
await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4342
|
+
for (const name of await readdir(directory)) {
|
|
4343
|
+
if (await abandoned(directory, name, liveness)) await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4161
4344
|
}
|
|
4162
4345
|
}
|
|
4163
4346
|
const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
|
|
@@ -4175,8 +4358,9 @@ async function collectTurns(profile, turn) {
|
|
|
4175
4358
|
await rm(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
4176
4359
|
}
|
|
4177
4360
|
}
|
|
4361
|
+
await sweepLiveness(liveness);
|
|
4178
4362
|
}
|
|
4179
|
-
async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
4363
|
+
async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
|
|
4180
4364
|
try {
|
|
4181
4365
|
await mkdir(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
4182
4366
|
} catch (error) {
|
|
@@ -4189,7 +4373,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
|
4189
4373
|
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
4190
4374
|
const ownerPath = join(lockPath, "owner.json");
|
|
4191
4375
|
const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
|
|
4192
|
-
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
4376
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
|
|
4193
4377
|
`;
|
|
4194
4378
|
try {
|
|
4195
4379
|
await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
@@ -4234,7 +4418,7 @@ async function signalTestLockContention(profile) {
|
|
|
4234
4418
|
await writeFile(signalPath, `${profile}
|
|
4235
4419
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
4236
4420
|
}
|
|
4237
|
-
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
4421
|
+
async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4238
4422
|
let aged;
|
|
4239
4423
|
try {
|
|
4240
4424
|
const { mtimeMs } = await stat(lockPath);
|
|
@@ -4251,7 +4435,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4251
4435
|
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
4252
4436
|
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
4253
4437
|
const claimPath = join(lockPath, name);
|
|
4254
|
-
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
4438
|
+
if (await hasLiveRecoveryOwner(claimPath, liveness)) {
|
|
4255
4439
|
if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
|
|
4256
4440
|
return false;
|
|
4257
4441
|
}
|
|
@@ -4267,7 +4451,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4267
4451
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4268
4452
|
return true;
|
|
4269
4453
|
}
|
|
4270
|
-
async function hasLiveRecoveryOwner(claimPath) {
|
|
4454
|
+
async function hasLiveRecoveryOwner(claimPath, liveness) {
|
|
4271
4455
|
let contents;
|
|
4272
4456
|
try {
|
|
4273
4457
|
contents = await readFile(claimPath, "utf8");
|
|
@@ -4281,8 +4465,8 @@ async function hasLiveRecoveryOwner(claimPath) {
|
|
|
4281
4465
|
} catch {
|
|
4282
4466
|
return false;
|
|
4283
4467
|
}
|
|
4284
|
-
const
|
|
4285
|
-
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 &&
|
|
4468
|
+
const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
|
|
4469
|
+
return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
|
|
4286
4470
|
}
|
|
4287
4471
|
function recoveryFenced(since, staleAfterMs) {
|
|
4288
4472
|
return performance.now() - since >= staleAfterMs / 2;
|
|
@@ -4299,11 +4483,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
|
4299
4483
|
if (aged) await rm(path, { force: true }).catch(() => void 0);
|
|
4300
4484
|
}
|
|
4301
4485
|
}
|
|
4302
|
-
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
4486
|
+
async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4303
4487
|
const ownerPath = join(lockPath, "owner.json");
|
|
4304
4488
|
const observed = await readFile(ownerPath, "utf8").catch(() => null);
|
|
4305
|
-
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
4306
|
-
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
4489
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
|
|
4490
|
+
if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
|
|
4307
4491
|
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
4308
4492
|
const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
|
|
4309
4493
|
let moved = false;
|
|
@@ -4339,7 +4523,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
|
4339
4523
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4340
4524
|
return true;
|
|
4341
4525
|
}
|
|
4342
|
-
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
4526
|
+
async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
|
|
4343
4527
|
let owner;
|
|
4344
4528
|
try {
|
|
4345
4529
|
owner = JSON.parse(observed);
|
|
@@ -4347,7 +4531,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
|
4347
4531
|
owner = null;
|
|
4348
4532
|
}
|
|
4349
4533
|
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
4350
|
-
return isStaleOwner(owner, staleAfterMs);
|
|
4534
|
+
return isStaleOwner(owner, liveness, staleAfterMs);
|
|
4351
4535
|
}
|
|
4352
4536
|
const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
4353
4537
|
return Date.now() - modifiedAt >= staleAfterMs;
|
|
@@ -4369,14 +4553,14 @@ async function waitForTestBarrier(environmentName, profile) {
|
|
|
4369
4553
|
}
|
|
4370
4554
|
}
|
|
4371
4555
|
}
|
|
4372
|
-
function isStaleOwner(owner, staleAfterMs) {
|
|
4556
|
+
async function isStaleOwner(owner, liveness, staleAfterMs) {
|
|
4373
4557
|
const now = Date.now();
|
|
4374
4558
|
const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
|
|
4375
4559
|
if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
|
|
4376
4560
|
if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
4377
4561
|
return false;
|
|
4378
4562
|
}
|
|
4379
|
-
return !
|
|
4563
|
+
return !await isRecordedProcessAlive(owner, liveness);
|
|
4380
4564
|
}
|
|
4381
4565
|
async function exists(path) {
|
|
4382
4566
|
try {
|
|
@@ -4387,13 +4571,22 @@ async function exists(path) {
|
|
|
4387
4571
|
throw error;
|
|
4388
4572
|
}
|
|
4389
4573
|
}
|
|
4390
|
-
function
|
|
4574
|
+
async function isRecordedProcessAlive(owner, acquisition) {
|
|
4575
|
+
const pid = owner.pid;
|
|
4576
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
|
|
4391
4577
|
try {
|
|
4392
4578
|
process.kill(pid, 0);
|
|
4393
|
-
return true;
|
|
4394
4579
|
} catch (error) {
|
|
4395
4580
|
return !isErrno(error, "ESRCH");
|
|
4396
4581
|
}
|
|
4582
|
+
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;
|
|
4583
|
+
const origin = processOrigin();
|
|
4584
|
+
if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
|
|
4585
|
+
if (owner.pidNamespace === origin.pidNamespace) {
|
|
4586
|
+
return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
|
|
4587
|
+
}
|
|
4588
|
+
const liveness = parseLiveness(owner.liveness);
|
|
4589
|
+
return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
|
|
4397
4590
|
}
|
|
4398
4591
|
function validateProfileName(profile) {
|
|
4399
4592
|
if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
|