@tinycloud/operations 0.3.4-beta.6 → 0.4.0-beta.8
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 +416 -50
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.d.cts +115 -58
- package/dist/artifacts.d.ts +115 -58
- package/dist/artifacts.js +417 -50
- package/dist/artifacts.js.map +1 -1
- package/dist/authority-ofwbEqmj.d.cts +30 -0
- package/dist/authority-ofwbEqmj.d.ts +30 -0
- package/dist/cli-runtime.cjs +652 -120
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +645 -116
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +7956 -0
- package/dist/delegation-binding.cjs.map +1 -0
- package/dist/delegation-binding.d.cts +130 -0
- package/dist/delegation-binding.d.ts +130 -0
- package/dist/delegation-binding.js +7948 -0
- package/dist/delegation-binding.js.map +1 -0
- package/dist/index.cjs +649 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +645 -116
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +9 -2
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +11 -2
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +426 -50
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +38 -5
- package/dist/state.d.ts +38 -5
- package/dist/state.js +424 -50
- package/dist/state.js.map +1 -1
- package/generated/operations.json +8 -4
- package/package.json +8 -3
package/dist/artifacts.js
CHANGED
|
@@ -6921,12 +6921,14 @@ import {
|
|
|
6921
6921
|
chmod,
|
|
6922
6922
|
link,
|
|
6923
6923
|
mkdir,
|
|
6924
|
+
open,
|
|
6924
6925
|
readFile,
|
|
6925
6926
|
readdir,
|
|
6926
6927
|
rename,
|
|
6927
6928
|
rmdir,
|
|
6928
6929
|
rm,
|
|
6929
6930
|
stat,
|
|
6931
|
+
unlink,
|
|
6930
6932
|
writeFile
|
|
6931
6933
|
} from "fs/promises";
|
|
6932
6934
|
import { homedir } from "os";
|
|
@@ -6943,17 +6945,33 @@ var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
|
6943
6945
|
var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
6944
6946
|
var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
6945
6947
|
var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
6946
|
-
var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
|
|
6947
6948
|
var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
6948
|
-
var
|
|
6949
|
-
var
|
|
6949
|
+
var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
|
|
6950
|
+
var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
6951
|
+
var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
6952
|
+
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
6953
|
+
function processWideContext(key) {
|
|
6954
|
+
var _a2;
|
|
6955
|
+
const registry = globalThis;
|
|
6956
|
+
return registry[_a2 = Symbol.for(key)] ?? (registry[_a2] = new AsyncLocalStorage());
|
|
6957
|
+
}
|
|
6958
|
+
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
6959
|
+
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
6950
6960
|
var ProfileLockTimeoutError = class extends Error {
|
|
6951
|
-
|
|
6952
|
-
|
|
6961
|
+
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
6962
|
+
constructor(profile, timeoutMs, detail) {
|
|
6963
|
+
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.${detail ? ` ${detail}` : ""}`);
|
|
6953
6964
|
this.code = "PROFILE_LOCK_TIMEOUT";
|
|
6954
6965
|
this.name = "ProfileLockTimeoutError";
|
|
6955
6966
|
}
|
|
6956
6967
|
};
|
|
6968
|
+
var ProfileDeletedError = class extends Error {
|
|
6969
|
+
constructor(profile) {
|
|
6970
|
+
super(`Profile "${profile}" was deleted while this change waited for its lock, so the change was not written.`);
|
|
6971
|
+
this.code = "PROFILE_NOT_FOUND";
|
|
6972
|
+
this.name = "ProfileDeletedError";
|
|
6973
|
+
}
|
|
6974
|
+
};
|
|
6957
6975
|
function tinycloudHomePath() {
|
|
6958
6976
|
const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
6959
6977
|
return join(home, ".tinycloud");
|
|
@@ -6964,6 +6982,9 @@ function profilesPath() {
|
|
|
6964
6982
|
function profilePath(profile) {
|
|
6965
6983
|
return join(profilesPath(), validateProfileName(profile));
|
|
6966
6984
|
}
|
|
6985
|
+
function profileConfigPath(profile) {
|
|
6986
|
+
return join(profilePath(profile), "profile.json");
|
|
6987
|
+
}
|
|
6967
6988
|
function profileStorePath(profile, store) {
|
|
6968
6989
|
return join(profilePath(profile), `${store}.json`);
|
|
6969
6990
|
}
|
|
@@ -6973,6 +6994,9 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
6973
6994
|
function profileLockPath(profile) {
|
|
6974
6995
|
return join(profilePath(profile), ".lock");
|
|
6975
6996
|
}
|
|
6997
|
+
function profileTurnLockPath(profile) {
|
|
6998
|
+
return join(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
|
|
6999
|
+
}
|
|
6976
7000
|
async function readJson(filePath) {
|
|
6977
7001
|
try {
|
|
6978
7002
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
@@ -6982,6 +7006,9 @@ async function readJson(filePath) {
|
|
|
6982
7006
|
}
|
|
6983
7007
|
}
|
|
6984
7008
|
async function writeJsonAtomic(filePath, value) {
|
|
7009
|
+
await replaceJson(filePath, value, false);
|
|
7010
|
+
}
|
|
7011
|
+
async function replaceJson(filePath, value, durable) {
|
|
6985
7012
|
const directory = dirname(filePath);
|
|
6986
7013
|
const temporaryPath = join(
|
|
6987
7014
|
directory,
|
|
@@ -6991,13 +7018,25 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
6991
7018
|
`;
|
|
6992
7019
|
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
6993
7020
|
try {
|
|
6994
|
-
await
|
|
7021
|
+
if (durable) await writeFileDurably(temporaryPath, contents);
|
|
7022
|
+
else await writeFile(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
6995
7023
|
await rename(temporaryPath, filePath);
|
|
6996
7024
|
} catch (error) {
|
|
6997
7025
|
await rm(temporaryPath, { force: true }).catch(() => void 0);
|
|
6998
7026
|
throw error;
|
|
6999
7027
|
}
|
|
7000
7028
|
}
|
|
7029
|
+
async function writeFileDurably(path, contents) {
|
|
7030
|
+
const handle = await open(path, "wx", PRIVATE_FILE_MODE);
|
|
7031
|
+
try {
|
|
7032
|
+
if (contents) {
|
|
7033
|
+
await handle.writeFile(contents, "utf8");
|
|
7034
|
+
await handle.datasync();
|
|
7035
|
+
}
|
|
7036
|
+
} finally {
|
|
7037
|
+
await handle.close();
|
|
7038
|
+
}
|
|
7039
|
+
}
|
|
7001
7040
|
async function readAuthRequests(profile) {
|
|
7002
7041
|
return (await readProfileStore(profile, "auth-requests")).records;
|
|
7003
7042
|
}
|
|
@@ -7024,15 +7063,28 @@ async function withProfileLock(profile, action, options = {}) {
|
|
|
7024
7063
|
const lockPath = profileLockPath(normalizedProfile);
|
|
7025
7064
|
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
7026
7065
|
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
7027
|
-
const
|
|
7028
|
-
const ownership = {
|
|
7066
|
+
const acquired = await acquireProfileLock(normalizedProfile, options);
|
|
7067
|
+
const ownership = {
|
|
7068
|
+
lockPath,
|
|
7069
|
+
turnSlot: acquired.turnSlot,
|
|
7070
|
+
deletedWhileWaiting: acquired.deletedWhileWaiting,
|
|
7071
|
+
active: true
|
|
7072
|
+
};
|
|
7029
7073
|
try {
|
|
7030
7074
|
return await heldProfileLocks.run([...held, ownership], action);
|
|
7031
7075
|
} finally {
|
|
7032
7076
|
ownership.active = false;
|
|
7033
|
-
await release();
|
|
7077
|
+
await acquired.release();
|
|
7078
|
+
if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
|
|
7034
7079
|
}
|
|
7035
7080
|
}
|
|
7081
|
+
async function refuseWriteToDeletedProfile(profile) {
|
|
7082
|
+
const lockPath = profileLockPath(profile);
|
|
7083
|
+
const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
|
|
7084
|
+
if (!ownership?.deletedWhileWaiting) return;
|
|
7085
|
+
if (await exists(profileConfigPath(profile))) return;
|
|
7086
|
+
throw new ProfileDeletedError(profile);
|
|
7087
|
+
}
|
|
7036
7088
|
async function updateProfileStore(profile, store, update, options = {}) {
|
|
7037
7089
|
return withProfileLock(
|
|
7038
7090
|
profile,
|
|
@@ -7041,6 +7093,7 @@ async function updateProfileStore(profile, store, update, options = {}) {
|
|
|
7041
7093
|
);
|
|
7042
7094
|
}
|
|
7043
7095
|
async function updateProfileStoreWhileLocked(profile, store, update) {
|
|
7096
|
+
await refuseWriteToDeletedProfile(profile);
|
|
7044
7097
|
const current = await readProfileStore(profile, store);
|
|
7045
7098
|
const next = await update(current.records);
|
|
7046
7099
|
await writeJsonAtomic(profileStorePath(profile, store), next.records);
|
|
@@ -7051,32 +7104,277 @@ async function writeFormatOneMetadata(profile, store) {
|
|
|
7051
7104
|
await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
|
|
7052
7105
|
}
|
|
7053
7106
|
async function acquireProfileLock(profile, options) {
|
|
7054
|
-
const
|
|
7055
|
-
|
|
7107
|
+
const deadline = {
|
|
7108
|
+
startedAt: Date.now(),
|
|
7109
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS,
|
|
7110
|
+
retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
|
|
7111
|
+
};
|
|
7056
7112
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7057
|
-
const startedAt = Date.now();
|
|
7058
7113
|
const lockPath = profileLockPath(profile);
|
|
7114
|
+
const turnDirectory = profileTurnLockPath(profile);
|
|
7059
7115
|
const directory = profilePath(profile);
|
|
7060
7116
|
await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7117
|
+
await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7061
7118
|
for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
|
|
7062
7119
|
await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
|
|
7063
7120
|
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
7064
7121
|
});
|
|
7065
7122
|
}
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7123
|
+
const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
|
|
7124
|
+
try {
|
|
7125
|
+
const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
|
|
7126
|
+
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
7127
|
+
while (true) {
|
|
7128
|
+
const token = randomUUID();
|
|
7129
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7130
|
+
return {
|
|
7131
|
+
deletedWhileWaiting,
|
|
7132
|
+
turnSlot: turn.slot,
|
|
7133
|
+
release: async () => {
|
|
7134
|
+
try {
|
|
7135
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7136
|
+
} finally {
|
|
7137
|
+
await releaseTurn(profile, turn);
|
|
7138
|
+
}
|
|
7139
|
+
}
|
|
7140
|
+
};
|
|
7141
|
+
}
|
|
7142
|
+
await signalTestLockContention(profile);
|
|
7143
|
+
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7144
|
+
await waitOrTimeOut(profile, deadline);
|
|
7072
7145
|
}
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7146
|
+
} catch (error) {
|
|
7147
|
+
await releaseTurn(profile, turn);
|
|
7148
|
+
throw error;
|
|
7149
|
+
}
|
|
7150
|
+
}
|
|
7151
|
+
function checkDeadline(profile, deadline, detail) {
|
|
7152
|
+
if (Date.now() - deadline.startedAt >= deadline.timeoutMs) {
|
|
7153
|
+
throw new ProfileLockTimeoutError(profile, deadline.timeoutMs, detail);
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
7156
|
+
async function waitOrTimeOut(profile, deadline, detail) {
|
|
7157
|
+
checkDeadline(profile, deadline, detail);
|
|
7158
|
+
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
7159
|
+
}
|
|
7160
|
+
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
7161
|
+
var _a;
|
|
7162
|
+
var relinquishedTurns = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Set());
|
|
7163
|
+
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
7164
|
+
var SETTLE_RETRY_MS = 1e3;
|
|
7165
|
+
var SETTLE_RETRIES = 300;
|
|
7166
|
+
async function acquireTurn(profile, directory, deadline) {
|
|
7167
|
+
let firstWaitedTurn;
|
|
7168
|
+
let problem;
|
|
7169
|
+
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.`;
|
|
7170
|
+
for (let first = true; ; first = false) {
|
|
7171
|
+
if (!first) checkDeadline(profile, deadline, problem);
|
|
7172
|
+
problem = void 0;
|
|
7173
|
+
const latest = await readLatestTurn(directory);
|
|
7174
|
+
if (latest === "missing") {
|
|
7175
|
+
await createTurnDirectory(directory);
|
|
7176
|
+
continue;
|
|
7177
|
+
}
|
|
7178
|
+
if (latest === "empty") {
|
|
7179
|
+
problem = damaged("has no turns");
|
|
7180
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
7181
|
+
continue;
|
|
7182
|
+
}
|
|
7183
|
+
if (latest === null) continue;
|
|
7184
|
+
if (typeof latest === "number") {
|
|
7185
|
+
problem = damaged(`has an unreadable turn ${latest}`);
|
|
7186
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
7187
|
+
continue;
|
|
7188
|
+
}
|
|
7189
|
+
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
7190
|
+
if (latest.held && latest.done) {
|
|
7191
|
+
const turn = await takeTurn(profile, directory, latest);
|
|
7192
|
+
if (turn !== null) return { turn, firstWaitedTurn };
|
|
7193
|
+
await waitOrTimeOut(profile, deadline);
|
|
7194
|
+
} else if (latest.done) {
|
|
7195
|
+
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
7196
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
7197
|
+
} else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
|
|
7198
|
+
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
7199
|
+
await settleAbandonedTurn(directory, latest);
|
|
7200
|
+
relinquishedTurns.delete(latest.token);
|
|
7201
|
+
} else {
|
|
7202
|
+
await signalTestLockContention(profile);
|
|
7203
|
+
await waitOrTimeOut(profile, deadline);
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
}
|
|
7207
|
+
async function readLatestTurn(directory) {
|
|
7208
|
+
let names;
|
|
7209
|
+
try {
|
|
7210
|
+
names = await readdir(directory);
|
|
7211
|
+
} catch (error) {
|
|
7212
|
+
if (isErrno(error, "ENOENT")) return "missing";
|
|
7213
|
+
throw error;
|
|
7214
|
+
}
|
|
7215
|
+
const slots = names.filter((name) => TURN.test(name)).map(Number);
|
|
7216
|
+
if (slots.length === 0) return "empty";
|
|
7217
|
+
const slot = Math.max(...slots);
|
|
7218
|
+
const turn = await readTurn(directory, slot);
|
|
7219
|
+
return turn === "damaged" ? slot : turn;
|
|
7220
|
+
}
|
|
7221
|
+
async function readTurn(directory, slot) {
|
|
7222
|
+
const path = join(directory, String(slot));
|
|
7223
|
+
let text;
|
|
7224
|
+
try {
|
|
7225
|
+
text = await readFile(join(path, "owner.json"), "utf8");
|
|
7226
|
+
} catch (error) {
|
|
7227
|
+
if (!isErrno(error, "ENOENT") && !isErrno(error, "ENOTDIR")) throw error;
|
|
7228
|
+
return await exists(path) ? "damaged" : null;
|
|
7229
|
+
}
|
|
7230
|
+
let owner;
|
|
7231
|
+
try {
|
|
7232
|
+
owner = JSON.parse(text);
|
|
7233
|
+
} catch {
|
|
7234
|
+
return "damaged";
|
|
7235
|
+
}
|
|
7236
|
+
if (owner === null || typeof owner !== "object" || typeof owner.token !== "string" || typeof owner.pid !== "number") {
|
|
7237
|
+
return "damaged";
|
|
7238
|
+
}
|
|
7239
|
+
const done = await exists(join(path, `${owner.token}.done`));
|
|
7240
|
+
const held = await exists(join(path, `${owner.token}.held`));
|
|
7241
|
+
return {
|
|
7242
|
+
slot,
|
|
7243
|
+
token: owner.token,
|
|
7244
|
+
pid: owner.pid,
|
|
7245
|
+
after: typeof owner.after === "string" ? owner.after : null,
|
|
7246
|
+
held,
|
|
7247
|
+
done
|
|
7248
|
+
};
|
|
7249
|
+
}
|
|
7250
|
+
async function isFinishedTurn(directory, slot, token) {
|
|
7251
|
+
const turn = await readTurn(directory, slot);
|
|
7252
|
+
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
7253
|
+
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
7254
|
+
}
|
|
7255
|
+
async function takeTurn(profile, directory, latest) {
|
|
7256
|
+
const token = randomUUID();
|
|
7257
|
+
const slot = latest.slot + 1;
|
|
7258
|
+
const path = join(directory, String(slot));
|
|
7259
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
7260
|
+
let published = false;
|
|
7261
|
+
try {
|
|
7262
|
+
published = await publishDirectory(
|
|
7263
|
+
join(directory, `.stage-${process.pid}-${token}`),
|
|
7264
|
+
path,
|
|
7265
|
+
{ "owner.json": `${JSON.stringify(owner, null, 2)}
|
|
7266
|
+
` },
|
|
7267
|
+
() => waitForTestBarrier(TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, profile)
|
|
7268
|
+
);
|
|
7269
|
+
if (published && await isFinishedTurn(directory, latest.slot, latest.token) && await markTurn(path, token, "held")) {
|
|
7270
|
+
return { directory, slot, token };
|
|
7271
|
+
}
|
|
7272
|
+
if (published) await markTurn(path, token, "done");
|
|
7273
|
+
return null;
|
|
7274
|
+
} catch (error) {
|
|
7275
|
+
if (published) settleOwnTurn(directory, slot, token);
|
|
7276
|
+
throw error;
|
|
7277
|
+
}
|
|
7278
|
+
}
|
|
7279
|
+
async function settleAbandonedTurn(directory, abandoned) {
|
|
7280
|
+
const path = join(directory, String(abandoned.slot));
|
|
7281
|
+
if (!abandoned.held && abandoned.after !== null && await isFinishedTurn(directory, abandoned.slot - 1, abandoned.after)) {
|
|
7282
|
+
await markTurn(path, abandoned.token, "held");
|
|
7283
|
+
}
|
|
7284
|
+
await markTurn(path, abandoned.token, "done");
|
|
7285
|
+
}
|
|
7286
|
+
function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
|
|
7287
|
+
relinquishedTurns.add(token);
|
|
7288
|
+
void (async () => {
|
|
7289
|
+
try {
|
|
7290
|
+
const turn = await readTurn(directory, slot);
|
|
7291
|
+
if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
|
|
7292
|
+
relinquishedTurns.delete(token);
|
|
7293
|
+
} catch {
|
|
7294
|
+
if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
|
|
7295
|
+
}
|
|
7296
|
+
})();
|
|
7297
|
+
}
|
|
7298
|
+
async function markTurn(path, token, kind) {
|
|
7299
|
+
for (let attempt = 0; ; attempt++) {
|
|
7300
|
+
try {
|
|
7301
|
+
await writeFile(join(path, `${token}.${kind}`), "", { mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7302
|
+
return true;
|
|
7303
|
+
} catch (error) {
|
|
7304
|
+
if (isErrno(error, "EEXIST")) return true;
|
|
7305
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
7306
|
+
if (attempt >= MARKER_RETRY_DELAYS_MS.length) throw error;
|
|
7307
|
+
await sleep(MARKER_RETRY_DELAYS_MS[attempt]);
|
|
7308
|
+
}
|
|
7309
|
+
}
|
|
7310
|
+
}
|
|
7311
|
+
async function createTurnDirectory(directory) {
|
|
7312
|
+
const token = randomUUID();
|
|
7313
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
7314
|
+
await publishDirectory(join(dirname(directory), `.stage-${basename(directory)}-${process.pid}-${token}`), directory, {
|
|
7315
|
+
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
7316
|
+
`,
|
|
7317
|
+
[`0/${token}.held`]: "",
|
|
7318
|
+
[`0/${token}.done`]: ""
|
|
7319
|
+
});
|
|
7320
|
+
}
|
|
7321
|
+
async function publishDirectory(stage, target, files, beforeRename) {
|
|
7322
|
+
try {
|
|
7323
|
+
for (const [name, contents] of Object.entries(files)) {
|
|
7324
|
+
await mkdir(dirname(join(stage, name)), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7325
|
+
await writeFileDurably(join(stage, name), contents);
|
|
7326
|
+
}
|
|
7327
|
+
await beforeRename?.();
|
|
7328
|
+
await rename(stage, target);
|
|
7329
|
+
return true;
|
|
7330
|
+
} catch (error) {
|
|
7331
|
+
if (["ENOTEMPTY", "EEXIST", "ENOENT"].some((code) => isErrno(error, code)) || await exists(target)) return false;
|
|
7332
|
+
throw error;
|
|
7333
|
+
} finally {
|
|
7334
|
+
await rm(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
async function releaseTurn(profile, turn) {
|
|
7338
|
+
try {
|
|
7339
|
+
await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
|
|
7340
|
+
} catch {
|
|
7341
|
+
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
7342
|
+
return;
|
|
7343
|
+
}
|
|
7344
|
+
await collectTurns(profile, turn).catch(() => void 0);
|
|
7345
|
+
}
|
|
7346
|
+
function isAbandonedTurnEntry(name) {
|
|
7347
|
+
if (name.startsWith(".trash-")) return true;
|
|
7348
|
+
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
7349
|
+
return writer !== void 0 && !isProcessAlive(Number(writer));
|
|
7350
|
+
}
|
|
7351
|
+
function isAbandonedTurnDirectoryStage(name) {
|
|
7352
|
+
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];
|
|
7353
|
+
return writer !== void 0 && !isProcessAlive(Number(writer));
|
|
7354
|
+
}
|
|
7355
|
+
async function collectTurns(profile, turn) {
|
|
7356
|
+
for (const [directory, abandoned] of [
|
|
7357
|
+
[turn.directory, isAbandonedTurnEntry],
|
|
7358
|
+
[dirname(turn.directory), isAbandonedTurnDirectoryStage]
|
|
7359
|
+
]) {
|
|
7360
|
+
for (const name of (await readdir(directory)).filter(abandoned)) {
|
|
7361
|
+
await rm(join(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
7362
|
+
}
|
|
7363
|
+
}
|
|
7364
|
+
const older = (await readdir(turn.directory)).filter((name) => TURN.test(name)).map(Number).filter((slot) => slot < turn.slot).sort((left, right) => left - right);
|
|
7365
|
+
for (const slot of older) {
|
|
7366
|
+
const candidate = await readTurn(turn.directory, slot);
|
|
7367
|
+
if (candidate === null || candidate === "damaged" || !candidate.done) continue;
|
|
7368
|
+
if (candidate.held) {
|
|
7369
|
+
if (await exists(join(turn.directory, String(slot - 1)))) continue;
|
|
7370
|
+
const next = await readTurn(turn.directory, slot + 1);
|
|
7371
|
+
if (next === null || next === "damaged" || !next.held || !next.done) continue;
|
|
7372
|
+
}
|
|
7373
|
+
await waitForTestBarrier(TEST_LOCK_TURN_COLLECT_BARRIER_DIR, profile);
|
|
7374
|
+
const trash = join(turn.directory, `.trash-${randomUUID()}`);
|
|
7375
|
+
if (await rename(join(turn.directory, String(slot)), trash).then(() => true, () => false)) {
|
|
7376
|
+
await rm(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
7078
7377
|
}
|
|
7079
|
-
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7080
7378
|
}
|
|
7081
7379
|
}
|
|
7082
7380
|
async function publishProfileLock(profile, lockPath, token) {
|
|
@@ -7089,12 +7387,18 @@ async function publishProfileLock(profile, lockPath, token) {
|
|
|
7089
7387
|
return false;
|
|
7090
7388
|
}
|
|
7091
7389
|
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
7390
|
+
const ownerPath = join(lockPath, "owner.json");
|
|
7092
7391
|
const staged = join(dirname(lockPath), `.lock-owner-${token}.tmp`);
|
|
7093
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }
|
|
7392
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
7393
|
+
`;
|
|
7094
7394
|
try {
|
|
7095
|
-
await writeFile(staged,
|
|
7096
|
-
|
|
7097
|
-
|
|
7395
|
+
await writeFile(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7396
|
+
try {
|
|
7397
|
+
await link(staged, ownerPath);
|
|
7398
|
+
} catch (error) {
|
|
7399
|
+
if (!lacksHardLinks(error)) throw error;
|
|
7400
|
+
await writeFile(ownerPath, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7401
|
+
}
|
|
7098
7402
|
return true;
|
|
7099
7403
|
} catch (error) {
|
|
7100
7404
|
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
@@ -7104,6 +7408,10 @@ async function publishProfileLock(profile, lockPath, token) {
|
|
|
7104
7408
|
await rm(staged, { force: true }).catch(() => void 0);
|
|
7105
7409
|
}
|
|
7106
7410
|
}
|
|
7411
|
+
var NO_HARD_LINK_ERRORS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EISDIR", "EMLINK"]);
|
|
7412
|
+
function lacksHardLinks(error) {
|
|
7413
|
+
return typeof error === "object" && error !== null && "code" in error && NO_HARD_LINK_ERRORS.has(String(error.code));
|
|
7414
|
+
}
|
|
7107
7415
|
async function releaseProfileLock(profile, lockPath, token) {
|
|
7108
7416
|
const ownerPath = join(lockPath, "owner.json");
|
|
7109
7417
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
@@ -7127,9 +7435,10 @@ async function signalTestLockContention(profile) {
|
|
|
7127
7435
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7128
7436
|
}
|
|
7129
7437
|
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
7438
|
+
let aged;
|
|
7130
7439
|
try {
|
|
7131
7440
|
const { mtimeMs } = await stat(lockPath);
|
|
7132
|
-
|
|
7441
|
+
aged = Date.now() - mtimeMs >= staleAfterMs;
|
|
7133
7442
|
} catch {
|
|
7134
7443
|
return false;
|
|
7135
7444
|
}
|
|
@@ -7138,7 +7447,16 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
7138
7447
|
try {
|
|
7139
7448
|
const entries = await readdir(lockPath);
|
|
7140
7449
|
if (entries.includes("owner.json")) return false;
|
|
7141
|
-
|
|
7450
|
+
const onlyOrphanedClaims = entries.length > 0 && entries.every((entry) => RECOVERY_CLAIM.test(entry));
|
|
7451
|
+
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
7452
|
+
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
7453
|
+
const claimPath = join(lockPath, name);
|
|
7454
|
+
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
7455
|
+
if (!await exists(join(lockPath, "owner.json"))) await rename(claimPath, join(lockPath, "owner.json"));
|
|
7456
|
+
return false;
|
|
7457
|
+
}
|
|
7458
|
+
await rm(claimPath, { force: true });
|
|
7459
|
+
}
|
|
7142
7460
|
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
7143
7461
|
await rm(join(lockPath, name), { force: true });
|
|
7144
7462
|
}
|
|
@@ -7149,10 +7467,28 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
7149
7467
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7150
7468
|
return true;
|
|
7151
7469
|
}
|
|
7470
|
+
async function hasLiveRecoveryOwner(claimPath) {
|
|
7471
|
+
let contents;
|
|
7472
|
+
try {
|
|
7473
|
+
contents = await readFile(claimPath, "utf8");
|
|
7474
|
+
} catch (error) {
|
|
7475
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
7476
|
+
throw error;
|
|
7477
|
+
}
|
|
7478
|
+
let owner;
|
|
7479
|
+
try {
|
|
7480
|
+
owner = JSON.parse(contents);
|
|
7481
|
+
} catch {
|
|
7482
|
+
return false;
|
|
7483
|
+
}
|
|
7484
|
+
const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
|
|
7485
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
|
|
7486
|
+
}
|
|
7152
7487
|
function recoveryFenced(since, staleAfterMs) {
|
|
7153
7488
|
return performance.now() - since >= staleAfterMs / 2;
|
|
7154
7489
|
}
|
|
7155
7490
|
var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7491
|
+
var RECOVERY_CLAIM = /^\.recover-[0-9a-f-]+\.json$/;
|
|
7156
7492
|
var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7157
7493
|
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
7158
7494
|
const directory = dirname(lockPath);
|
|
@@ -7165,37 +7501,57 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
|
7165
7501
|
}
|
|
7166
7502
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7167
7503
|
const ownerPath = join(lockPath, "owner.json");
|
|
7168
|
-
const
|
|
7169
|
-
if (
|
|
7170
|
-
if (!
|
|
7171
|
-
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7504
|
+
const observed = await readFile(ownerPath, "utf8").catch(() => null);
|
|
7505
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7506
|
+
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
7172
7507
|
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7173
|
-
const claimPath = join(lockPath, `.
|
|
7508
|
+
const claimPath = join(lockPath, `.recover-${randomUUID()}.json`);
|
|
7509
|
+
let moved = false;
|
|
7174
7510
|
try {
|
|
7175
7511
|
await link(ownerPath, claimPath);
|
|
7176
|
-
} catch {
|
|
7177
|
-
return false;
|
|
7512
|
+
} catch (error) {
|
|
7513
|
+
if (!lacksHardLinks(error)) return false;
|
|
7514
|
+
try {
|
|
7515
|
+
await rename(ownerPath, claimPath);
|
|
7516
|
+
moved = true;
|
|
7517
|
+
} catch {
|
|
7518
|
+
return false;
|
|
7519
|
+
}
|
|
7178
7520
|
}
|
|
7179
|
-
|
|
7180
|
-
await
|
|
7181
|
-
const claimed = await readJson(claimPath).catch(() => null);
|
|
7182
|
-
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7521
|
+
if (moved) await waitForTestBarrier(TEST_LOCK_MOVED_BARRIER_DIR, profile);
|
|
7522
|
+
const sameInstance = await readFile(claimPath, "utf8").then((claimed) => claimed === observed, () => false);
|
|
7183
7523
|
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
7184
|
-
if (sameInstance
|
|
7185
|
-
|
|
7186
|
-
|
|
7524
|
+
if (!sameInstance) {
|
|
7525
|
+
if (moved) {
|
|
7526
|
+
await rename(claimPath, ownerPath).catch(() => void 0);
|
|
7527
|
+
} else {
|
|
7528
|
+
await unlink(claimPath).catch(() => void 0);
|
|
7529
|
+
}
|
|
7187
7530
|
return false;
|
|
7188
7531
|
}
|
|
7189
|
-
if (
|
|
7190
|
-
|
|
7191
|
-
|
|
7532
|
+
if (!moved) await rm(ownerPath, { force: true });
|
|
7533
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7534
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7535
|
+
for (const name of (await readdir(lockPath).catch(() => [])).filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
7536
|
+
if (join(lockPath, name) !== claimPath) await rm(join(lockPath, name), { force: true });
|
|
7192
7537
|
}
|
|
7193
|
-
await
|
|
7194
|
-
await rmdir(lockPath).catch(() => void 0);
|
|
7195
|
-
if (!sameInstance) return false;
|
|
7538
|
+
if (await unlink(claimPath).then(() => true, () => false)) await rmdir(lockPath).catch(() => void 0);
|
|
7196
7539
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7197
7540
|
return true;
|
|
7198
7541
|
}
|
|
7542
|
+
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
7543
|
+
let owner;
|
|
7544
|
+
try {
|
|
7545
|
+
owner = JSON.parse(observed);
|
|
7546
|
+
} catch {
|
|
7547
|
+
owner = null;
|
|
7548
|
+
}
|
|
7549
|
+
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
7550
|
+
return isStaleOwner(owner, staleAfterMs);
|
|
7551
|
+
}
|
|
7552
|
+
const modifiedAt = await stat(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
7553
|
+
return Date.now() - modifiedAt >= staleAfterMs;
|
|
7554
|
+
}
|
|
7199
7555
|
async function waitForTestBarrier(environmentName, profile) {
|
|
7200
7556
|
if (process.env.NODE_ENV !== "test") return;
|
|
7201
7557
|
const barrierDirectory = process.env[environmentName];
|
|
@@ -7222,6 +7578,15 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7222
7578
|
}
|
|
7223
7579
|
return !isProcessAlive(owner.pid);
|
|
7224
7580
|
}
|
|
7581
|
+
async function exists(path) {
|
|
7582
|
+
try {
|
|
7583
|
+
await stat(path);
|
|
7584
|
+
return true;
|
|
7585
|
+
} catch (error) {
|
|
7586
|
+
if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR")) return false;
|
|
7587
|
+
throw error;
|
|
7588
|
+
}
|
|
7589
|
+
}
|
|
7225
7590
|
function isProcessAlive(pid) {
|
|
7226
7591
|
try {
|
|
7227
7592
|
process.kill(pid, 0);
|
|
@@ -7310,6 +7675,7 @@ var DelegationImportArtifactSchema = z2.object({
|
|
|
7310
7675
|
expiry: z2.string().datetime({ offset: true }).optional(),
|
|
7311
7676
|
prompted: z2.boolean().optional()
|
|
7312
7677
|
}).strict();
|
|
7678
|
+
var DelegationRequestBindingSchema = PermissionRequestArtifactSchema.pick({ requestId: true, requested: true }).strict();
|
|
7313
7679
|
function validatePermissionRequestArtifact(value) {
|
|
7314
7680
|
return PermissionRequestArtifactSchema.parse(value);
|
|
7315
7681
|
}
|
|
@@ -7522,6 +7888,7 @@ function defaultRequestId(now) {
|
|
|
7522
7888
|
}
|
|
7523
7889
|
export {
|
|
7524
7890
|
DelegationImportArtifactSchema,
|
|
7891
|
+
DelegationRequestBindingSchema,
|
|
7525
7892
|
PermissionEntrySchema,
|
|
7526
7893
|
PermissionRequestArtifactSchema,
|
|
7527
7894
|
buildPermissionRequestArtifact,
|