@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
|
@@ -3654,10 +3654,10 @@ function evaluateOperationAuthority(granted, required, resolveSpace) {
|
|
|
3654
3654
|
const canonicalRequired = canonicalizeOperationCapabilities(required, resolveSpace);
|
|
3655
3655
|
const missing = [];
|
|
3656
3656
|
for (const requested of canonicalRequired) {
|
|
3657
|
-
const
|
|
3658
|
-
(candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
|
|
3657
|
+
const sameScope = canonicalGranted.filter(
|
|
3658
|
+
(candidate) => candidate.service === requested.service && operationSpacesEqual(candidate.space, requested.space)
|
|
3659
3659
|
);
|
|
3660
|
-
if (!
|
|
3660
|
+
if (!isCapabilitySubset([requested], sameScope).subset) missing.push(requested);
|
|
3661
3661
|
}
|
|
3662
3662
|
return {
|
|
3663
3663
|
satisfied: missing.length === 0,
|
|
@@ -3734,7 +3734,9 @@ function canonicalizeCaveats(caveats) {
|
|
|
3734
3734
|
// src/state.ts
|
|
3735
3735
|
var import_node_crypto = require("crypto");
|
|
3736
3736
|
var import_node_async_hooks = require("async_hooks");
|
|
3737
|
+
var import_node_fs = require("fs");
|
|
3737
3738
|
var import_promises = require("fs/promises");
|
|
3739
|
+
var import_node_net = require("net");
|
|
3738
3740
|
var import_node_os = require("os");
|
|
3739
3741
|
var import_node_path = require("path");
|
|
3740
3742
|
var DEFAULT_LOCK_TIMEOUT_MS = 2e3;
|
|
@@ -3754,13 +3756,171 @@ var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
|
|
|
3754
3756
|
var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
3755
3757
|
var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
3756
3758
|
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
3759
|
+
var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
3757
3760
|
function processWideContext(key) {
|
|
3758
|
-
var
|
|
3761
|
+
var _a4;
|
|
3759
3762
|
const registry = globalThis;
|
|
3760
|
-
return registry[
|
|
3763
|
+
return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
|
|
3761
3764
|
}
|
|
3762
3765
|
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
3763
3766
|
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
3767
|
+
var _a2;
|
|
3768
|
+
var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
|
|
3769
|
+
var cachedProcessOrigin;
|
|
3770
|
+
function processOrigin() {
|
|
3771
|
+
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
3772
|
+
if (process.platform !== "linux") return cachedProcessOrigin = null;
|
|
3773
|
+
try {
|
|
3774
|
+
const bootId = (0, import_node_fs.readFileSync)("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
3775
|
+
const pidNamespace = (0, import_node_fs.readlinkSync)("/proc/self/ns/pid");
|
|
3776
|
+
const processStat = (0, import_node_fs.readFileSync)("/proc/self/stat", "utf8");
|
|
3777
|
+
const closingParen = processStat.lastIndexOf(") ");
|
|
3778
|
+
const processStartTime = closingParen < 0 ? void 0 : processStat.slice(closingParen + 2).trim().split(/\s+/)[19];
|
|
3779
|
+
if (!/^[0-9a-f-]{36}$/.test(bootId) || !/^pid:\[[0-9]+\]$/.test(pidNamespace) || !processStartTime || !/^[0-9]+$/.test(processStartTime)) {
|
|
3780
|
+
return cachedProcessOrigin = null;
|
|
3781
|
+
}
|
|
3782
|
+
return cachedProcessOrigin = { bootId, pidNamespace, processStartTime };
|
|
3783
|
+
} catch {
|
|
3784
|
+
return cachedProcessOrigin = null;
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
function processOwner(liveness) {
|
|
3788
|
+
return { instanceId: processInstanceId, ...processOrigin(), ...liveness ? { liveness: liveness.record } : {} };
|
|
3789
|
+
}
|
|
3790
|
+
var LIVENESS_SOCKET = /^\.live-([0-9a-f]+)-([0-9a-f]+)-[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}(\.new)?$/;
|
|
3791
|
+
var LIVENESS_PROBE_TIMEOUT_MS = 1e3;
|
|
3792
|
+
function socketDirectory(name) {
|
|
3793
|
+
const match = LIVENESS_SOCKET.exec(name);
|
|
3794
|
+
return match ? `${match[1]}-${match[2]}` : null;
|
|
3795
|
+
}
|
|
3796
|
+
async function pinDirectory(directory) {
|
|
3797
|
+
const handle = await (0, import_promises.open)(directory, "r");
|
|
3798
|
+
try {
|
|
3799
|
+
const { dev, ino } = await handle.stat({ bigint: true });
|
|
3800
|
+
return {
|
|
3801
|
+
identity: `${dev.toString(16)}-${ino.toString(16)}`,
|
|
3802
|
+
path: (name) => `/proc/self/fd/${handle.fd}/${name}`,
|
|
3803
|
+
close: () => handle.close()
|
|
3804
|
+
};
|
|
3805
|
+
} catch (error) {
|
|
3806
|
+
await handle.close();
|
|
3807
|
+
throw error;
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
async function bindLiveness(directory) {
|
|
3811
|
+
if (!processOrigin()) return null;
|
|
3812
|
+
let pinned;
|
|
3813
|
+
try {
|
|
3814
|
+
pinned = await pinDirectory(directory);
|
|
3815
|
+
} catch {
|
|
3816
|
+
return null;
|
|
3817
|
+
}
|
|
3818
|
+
const socket = `.live-${pinned.identity}-${(0, import_node_crypto.randomUUID)()}`;
|
|
3819
|
+
const server = (0, import_node_net.createServer)((connection) => connection.destroy());
|
|
3820
|
+
let bound = false;
|
|
3821
|
+
try {
|
|
3822
|
+
await new Promise((resolve2, reject) => {
|
|
3823
|
+
server.once("error", reject);
|
|
3824
|
+
server.listen({ path: pinned.path(`${socket}.new`), exclusive: true }, () => {
|
|
3825
|
+
server.off("error", reject);
|
|
3826
|
+
resolve2();
|
|
3827
|
+
});
|
|
3828
|
+
});
|
|
3829
|
+
bound = true;
|
|
3830
|
+
server.on("error", () => void 0);
|
|
3831
|
+
server.unref();
|
|
3832
|
+
await (0, import_promises.rename)(pinned.path(`${socket}.new`), pinned.path(socket));
|
|
3833
|
+
const stats = await (0, import_promises.lstat)(pinned.path(socket), { bigint: true });
|
|
3834
|
+
if (!stats.isSocket()) throw new Error(`${socket} is not a socket`);
|
|
3835
|
+
await waitForTestBarrier(TEST_LOCK_LIVENESS_BARRIER_DIR, (0, import_node_path.basename)(directory));
|
|
3836
|
+
return { record: { socket, dev: String(stats.dev), ino: String(stats.ino) }, path: (0, import_node_path.join)(directory, socket), server };
|
|
3837
|
+
} catch {
|
|
3838
|
+
if (bound) {
|
|
3839
|
+
server.close();
|
|
3840
|
+
for (const name of [`${socket}.new`, socket]) await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
|
|
3841
|
+
}
|
|
3842
|
+
return null;
|
|
3843
|
+
} finally {
|
|
3844
|
+
await pinned.close();
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
async function ensureLiveness(acquisition) {
|
|
3848
|
+
if (acquisition.held === void 0) acquisition.held = await bindLiveness(acquisition.directory);
|
|
3849
|
+
return acquisition.held;
|
|
3850
|
+
}
|
|
3851
|
+
async function closeLiveness(held) {
|
|
3852
|
+
held.server.close();
|
|
3853
|
+
await (0, import_promises.unlink)(held.path).catch(() => void 0);
|
|
3854
|
+
}
|
|
3855
|
+
function parseLiveness(value) {
|
|
3856
|
+
if (typeof value !== "object" || value === null) return null;
|
|
3857
|
+
const { socket, dev, ino } = value;
|
|
3858
|
+
const name = typeof socket === "string" ? LIVENESS_SOCKET.exec(socket) : null;
|
|
3859
|
+
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;
|
|
3860
|
+
}
|
|
3861
|
+
async function controlIn(pinned, acquisition) {
|
|
3862
|
+
const own = await ensureLiveness(acquisition);
|
|
3863
|
+
if (!own || socketDirectory(own.record.socket) !== pinned.identity) return null;
|
|
3864
|
+
const stats = await (0, import_promises.lstat)(pinned.path(own.record.socket), { bigint: true }).catch(() => null);
|
|
3865
|
+
return stats?.isSocket() && String(stats.dev) === own.record.dev && String(stats.ino) === own.record.ino ? own : null;
|
|
3866
|
+
}
|
|
3867
|
+
async function tryConnect(path) {
|
|
3868
|
+
return new Promise((resolve2) => {
|
|
3869
|
+
const probe = (0, import_node_net.connect)({ path });
|
|
3870
|
+
const settle = (outcome) => {
|
|
3871
|
+
clearTimeout(timer);
|
|
3872
|
+
probe.destroy();
|
|
3873
|
+
resolve2(outcome);
|
|
3874
|
+
};
|
|
3875
|
+
const timer = setTimeout(() => settle("other"), LIVENESS_PROBE_TIMEOUT_MS);
|
|
3876
|
+
probe.once("connect", () => settle("connected"));
|
|
3877
|
+
probe.once("error", (error) => settle(isErrno(error, "ECONNREFUSED") ? "refused" : "other"));
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
async function kernelRefuses(pinned, name, control) {
|
|
3881
|
+
if (process.versions.bun !== void 0) return false;
|
|
3882
|
+
return await tryConnect(pinned.path(name)) === "refused" && await tryConnect(pinned.path(control.record.socket)) === "connected";
|
|
3883
|
+
}
|
|
3884
|
+
async function probeLiveness(liveness, acquisition) {
|
|
3885
|
+
let pinned;
|
|
3886
|
+
try {
|
|
3887
|
+
pinned = await pinDirectory(acquisition.directory);
|
|
3888
|
+
} catch {
|
|
3889
|
+
return "unknown";
|
|
3890
|
+
}
|
|
3891
|
+
try {
|
|
3892
|
+
if (socketDirectory(liveness.socket) !== pinned.identity) return "unknown";
|
|
3893
|
+
const control = await controlIn(pinned, acquisition);
|
|
3894
|
+
if (!control) return "unknown";
|
|
3895
|
+
let stats;
|
|
3896
|
+
try {
|
|
3897
|
+
stats = await (0, import_promises.lstat)(pinned.path(liveness.socket), { bigint: true });
|
|
3898
|
+
} catch (error) {
|
|
3899
|
+
return isErrno(error, "ENOENT") ? "gone" : "unknown";
|
|
3900
|
+
}
|
|
3901
|
+
if (!stats.isSocket() || String(stats.dev) !== liveness.dev || String(stats.ino) !== liveness.ino) return "unknown";
|
|
3902
|
+
return await kernelRefuses(pinned, liveness.socket, control) ? "refused" : "unknown";
|
|
3903
|
+
} finally {
|
|
3904
|
+
await pinned.close();
|
|
3905
|
+
}
|
|
3906
|
+
}
|
|
3907
|
+
async function sweepLiveness(acquisition) {
|
|
3908
|
+
if (process.versions.bun !== void 0 || !acquisition.held) return;
|
|
3909
|
+
const pinned = await pinDirectory(acquisition.directory);
|
|
3910
|
+
try {
|
|
3911
|
+
const control = await controlIn(pinned, acquisition);
|
|
3912
|
+
if (!control) return;
|
|
3913
|
+
for (const name of await (0, import_promises.readdir)(pinned.path("."))) {
|
|
3914
|
+
if (name === control.record.socket || socketDirectory(name) !== pinned.identity) continue;
|
|
3915
|
+
const stats = await (0, import_promises.lstat)(pinned.path(name)).catch(() => null);
|
|
3916
|
+
if (stats?.isSocket() && await kernelRefuses(pinned, name, control)) {
|
|
3917
|
+
await (0, import_promises.unlink)(pinned.path(name)).catch(() => void 0);
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
} finally {
|
|
3921
|
+
await pinned.close();
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3764
3924
|
var ProfileLockTimeoutError = class extends Error {
|
|
3765
3925
|
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
3766
3926
|
constructor(profile, timeoutMs, detail) {
|
|
@@ -3932,7 +4092,18 @@ async function acquireProfileLock(profile, options) {
|
|
|
3932
4092
|
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
3933
4093
|
});
|
|
3934
4094
|
}
|
|
3935
|
-
const
|
|
4095
|
+
const liveness = { directory: turnDirectory, held: void 0 };
|
|
4096
|
+
const closeAcquisitionLiveness = async () => {
|
|
4097
|
+
if (liveness.held) await closeLiveness(liveness.held);
|
|
4098
|
+
};
|
|
4099
|
+
let taken;
|
|
4100
|
+
try {
|
|
4101
|
+
taken = await acquireTurn(profile, turnDirectory, deadline, liveness);
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
await closeAcquisitionLiveness();
|
|
4104
|
+
throw error;
|
|
4105
|
+
}
|
|
4106
|
+
const { turn, firstWaitedTurn } = taken;
|
|
3936
4107
|
try {
|
|
3937
4108
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) {
|
|
3938
4109
|
await (0, import_promises.rmdir)(directory).catch(() => void 0);
|
|
@@ -3942,7 +4113,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
3942
4113
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
3943
4114
|
while (true) {
|
|
3944
4115
|
const token = (0, import_node_crypto.randomUUID)();
|
|
3945
|
-
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true)) {
|
|
4116
|
+
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
3946
4117
|
return {
|
|
3947
4118
|
deletedWhileWaiting,
|
|
3948
4119
|
turnSlot: turn.slot,
|
|
@@ -3950,17 +4121,19 @@ async function acquireProfileLock(profile, options) {
|
|
|
3950
4121
|
try {
|
|
3951
4122
|
await releaseProfileLock(profile, lockPath, token);
|
|
3952
4123
|
} finally {
|
|
3953
|
-
await releaseTurn(profile, turn);
|
|
4124
|
+
await releaseTurn(profile, turn, liveness);
|
|
4125
|
+
await closeAcquisitionLiveness();
|
|
3954
4126
|
}
|
|
3955
4127
|
}
|
|
3956
4128
|
};
|
|
3957
4129
|
}
|
|
3958
4130
|
await signalTestLockContention(profile);
|
|
3959
|
-
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
4131
|
+
if (await recoverStaleLock(profile, lockPath, liveness, staleAfterMs)) continue;
|
|
3960
4132
|
await waitOrTimeOut(profile, deadline);
|
|
3961
4133
|
}
|
|
3962
4134
|
} catch (error) {
|
|
3963
|
-
await releaseTurn(profile, turn);
|
|
4135
|
+
await releaseTurn(profile, turn, liveness);
|
|
4136
|
+
await closeAcquisitionLiveness();
|
|
3964
4137
|
throw error;
|
|
3965
4138
|
}
|
|
3966
4139
|
}
|
|
@@ -3974,12 +4147,12 @@ async function waitOrTimeOut(profile, deadline, detail) {
|
|
|
3974
4147
|
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
3975
4148
|
}
|
|
3976
4149
|
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
3977
|
-
var
|
|
3978
|
-
var relinquishedTurns = globalThis[
|
|
4150
|
+
var _a3;
|
|
4151
|
+
var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
|
|
3979
4152
|
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
3980
4153
|
var SETTLE_RETRY_MS = 1e3;
|
|
3981
4154
|
var SETTLE_RETRIES = 300;
|
|
3982
|
-
async function acquireTurn(profile, directory, deadline) {
|
|
4155
|
+
async function acquireTurn(profile, directory, deadline, liveness) {
|
|
3983
4156
|
let firstWaitedTurn;
|
|
3984
4157
|
let problem;
|
|
3985
4158
|
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.`;
|
|
@@ -4004,13 +4177,13 @@ async function acquireTurn(profile, directory, deadline) {
|
|
|
4004
4177
|
}
|
|
4005
4178
|
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
4006
4179
|
if (latest.held && latest.done) {
|
|
4007
|
-
const turn = await takeTurn(profile, directory, latest);
|
|
4180
|
+
const turn = await takeTurn(profile, directory, latest, await ensureLiveness(liveness));
|
|
4008
4181
|
if (turn !== null) return { turn, firstWaitedTurn };
|
|
4009
4182
|
await waitOrTimeOut(profile, deadline);
|
|
4010
4183
|
} else if (latest.done) {
|
|
4011
4184
|
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
4012
4185
|
await waitOrTimeOut(profile, deadline, problem);
|
|
4013
|
-
} else if (latest.pid === process.pid
|
|
4186
|
+
} else if (latest.pid === process.pid && relinquishedTurns.has(latest.token) || !await isRecordedProcessAlive(latest, liveness)) {
|
|
4014
4187
|
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
4015
4188
|
await settleAbandonedTurn(directory, latest);
|
|
4016
4189
|
relinquishedTurns.delete(latest.token);
|
|
@@ -4055,6 +4228,7 @@ async function readTurn(directory, slot) {
|
|
|
4055
4228
|
const done = await exists((0, import_node_path.join)(path, `${owner.token}.done`));
|
|
4056
4229
|
const held = await exists((0, import_node_path.join)(path, `${owner.token}.held`));
|
|
4057
4230
|
return {
|
|
4231
|
+
...owner,
|
|
4058
4232
|
slot,
|
|
4059
4233
|
token: owner.token,
|
|
4060
4234
|
pid: owner.pid,
|
|
@@ -4068,11 +4242,11 @@ async function isFinishedTurn(directory, slot, token) {
|
|
|
4068
4242
|
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
4069
4243
|
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
4070
4244
|
}
|
|
4071
|
-
async function takeTurn(profile, directory, latest) {
|
|
4245
|
+
async function takeTurn(profile, directory, latest, liveness) {
|
|
4072
4246
|
const token = (0, import_node_crypto.randomUUID)();
|
|
4073
4247
|
const slot = latest.slot + 1;
|
|
4074
4248
|
const path = (0, import_node_path.join)(directory, String(slot));
|
|
4075
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
4249
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token, ...processOwner(liveness) };
|
|
4076
4250
|
let published = false;
|
|
4077
4251
|
try {
|
|
4078
4252
|
published = await publishDirectory(
|
|
@@ -4126,7 +4300,7 @@ async function markTurn(path, token, kind) {
|
|
|
4126
4300
|
}
|
|
4127
4301
|
async function createTurnDirectory(directory) {
|
|
4128
4302
|
const token = (0, import_node_crypto.randomUUID)();
|
|
4129
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
4303
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null, ...processOwner(null) };
|
|
4130
4304
|
await publishDirectory((0, import_node_path.join)((0, import_node_path.dirname)(directory), `.stage-${(0, import_node_path.basename)(directory)}-${process.pid}-${token}`), directory, {
|
|
4131
4305
|
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
4132
4306
|
`,
|
|
@@ -4150,31 +4324,39 @@ async function publishDirectory(stage, target, files, beforeRename) {
|
|
|
4150
4324
|
await (0, import_promises.rm)(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
4151
4325
|
}
|
|
4152
4326
|
}
|
|
4153
|
-
async function releaseTurn(profile, turn) {
|
|
4327
|
+
async function releaseTurn(profile, turn, liveness) {
|
|
4154
4328
|
try {
|
|
4155
4329
|
await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
|
|
4156
4330
|
} catch {
|
|
4157
4331
|
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
4158
4332
|
return;
|
|
4159
4333
|
}
|
|
4160
|
-
await collectTurns(profile, turn).catch(() => void 0);
|
|
4334
|
+
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
4161
4335
|
}
|
|
4162
|
-
function isAbandonedTurnEntry(name) {
|
|
4336
|
+
async function isAbandonedTurnEntry(directory, name, liveness) {
|
|
4163
4337
|
if (name.startsWith(".trash-")) return true;
|
|
4164
4338
|
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
4165
|
-
|
|
4339
|
+
if (writer === void 0) return false;
|
|
4340
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4341
|
+
return name.startsWith(".stage-") && await isStagedOwnerGone((0, import_node_path.join)(directory, name, "owner.json"), liveness);
|
|
4166
4342
|
}
|
|
4167
|
-
function isAbandonedTurnDirectoryStage(name) {
|
|
4343
|
+
async function isAbandonedTurnDirectoryStage(directory, name, liveness) {
|
|
4168
4344
|
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];
|
|
4169
|
-
|
|
4345
|
+
if (writer === void 0) return false;
|
|
4346
|
+
if (Number(writer) !== process.pid) return !await isRecordedProcessAlive({ pid: Number(writer) }, liveness);
|
|
4347
|
+
return isStagedOwnerGone((0, import_node_path.join)(directory, name, "0", "owner.json"), liveness);
|
|
4170
4348
|
}
|
|
4171
|
-
async function
|
|
4349
|
+
async function isStagedOwnerGone(recordPath, liveness) {
|
|
4350
|
+
const owner = await readJson(recordPath).catch(() => null);
|
|
4351
|
+
return typeof owner === "object" && owner !== null && owner.pid === process.pid && !await isRecordedProcessAlive(owner, liveness);
|
|
4352
|
+
}
|
|
4353
|
+
async function collectTurns(profile, turn, liveness) {
|
|
4172
4354
|
for (const [directory, abandoned] of [
|
|
4173
4355
|
[turn.directory, isAbandonedTurnEntry],
|
|
4174
4356
|
[(0, import_node_path.dirname)(turn.directory), isAbandonedTurnDirectoryStage]
|
|
4175
4357
|
]) {
|
|
4176
|
-
for (const name of
|
|
4177
|
-
await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
4358
|
+
for (const name of await (0, import_promises.readdir)(directory)) {
|
|
4359
|
+
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);
|
|
4178
4360
|
}
|
|
4179
4361
|
}
|
|
4180
4362
|
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);
|
|
@@ -4192,8 +4374,9 @@ async function collectTurns(profile, turn) {
|
|
|
4192
4374
|
await (0, import_promises.rm)(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
4193
4375
|
}
|
|
4194
4376
|
}
|
|
4377
|
+
await sweepLiveness(liveness);
|
|
4195
4378
|
}
|
|
4196
|
-
async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
4379
|
+
async function publishProfileLock(profile, lockPath, token, requireProfile, liveness) {
|
|
4197
4380
|
try {
|
|
4198
4381
|
await (0, import_promises.mkdir)(lockPath, { mode: PRIVATE_DIR_MODE });
|
|
4199
4382
|
} catch (error) {
|
|
@@ -4206,7 +4389,7 @@ async function publishProfileLock(profile, lockPath, token, requireProfile) {
|
|
|
4206
4389
|
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
4207
4390
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
4208
4391
|
const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
|
|
4209
|
-
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
4392
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, ...processOwner(liveness) }, null, 2)}
|
|
4210
4393
|
`;
|
|
4211
4394
|
try {
|
|
4212
4395
|
await (0, import_promises.writeFile)(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
@@ -4251,7 +4434,7 @@ async function signalTestLockContention(profile) {
|
|
|
4251
4434
|
await (0, import_promises.writeFile)(signalPath, `${profile}
|
|
4252
4435
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
4253
4436
|
}
|
|
4254
|
-
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
4437
|
+
async function recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4255
4438
|
let aged;
|
|
4256
4439
|
try {
|
|
4257
4440
|
const { mtimeMs } = await (0, import_promises.stat)(lockPath);
|
|
@@ -4268,7 +4451,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4268
4451
|
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
4269
4452
|
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
4270
4453
|
const claimPath = (0, import_node_path.join)(lockPath, name);
|
|
4271
|
-
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
4454
|
+
if (await hasLiveRecoveryOwner(claimPath, liveness)) {
|
|
4272
4455
|
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"));
|
|
4273
4456
|
return false;
|
|
4274
4457
|
}
|
|
@@ -4284,7 +4467,7 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
4284
4467
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4285
4468
|
return true;
|
|
4286
4469
|
}
|
|
4287
|
-
async function hasLiveRecoveryOwner(claimPath) {
|
|
4470
|
+
async function hasLiveRecoveryOwner(claimPath, liveness) {
|
|
4288
4471
|
let contents;
|
|
4289
4472
|
try {
|
|
4290
4473
|
contents = await (0, import_promises.readFile)(claimPath, "utf8");
|
|
@@ -4298,8 +4481,8 @@ async function hasLiveRecoveryOwner(claimPath) {
|
|
|
4298
4481
|
} catch {
|
|
4299
4482
|
return false;
|
|
4300
4483
|
}
|
|
4301
|
-
const
|
|
4302
|
-
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 &&
|
|
4484
|
+
const record = typeof owner === "object" && owner !== null && "pid" in owner ? owner : null;
|
|
4485
|
+
return typeof record?.pid === "number" && Number.isInteger(record.pid) && record.pid > 0 && await isRecordedProcessAlive(record, liveness);
|
|
4303
4486
|
}
|
|
4304
4487
|
function recoveryFenced(since, staleAfterMs) {
|
|
4305
4488
|
return performance.now() - since >= staleAfterMs / 2;
|
|
@@ -4316,11 +4499,11 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
|
4316
4499
|
if (aged) await (0, import_promises.rm)(path, { force: true }).catch(() => void 0);
|
|
4317
4500
|
}
|
|
4318
4501
|
}
|
|
4319
|
-
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
4502
|
+
async function recoverStaleLock(profile, lockPath, liveness, staleAfterMs) {
|
|
4320
4503
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
4321
4504
|
const observed = await (0, import_promises.readFile)(ownerPath, "utf8").catch(() => null);
|
|
4322
|
-
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
4323
|
-
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
4505
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, liveness, staleAfterMs);
|
|
4506
|
+
if (!await isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs)) return false;
|
|
4324
4507
|
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
4325
4508
|
const claimPath = (0, import_node_path.join)(lockPath, `.recover-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
4326
4509
|
let moved = false;
|
|
@@ -4356,7 +4539,7 @@ async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
|
4356
4539
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
4357
4540
|
return true;
|
|
4358
4541
|
}
|
|
4359
|
-
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
4542
|
+
async function isAbandonedOwner(observed, ownerPath, liveness, staleAfterMs) {
|
|
4360
4543
|
let owner;
|
|
4361
4544
|
try {
|
|
4362
4545
|
owner = JSON.parse(observed);
|
|
@@ -4364,7 +4547,7 @@ async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
|
4364
4547
|
owner = null;
|
|
4365
4548
|
}
|
|
4366
4549
|
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
4367
|
-
return isStaleOwner(owner, staleAfterMs);
|
|
4550
|
+
return isStaleOwner(owner, liveness, staleAfterMs);
|
|
4368
4551
|
}
|
|
4369
4552
|
const modifiedAt = await (0, import_promises.stat)(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
4370
4553
|
return Date.now() - modifiedAt >= staleAfterMs;
|
|
@@ -4386,14 +4569,14 @@ async function waitForTestBarrier(environmentName, profile) {
|
|
|
4386
4569
|
}
|
|
4387
4570
|
}
|
|
4388
4571
|
}
|
|
4389
|
-
function isStaleOwner(owner, staleAfterMs) {
|
|
4572
|
+
async function isStaleOwner(owner, liveness, staleAfterMs) {
|
|
4390
4573
|
const now = Date.now();
|
|
4391
4574
|
const createdAt = typeof owner?.createdAt === "string" ? Date.parse(owner.createdAt) : Number.NaN;
|
|
4392
4575
|
if (!Number.isFinite(createdAt) || now - createdAt < staleAfterMs) return false;
|
|
4393
4576
|
if (typeof owner?.pid !== "number" || !Number.isInteger(owner.pid) || owner.pid <= 0) {
|
|
4394
4577
|
return false;
|
|
4395
4578
|
}
|
|
4396
|
-
return !
|
|
4579
|
+
return !await isRecordedProcessAlive(owner, liveness);
|
|
4397
4580
|
}
|
|
4398
4581
|
async function exists(path) {
|
|
4399
4582
|
try {
|
|
@@ -4404,13 +4587,22 @@ async function exists(path) {
|
|
|
4404
4587
|
throw error;
|
|
4405
4588
|
}
|
|
4406
4589
|
}
|
|
4407
|
-
function
|
|
4590
|
+
async function isRecordedProcessAlive(owner, acquisition) {
|
|
4591
|
+
const pid = owner.pid;
|
|
4592
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return true;
|
|
4408
4593
|
try {
|
|
4409
4594
|
process.kill(pid, 0);
|
|
4410
|
-
return true;
|
|
4411
4595
|
} catch (error) {
|
|
4412
4596
|
return !isErrno(error, "ESRCH");
|
|
4413
4597
|
}
|
|
4598
|
+
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;
|
|
4599
|
+
const origin = processOrigin();
|
|
4600
|
+
if (!origin || owner.bootId !== origin.bootId || typeof owner.pidNamespace !== "string" || !/^pid:\[[0-9]+\]$/.test(owner.pidNamespace)) return true;
|
|
4601
|
+
if (owner.pidNamespace === origin.pidNamespace) {
|
|
4602
|
+
return typeof owner.processStartTime !== "string" || !/^[0-9]+$/.test(owner.processStartTime) || owner.processStartTime === origin.processStartTime;
|
|
4603
|
+
}
|
|
4604
|
+
const liveness = parseLiveness(owner.liveness);
|
|
4605
|
+
return liveness === null || await probeLiveness(liveness, acquisition) === "unknown";
|
|
4414
4606
|
}
|
|
4415
4607
|
function validateProfileName(profile) {
|
|
4416
4608
|
if (!profile || profile === "." || profile === ".." || profile.includes("/") || profile.includes("\\") || profile.includes("\0")) {
|