@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.cjs
CHANGED
|
@@ -188,6 +188,7 @@ var require_decrypt_transport_response_error = __commonJS({
|
|
|
188
188
|
var artifacts_exports = {};
|
|
189
189
|
__export(artifacts_exports, {
|
|
190
190
|
DelegationImportArtifactSchema: () => DelegationImportArtifactSchema,
|
|
191
|
+
DelegationRequestBindingSchema: () => DelegationRequestBindingSchema,
|
|
191
192
|
PermissionEntrySchema: () => PermissionEntrySchema,
|
|
192
193
|
PermissionRequestArtifactSchema: () => PermissionRequestArtifactSchema,
|
|
193
194
|
buildPermissionRequestArtifact: () => buildPermissionRequestArtifact,
|
|
@@ -6956,17 +6957,33 @@ var TEST_LOCK_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_PUBLISH_BARRIER_DIR";
|
|
|
6956
6957
|
var TEST_LOCK_RELEASE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_RELEASE_BARRIER_DIR";
|
|
6957
6958
|
var TEST_LOCK_CLAIM_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIM_BARRIER_DIR";
|
|
6958
6959
|
var TEST_LOCK_CLAIMED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_CLAIMED_BARRIER_DIR";
|
|
6959
|
-
var TEST_LOCK_FENCED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_FENCED_BARRIER_DIR";
|
|
6960
6960
|
var TEST_LOCK_VERIFIED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_VERIFIED_BARRIER_DIR";
|
|
6961
|
-
var
|
|
6962
|
-
var
|
|
6961
|
+
var TEST_LOCK_MOVED_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_MOVED_BARRIER_DIR";
|
|
6962
|
+
var TEST_LOCK_TURN_PUBLISH_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_PUBLISH_BARRIER_DIR";
|
|
6963
|
+
var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIER_DIR";
|
|
6964
|
+
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
6965
|
+
function processWideContext(key) {
|
|
6966
|
+
var _a2;
|
|
6967
|
+
const registry = globalThis;
|
|
6968
|
+
return registry[_a2 = Symbol.for(key)] ?? (registry[_a2] = new import_node_async_hooks.AsyncLocalStorage());
|
|
6969
|
+
}
|
|
6970
|
+
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
6971
|
+
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
6963
6972
|
var ProfileLockTimeoutError = class extends Error {
|
|
6964
|
-
|
|
6965
|
-
|
|
6973
|
+
/** `detail` names the cause when it is not simply another process holding the lock. */
|
|
6974
|
+
constructor(profile, timeoutMs, detail) {
|
|
6975
|
+
super(`Timed out waiting for the profile lock for "${profile}" after ${timeoutMs}ms.${detail ? ` ${detail}` : ""}`);
|
|
6966
6976
|
this.code = "PROFILE_LOCK_TIMEOUT";
|
|
6967
6977
|
this.name = "ProfileLockTimeoutError";
|
|
6968
6978
|
}
|
|
6969
6979
|
};
|
|
6980
|
+
var ProfileDeletedError = class extends Error {
|
|
6981
|
+
constructor(profile) {
|
|
6982
|
+
super(`Profile "${profile}" was deleted while this change waited for its lock, so the change was not written.`);
|
|
6983
|
+
this.code = "PROFILE_NOT_FOUND";
|
|
6984
|
+
this.name = "ProfileDeletedError";
|
|
6985
|
+
}
|
|
6986
|
+
};
|
|
6970
6987
|
function tinycloudHomePath() {
|
|
6971
6988
|
const home = invocationStateRoot.getStore() ?? process.env.TC_HOME ?? process.env.HOME ?? process.env.USERPROFILE ?? (0, import_node_os.homedir)();
|
|
6972
6989
|
return (0, import_node_path.join)(home, ".tinycloud");
|
|
@@ -6977,6 +6994,9 @@ function profilesPath() {
|
|
|
6977
6994
|
function profilePath(profile) {
|
|
6978
6995
|
return (0, import_node_path.join)(profilesPath(), validateProfileName(profile));
|
|
6979
6996
|
}
|
|
6997
|
+
function profileConfigPath(profile) {
|
|
6998
|
+
return (0, import_node_path.join)(profilePath(profile), "profile.json");
|
|
6999
|
+
}
|
|
6980
7000
|
function profileStorePath(profile, store) {
|
|
6981
7001
|
return (0, import_node_path.join)(profilePath(profile), `${store}.json`);
|
|
6982
7002
|
}
|
|
@@ -6986,6 +7006,9 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
6986
7006
|
function profileLockPath(profile) {
|
|
6987
7007
|
return (0, import_node_path.join)(profilePath(profile), ".lock");
|
|
6988
7008
|
}
|
|
7009
|
+
function profileTurnLockPath(profile) {
|
|
7010
|
+
return (0, import_node_path.join)(tinycloudHomePath(), "profile-locks", validateProfileName(profile));
|
|
7011
|
+
}
|
|
6989
7012
|
async function readJson(filePath) {
|
|
6990
7013
|
try {
|
|
6991
7014
|
return JSON.parse(await (0, import_promises.readFile)(filePath, "utf8"));
|
|
@@ -6995,6 +7018,9 @@ async function readJson(filePath) {
|
|
|
6995
7018
|
}
|
|
6996
7019
|
}
|
|
6997
7020
|
async function writeJsonAtomic(filePath, value) {
|
|
7021
|
+
await replaceJson(filePath, value, false);
|
|
7022
|
+
}
|
|
7023
|
+
async function replaceJson(filePath, value, durable) {
|
|
6998
7024
|
const directory = (0, import_node_path.dirname)(filePath);
|
|
6999
7025
|
const temporaryPath = (0, import_node_path.join)(
|
|
7000
7026
|
directory,
|
|
@@ -7004,13 +7030,25 @@ async function writeJsonAtomic(filePath, value) {
|
|
|
7004
7030
|
`;
|
|
7005
7031
|
await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7006
7032
|
try {
|
|
7007
|
-
|
|
7033
|
+
if (durable) await writeFileDurably(temporaryPath, contents);
|
|
7034
|
+
else await (0, import_promises.writeFile)(temporaryPath, contents, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
7008
7035
|
await (0, import_promises.rename)(temporaryPath, filePath);
|
|
7009
7036
|
} catch (error) {
|
|
7010
7037
|
await (0, import_promises.rm)(temporaryPath, { force: true }).catch(() => void 0);
|
|
7011
7038
|
throw error;
|
|
7012
7039
|
}
|
|
7013
7040
|
}
|
|
7041
|
+
async function writeFileDurably(path, contents) {
|
|
7042
|
+
const handle = await (0, import_promises.open)(path, "wx", PRIVATE_FILE_MODE);
|
|
7043
|
+
try {
|
|
7044
|
+
if (contents) {
|
|
7045
|
+
await handle.writeFile(contents, "utf8");
|
|
7046
|
+
await handle.datasync();
|
|
7047
|
+
}
|
|
7048
|
+
} finally {
|
|
7049
|
+
await handle.close();
|
|
7050
|
+
}
|
|
7051
|
+
}
|
|
7014
7052
|
async function readAuthRequests(profile) {
|
|
7015
7053
|
return (await readProfileStore(profile, "auth-requests")).records;
|
|
7016
7054
|
}
|
|
@@ -7037,15 +7075,28 @@ async function withProfileLock(profile, action, options = {}) {
|
|
|
7037
7075
|
const lockPath = profileLockPath(normalizedProfile);
|
|
7038
7076
|
const held = (heldProfileLocks.getStore() ?? []).filter((ownership2) => ownership2.active);
|
|
7039
7077
|
if (held.some((ownership2) => ownership2.lockPath === lockPath)) return action();
|
|
7040
|
-
const
|
|
7041
|
-
const ownership = {
|
|
7078
|
+
const acquired = await acquireProfileLock(normalizedProfile, options);
|
|
7079
|
+
const ownership = {
|
|
7080
|
+
lockPath,
|
|
7081
|
+
turnSlot: acquired.turnSlot,
|
|
7082
|
+
deletedWhileWaiting: acquired.deletedWhileWaiting,
|
|
7083
|
+
active: true
|
|
7084
|
+
};
|
|
7042
7085
|
try {
|
|
7043
7086
|
return await heldProfileLocks.run([...held, ownership], action);
|
|
7044
7087
|
} finally {
|
|
7045
7088
|
ownership.active = false;
|
|
7046
|
-
await release();
|
|
7089
|
+
await acquired.release();
|
|
7090
|
+
if (acquired.deletedWhileWaiting) await (0, import_promises.rmdir)(profilePath(normalizedProfile)).catch(() => void 0);
|
|
7047
7091
|
}
|
|
7048
7092
|
}
|
|
7093
|
+
async function refuseWriteToDeletedProfile(profile) {
|
|
7094
|
+
const lockPath = profileLockPath(profile);
|
|
7095
|
+
const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
|
|
7096
|
+
if (!ownership?.deletedWhileWaiting) return;
|
|
7097
|
+
if (await exists(profileConfigPath(profile))) return;
|
|
7098
|
+
throw new ProfileDeletedError(profile);
|
|
7099
|
+
}
|
|
7049
7100
|
async function updateProfileStore(profile, store, update, options = {}) {
|
|
7050
7101
|
return withProfileLock(
|
|
7051
7102
|
profile,
|
|
@@ -7054,6 +7105,7 @@ async function updateProfileStore(profile, store, update, options = {}) {
|
|
|
7054
7105
|
);
|
|
7055
7106
|
}
|
|
7056
7107
|
async function updateProfileStoreWhileLocked(profile, store, update) {
|
|
7108
|
+
await refuseWriteToDeletedProfile(profile);
|
|
7057
7109
|
const current = await readProfileStore(profile, store);
|
|
7058
7110
|
const next = await update(current.records);
|
|
7059
7111
|
await writeJsonAtomic(profileStorePath(profile, store), next.records);
|
|
@@ -7064,32 +7116,277 @@ async function writeFormatOneMetadata(profile, store) {
|
|
|
7064
7116
|
await writeJsonAtomic(profileStoreMetadataPath(profile, store), { formatVersion: 1 });
|
|
7065
7117
|
}
|
|
7066
7118
|
async function acquireProfileLock(profile, options) {
|
|
7067
|
-
const
|
|
7068
|
-
|
|
7119
|
+
const deadline = {
|
|
7120
|
+
startedAt: Date.now(),
|
|
7121
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS,
|
|
7122
|
+
retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
|
|
7123
|
+
};
|
|
7069
7124
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
7070
|
-
const startedAt = Date.now();
|
|
7071
7125
|
const lockPath = profileLockPath(profile);
|
|
7126
|
+
const turnDirectory = profileTurnLockPath(profile);
|
|
7072
7127
|
const directory = profilePath(profile);
|
|
7073
7128
|
await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7129
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7074
7130
|
for (const path of [tinycloudHomePath(), profilesPath(), directory]) {
|
|
7075
7131
|
await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
|
|
7076
7132
|
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
7077
7133
|
});
|
|
7078
7134
|
}
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7135
|
+
const { turn, firstWaitedTurn } = await acquireTurn(profile, turnDirectory, deadline);
|
|
7136
|
+
try {
|
|
7137
|
+
const deletion = await readJson((0, import_node_path.join)(turnDirectory, "deleted.json")).catch(() => null);
|
|
7138
|
+
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
7139
|
+
while (true) {
|
|
7140
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7141
|
+
if (await publishProfileLock(profile, lockPath, token)) {
|
|
7142
|
+
return {
|
|
7143
|
+
deletedWhileWaiting,
|
|
7144
|
+
turnSlot: turn.slot,
|
|
7145
|
+
release: async () => {
|
|
7146
|
+
try {
|
|
7147
|
+
await releaseProfileLock(profile, lockPath, token);
|
|
7148
|
+
} finally {
|
|
7149
|
+
await releaseTurn(profile, turn);
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
};
|
|
7153
|
+
}
|
|
7154
|
+
await signalTestLockContention(profile);
|
|
7155
|
+
if (await recoverStaleLock(profile, lockPath, staleAfterMs)) continue;
|
|
7156
|
+
await waitOrTimeOut(profile, deadline);
|
|
7085
7157
|
}
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7158
|
+
} catch (error) {
|
|
7159
|
+
await releaseTurn(profile, turn);
|
|
7160
|
+
throw error;
|
|
7161
|
+
}
|
|
7162
|
+
}
|
|
7163
|
+
function checkDeadline(profile, deadline, detail) {
|
|
7164
|
+
if (Date.now() - deadline.startedAt >= deadline.timeoutMs) {
|
|
7165
|
+
throw new ProfileLockTimeoutError(profile, deadline.timeoutMs, detail);
|
|
7166
|
+
}
|
|
7167
|
+
}
|
|
7168
|
+
async function waitOrTimeOut(profile, deadline, detail) {
|
|
7169
|
+
checkDeadline(profile, deadline, detail);
|
|
7170
|
+
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
7171
|
+
}
|
|
7172
|
+
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
7173
|
+
var _a;
|
|
7174
|
+
var relinquishedTurns = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Set());
|
|
7175
|
+
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
7176
|
+
var SETTLE_RETRY_MS = 1e3;
|
|
7177
|
+
var SETTLE_RETRIES = 300;
|
|
7178
|
+
async function acquireTurn(profile, directory, deadline) {
|
|
7179
|
+
let firstWaitedTurn;
|
|
7180
|
+
let problem;
|
|
7181
|
+
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.`;
|
|
7182
|
+
for (let first = true; ; first = false) {
|
|
7183
|
+
if (!first) checkDeadline(profile, deadline, problem);
|
|
7184
|
+
problem = void 0;
|
|
7185
|
+
const latest = await readLatestTurn(directory);
|
|
7186
|
+
if (latest === "missing") {
|
|
7187
|
+
await createTurnDirectory(directory);
|
|
7188
|
+
continue;
|
|
7189
|
+
}
|
|
7190
|
+
if (latest === "empty") {
|
|
7191
|
+
problem = damaged("has no turns");
|
|
7192
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
7193
|
+
continue;
|
|
7194
|
+
}
|
|
7195
|
+
if (latest === null) continue;
|
|
7196
|
+
if (typeof latest === "number") {
|
|
7197
|
+
problem = damaged(`has an unreadable turn ${latest}`);
|
|
7198
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
7199
|
+
continue;
|
|
7200
|
+
}
|
|
7201
|
+
firstWaitedTurn ?? (firstWaitedTurn = latest.done ? latest.slot + 1 : latest.slot);
|
|
7202
|
+
if (latest.held && latest.done) {
|
|
7203
|
+
const turn = await takeTurn(profile, directory, latest);
|
|
7204
|
+
if (turn !== null) return { turn, firstWaitedTurn };
|
|
7205
|
+
await waitOrTimeOut(profile, deadline);
|
|
7206
|
+
} else if (latest.done) {
|
|
7207
|
+
problem = damaged(`ends with turn ${latest.slot}, which was never granted`);
|
|
7208
|
+
await waitOrTimeOut(profile, deadline, problem);
|
|
7209
|
+
} else if (latest.pid === process.pid ? relinquishedTurns.has(latest.token) : !isProcessAlive(latest.pid)) {
|
|
7210
|
+
await waitForTestBarrier(TEST_LOCK_TURN_SETTLE_BARRIER_DIR, profile);
|
|
7211
|
+
await settleAbandonedTurn(directory, latest);
|
|
7212
|
+
relinquishedTurns.delete(latest.token);
|
|
7213
|
+
} else {
|
|
7214
|
+
await signalTestLockContention(profile);
|
|
7215
|
+
await waitOrTimeOut(profile, deadline);
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
}
|
|
7219
|
+
async function readLatestTurn(directory) {
|
|
7220
|
+
let names;
|
|
7221
|
+
try {
|
|
7222
|
+
names = await (0, import_promises.readdir)(directory);
|
|
7223
|
+
} catch (error) {
|
|
7224
|
+
if (isErrno(error, "ENOENT")) return "missing";
|
|
7225
|
+
throw error;
|
|
7226
|
+
}
|
|
7227
|
+
const slots = names.filter((name) => TURN.test(name)).map(Number);
|
|
7228
|
+
if (slots.length === 0) return "empty";
|
|
7229
|
+
const slot = Math.max(...slots);
|
|
7230
|
+
const turn = await readTurn(directory, slot);
|
|
7231
|
+
return turn === "damaged" ? slot : turn;
|
|
7232
|
+
}
|
|
7233
|
+
async function readTurn(directory, slot) {
|
|
7234
|
+
const path = (0, import_node_path.join)(directory, String(slot));
|
|
7235
|
+
let text;
|
|
7236
|
+
try {
|
|
7237
|
+
text = await (0, import_promises.readFile)((0, import_node_path.join)(path, "owner.json"), "utf8");
|
|
7238
|
+
} catch (error) {
|
|
7239
|
+
if (!isErrno(error, "ENOENT") && !isErrno(error, "ENOTDIR")) throw error;
|
|
7240
|
+
return await exists(path) ? "damaged" : null;
|
|
7241
|
+
}
|
|
7242
|
+
let owner;
|
|
7243
|
+
try {
|
|
7244
|
+
owner = JSON.parse(text);
|
|
7245
|
+
} catch {
|
|
7246
|
+
return "damaged";
|
|
7247
|
+
}
|
|
7248
|
+
if (owner === null || typeof owner !== "object" || typeof owner.token !== "string" || typeof owner.pid !== "number") {
|
|
7249
|
+
return "damaged";
|
|
7250
|
+
}
|
|
7251
|
+
const done = await exists((0, import_node_path.join)(path, `${owner.token}.done`));
|
|
7252
|
+
const held = await exists((0, import_node_path.join)(path, `${owner.token}.held`));
|
|
7253
|
+
return {
|
|
7254
|
+
slot,
|
|
7255
|
+
token: owner.token,
|
|
7256
|
+
pid: owner.pid,
|
|
7257
|
+
after: typeof owner.after === "string" ? owner.after : null,
|
|
7258
|
+
held,
|
|
7259
|
+
done
|
|
7260
|
+
};
|
|
7261
|
+
}
|
|
7262
|
+
async function isFinishedTurn(directory, slot, token) {
|
|
7263
|
+
const turn = await readTurn(directory, slot);
|
|
7264
|
+
if (turn === "damaged") throw new Error(`Turn ${slot} of the turn lock ${directory} is unreadable.`);
|
|
7265
|
+
return turn !== null && turn.token === token && turn.held && turn.done;
|
|
7266
|
+
}
|
|
7267
|
+
async function takeTurn(profile, directory, latest) {
|
|
7268
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7269
|
+
const slot = latest.slot + 1;
|
|
7270
|
+
const path = (0, import_node_path.join)(directory, String(slot));
|
|
7271
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: latest.token };
|
|
7272
|
+
let published = false;
|
|
7273
|
+
try {
|
|
7274
|
+
published = await publishDirectory(
|
|
7275
|
+
(0, import_node_path.join)(directory, `.stage-${process.pid}-${token}`),
|
|
7276
|
+
path,
|
|
7277
|
+
{ "owner.json": `${JSON.stringify(owner, null, 2)}
|
|
7278
|
+
` },
|
|
7279
|
+
() => waitForTestBarrier(TEST_LOCK_TURN_PUBLISH_BARRIER_DIR, profile)
|
|
7280
|
+
);
|
|
7281
|
+
if (published && await isFinishedTurn(directory, latest.slot, latest.token) && await markTurn(path, token, "held")) {
|
|
7282
|
+
return { directory, slot, token };
|
|
7283
|
+
}
|
|
7284
|
+
if (published) await markTurn(path, token, "done");
|
|
7285
|
+
return null;
|
|
7286
|
+
} catch (error) {
|
|
7287
|
+
if (published) settleOwnTurn(directory, slot, token);
|
|
7288
|
+
throw error;
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
async function settleAbandonedTurn(directory, abandoned) {
|
|
7292
|
+
const path = (0, import_node_path.join)(directory, String(abandoned.slot));
|
|
7293
|
+
if (!abandoned.held && abandoned.after !== null && await isFinishedTurn(directory, abandoned.slot - 1, abandoned.after)) {
|
|
7294
|
+
await markTurn(path, abandoned.token, "held");
|
|
7295
|
+
}
|
|
7296
|
+
await markTurn(path, abandoned.token, "done");
|
|
7297
|
+
}
|
|
7298
|
+
function settleOwnTurn(directory, slot, token, retries = SETTLE_RETRIES) {
|
|
7299
|
+
relinquishedTurns.add(token);
|
|
7300
|
+
void (async () => {
|
|
7301
|
+
try {
|
|
7302
|
+
const turn = await readTurn(directory, slot);
|
|
7303
|
+
if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(directory, turn);
|
|
7304
|
+
relinquishedTurns.delete(token);
|
|
7305
|
+
} catch {
|
|
7306
|
+
if (retries > 0) setTimeout(() => settleOwnTurn(directory, slot, token, retries - 1), SETTLE_RETRY_MS).unref();
|
|
7307
|
+
}
|
|
7308
|
+
})();
|
|
7309
|
+
}
|
|
7310
|
+
async function markTurn(path, token, kind) {
|
|
7311
|
+
for (let attempt = 0; ; attempt++) {
|
|
7312
|
+
try {
|
|
7313
|
+
await (0, import_promises.writeFile)((0, import_node_path.join)(path, `${token}.${kind}`), "", { mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7314
|
+
return true;
|
|
7315
|
+
} catch (error) {
|
|
7316
|
+
if (isErrno(error, "EEXIST")) return true;
|
|
7317
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
7318
|
+
if (attempt >= MARKER_RETRY_DELAYS_MS.length) throw error;
|
|
7319
|
+
await sleep(MARKER_RETRY_DELAYS_MS[attempt]);
|
|
7320
|
+
}
|
|
7321
|
+
}
|
|
7322
|
+
}
|
|
7323
|
+
async function createTurnDirectory(directory) {
|
|
7324
|
+
const token = (0, import_node_crypto.randomUUID)();
|
|
7325
|
+
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token, after: null };
|
|
7326
|
+
await publishDirectory((0, import_node_path.join)((0, import_node_path.dirname)(directory), `.stage-${(0, import_node_path.basename)(directory)}-${process.pid}-${token}`), directory, {
|
|
7327
|
+
"0/owner.json": `${JSON.stringify(owner, null, 2)}
|
|
7328
|
+
`,
|
|
7329
|
+
[`0/${token}.held`]: "",
|
|
7330
|
+
[`0/${token}.done`]: ""
|
|
7331
|
+
});
|
|
7332
|
+
}
|
|
7333
|
+
async function publishDirectory(stage, target, files, beforeRename) {
|
|
7334
|
+
try {
|
|
7335
|
+
for (const [name, contents] of Object.entries(files)) {
|
|
7336
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)((0, import_node_path.join)(stage, name)), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
7337
|
+
await writeFileDurably((0, import_node_path.join)(stage, name), contents);
|
|
7338
|
+
}
|
|
7339
|
+
await beforeRename?.();
|
|
7340
|
+
await (0, import_promises.rename)(stage, target);
|
|
7341
|
+
return true;
|
|
7342
|
+
} catch (error) {
|
|
7343
|
+
if (["ENOTEMPTY", "EEXIST", "ENOENT"].some((code) => isErrno(error, code)) || await exists(target)) return false;
|
|
7344
|
+
throw error;
|
|
7345
|
+
} finally {
|
|
7346
|
+
await (0, import_promises.rm)(stage, { recursive: true, force: true }).catch(() => void 0);
|
|
7347
|
+
}
|
|
7348
|
+
}
|
|
7349
|
+
async function releaseTurn(profile, turn) {
|
|
7350
|
+
try {
|
|
7351
|
+
await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
|
|
7352
|
+
} catch {
|
|
7353
|
+
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
7354
|
+
return;
|
|
7355
|
+
}
|
|
7356
|
+
await collectTurns(profile, turn).catch(() => void 0);
|
|
7357
|
+
}
|
|
7358
|
+
function isAbandonedTurnEntry(name) {
|
|
7359
|
+
if (name.startsWith(".trash-")) return true;
|
|
7360
|
+
const writer = /^\.(?:stage|deleted\.json)[-.]([0-9]+)[-.]/.exec(name)?.[1];
|
|
7361
|
+
return writer !== void 0 && !isProcessAlive(Number(writer));
|
|
7362
|
+
}
|
|
7363
|
+
function isAbandonedTurnDirectoryStage(name) {
|
|
7364
|
+
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];
|
|
7365
|
+
return writer !== void 0 && !isProcessAlive(Number(writer));
|
|
7366
|
+
}
|
|
7367
|
+
async function collectTurns(profile, turn) {
|
|
7368
|
+
for (const [directory, abandoned] of [
|
|
7369
|
+
[turn.directory, isAbandonedTurnEntry],
|
|
7370
|
+
[(0, import_node_path.dirname)(turn.directory), isAbandonedTurnDirectoryStage]
|
|
7371
|
+
]) {
|
|
7372
|
+
for (const name of (await (0, import_promises.readdir)(directory)).filter(abandoned)) {
|
|
7373
|
+
await (0, import_promises.rm)((0, import_node_path.join)(directory, name), { recursive: true, force: true }).catch(() => void 0);
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
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);
|
|
7377
|
+
for (const slot of older) {
|
|
7378
|
+
const candidate = await readTurn(turn.directory, slot);
|
|
7379
|
+
if (candidate === null || candidate === "damaged" || !candidate.done) continue;
|
|
7380
|
+
if (candidate.held) {
|
|
7381
|
+
if (await exists((0, import_node_path.join)(turn.directory, String(slot - 1)))) continue;
|
|
7382
|
+
const next = await readTurn(turn.directory, slot + 1);
|
|
7383
|
+
if (next === null || next === "damaged" || !next.held || !next.done) continue;
|
|
7384
|
+
}
|
|
7385
|
+
await waitForTestBarrier(TEST_LOCK_TURN_COLLECT_BARRIER_DIR, profile);
|
|
7386
|
+
const trash = (0, import_node_path.join)(turn.directory, `.trash-${(0, import_node_crypto.randomUUID)()}`);
|
|
7387
|
+
if (await (0, import_promises.rename)((0, import_node_path.join)(turn.directory, String(slot)), trash).then(() => true, () => false)) {
|
|
7388
|
+
await (0, import_promises.rm)(trash, { recursive: true, force: true }).catch(() => void 0);
|
|
7091
7389
|
}
|
|
7092
|
-
await sleep(Math.min(retryMs, timeoutMs - elapsedMs));
|
|
7093
7390
|
}
|
|
7094
7391
|
}
|
|
7095
7392
|
async function publishProfileLock(profile, lockPath, token) {
|
|
@@ -7102,12 +7399,18 @@ async function publishProfileLock(profile, lockPath, token) {
|
|
|
7102
7399
|
return false;
|
|
7103
7400
|
}
|
|
7104
7401
|
await waitForTestBarrier(TEST_LOCK_PUBLISH_BARRIER_DIR, profile);
|
|
7402
|
+
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7105
7403
|
const staged = (0, import_node_path.join)((0, import_node_path.dirname)(lockPath), `.lock-owner-${token}.tmp`);
|
|
7106
|
-
const owner = { pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }
|
|
7404
|
+
const owner = `${JSON.stringify({ pid: process.pid, createdAt: (/* @__PURE__ */ new Date()).toISOString(), token }, null, 2)}
|
|
7405
|
+
`;
|
|
7107
7406
|
try {
|
|
7108
|
-
await (0, import_promises.writeFile)(staged,
|
|
7109
|
-
|
|
7110
|
-
|
|
7407
|
+
await (0, import_promises.writeFile)(staged, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7408
|
+
try {
|
|
7409
|
+
await (0, import_promises.link)(staged, ownerPath);
|
|
7410
|
+
} catch (error) {
|
|
7411
|
+
if (!lacksHardLinks(error)) throw error;
|
|
7412
|
+
await (0, import_promises.writeFile)(ownerPath, owner, { encoding: "utf8", mode: PRIVATE_FILE_MODE, flag: "wx" });
|
|
7413
|
+
}
|
|
7111
7414
|
return true;
|
|
7112
7415
|
} catch (error) {
|
|
7113
7416
|
if (isErrno(error, "EEXIST") || isErrno(error, "ENOENT")) return false;
|
|
@@ -7117,6 +7420,10 @@ async function publishProfileLock(profile, lockPath, token) {
|
|
|
7117
7420
|
await (0, import_promises.rm)(staged, { force: true }).catch(() => void 0);
|
|
7118
7421
|
}
|
|
7119
7422
|
}
|
|
7423
|
+
var NO_HARD_LINK_ERRORS = /* @__PURE__ */ new Set(["EPERM", "ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EISDIR", "EMLINK"]);
|
|
7424
|
+
function lacksHardLinks(error) {
|
|
7425
|
+
return typeof error === "object" && error !== null && "code" in error && NO_HARD_LINK_ERRORS.has(String(error.code));
|
|
7426
|
+
}
|
|
7120
7427
|
async function releaseProfileLock(profile, lockPath, token) {
|
|
7121
7428
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7122
7429
|
const owner = await readJson(ownerPath).catch(() => null);
|
|
@@ -7140,9 +7447,10 @@ async function signalTestLockContention(profile) {
|
|
|
7140
7447
|
`, { encoding: "utf8", flag: "wx" }).catch(() => void 0);
|
|
7141
7448
|
}
|
|
7142
7449
|
async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
7450
|
+
let aged;
|
|
7143
7451
|
try {
|
|
7144
7452
|
const { mtimeMs } = await (0, import_promises.stat)(lockPath);
|
|
7145
|
-
|
|
7453
|
+
aged = Date.now() - mtimeMs >= staleAfterMs;
|
|
7146
7454
|
} catch {
|
|
7147
7455
|
return false;
|
|
7148
7456
|
}
|
|
@@ -7151,7 +7459,16 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
7151
7459
|
try {
|
|
7152
7460
|
const entries = await (0, import_promises.readdir)(lockPath);
|
|
7153
7461
|
if (entries.includes("owner.json")) return false;
|
|
7154
|
-
|
|
7462
|
+
const onlyOrphanedClaims = entries.length > 0 && entries.every((entry) => RECOVERY_CLAIM.test(entry));
|
|
7463
|
+
if (!onlyOrphanedClaims && (!aged || recoveryFenced(checkedAt, staleAfterMs))) return false;
|
|
7464
|
+
for (const name of entries.filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
7465
|
+
const claimPath = (0, import_node_path.join)(lockPath, name);
|
|
7466
|
+
if (await hasLiveRecoveryOwner(claimPath)) {
|
|
7467
|
+
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"));
|
|
7468
|
+
return false;
|
|
7469
|
+
}
|
|
7470
|
+
await (0, import_promises.rm)(claimPath, { force: true });
|
|
7471
|
+
}
|
|
7155
7472
|
for (const name of entries.filter((entry) => ABANDONED_CLAIM.test(entry))) {
|
|
7156
7473
|
await (0, import_promises.rm)((0, import_node_path.join)(lockPath, name), { force: true });
|
|
7157
7474
|
}
|
|
@@ -7162,10 +7479,28 @@ async function recoverOwnerlessLock(profile, lockPath, staleAfterMs) {
|
|
|
7162
7479
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7163
7480
|
return true;
|
|
7164
7481
|
}
|
|
7482
|
+
async function hasLiveRecoveryOwner(claimPath) {
|
|
7483
|
+
let contents;
|
|
7484
|
+
try {
|
|
7485
|
+
contents = await (0, import_promises.readFile)(claimPath, "utf8");
|
|
7486
|
+
} catch (error) {
|
|
7487
|
+
if (isErrno(error, "ENOENT")) return false;
|
|
7488
|
+
throw error;
|
|
7489
|
+
}
|
|
7490
|
+
let owner;
|
|
7491
|
+
try {
|
|
7492
|
+
owner = JSON.parse(contents);
|
|
7493
|
+
} catch {
|
|
7494
|
+
return false;
|
|
7495
|
+
}
|
|
7496
|
+
const pid = typeof owner === "object" && owner !== null && "pid" in owner ? owner.pid : null;
|
|
7497
|
+
return typeof pid === "number" && Number.isInteger(pid) && pid > 0 && isProcessAlive(pid);
|
|
7498
|
+
}
|
|
7165
7499
|
function recoveryFenced(since, staleAfterMs) {
|
|
7166
7500
|
return performance.now() - since >= staleAfterMs / 2;
|
|
7167
7501
|
}
|
|
7168
7502
|
var ABANDONED_CLAIM = /^\.(?:release|stale)-[0-9a-f-]+\.json$/;
|
|
7503
|
+
var RECOVERY_CLAIM = /^\.recover-[0-9a-f-]+\.json$/;
|
|
7169
7504
|
var STAGED_OWNER = /^\.lock-owner-[0-9a-f-]+\.tmp$/;
|
|
7170
7505
|
async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
7171
7506
|
const directory = (0, import_node_path.dirname)(lockPath);
|
|
@@ -7178,37 +7513,57 @@ async function removeAgedOwnerFiles(lockPath, staleAfterMs) {
|
|
|
7178
7513
|
}
|
|
7179
7514
|
async function recoverStaleLock(profile, lockPath, staleAfterMs) {
|
|
7180
7515
|
const ownerPath = (0, import_node_path.join)(lockPath, "owner.json");
|
|
7181
|
-
const
|
|
7182
|
-
if (
|
|
7183
|
-
if (!
|
|
7184
|
-
const observedToken = typeof owner?.token === "string" && owner.token.length > 0 ? owner.token : "legacy";
|
|
7516
|
+
const observed = await (0, import_promises.readFile)(ownerPath, "utf8").catch(() => null);
|
|
7517
|
+
if (observed === null) return recoverOwnerlessLock(profile, lockPath, staleAfterMs);
|
|
7518
|
+
if (!await isAbandonedOwner(observed, ownerPath, staleAfterMs)) return false;
|
|
7185
7519
|
await waitForTestBarrier(TEST_LOCK_RECOVERY_BARRIER_DIR, profile);
|
|
7186
|
-
const claimPath = (0, import_node_path.join)(lockPath, `.
|
|
7520
|
+
const claimPath = (0, import_node_path.join)(lockPath, `.recover-${(0, import_node_crypto.randomUUID)()}.json`);
|
|
7521
|
+
let moved = false;
|
|
7187
7522
|
try {
|
|
7188
7523
|
await (0, import_promises.link)(ownerPath, claimPath);
|
|
7189
|
-
} catch {
|
|
7190
|
-
return false;
|
|
7524
|
+
} catch (error) {
|
|
7525
|
+
if (!lacksHardLinks(error)) return false;
|
|
7526
|
+
try {
|
|
7527
|
+
await (0, import_promises.rename)(ownerPath, claimPath);
|
|
7528
|
+
moved = true;
|
|
7529
|
+
} catch {
|
|
7530
|
+
return false;
|
|
7531
|
+
}
|
|
7191
7532
|
}
|
|
7192
|
-
|
|
7193
|
-
await
|
|
7194
|
-
const claimed = await readJson(claimPath).catch(() => null);
|
|
7195
|
-
const sameInstance = claimed !== null && (observedToken === "legacy" ? claimed.token === void 0 : claimed.token === observedToken);
|
|
7533
|
+
if (moved) await waitForTestBarrier(TEST_LOCK_MOVED_BARRIER_DIR, profile);
|
|
7534
|
+
const sameInstance = await (0, import_promises.readFile)(claimPath, "utf8").then((claimed) => claimed === observed, () => false);
|
|
7196
7535
|
await waitForTestBarrier(TEST_LOCK_VERIFIED_BARRIER_DIR, profile);
|
|
7197
|
-
if (sameInstance
|
|
7198
|
-
|
|
7199
|
-
|
|
7536
|
+
if (!sameInstance) {
|
|
7537
|
+
if (moved) {
|
|
7538
|
+
await (0, import_promises.rename)(claimPath, ownerPath).catch(() => void 0);
|
|
7539
|
+
} else {
|
|
7540
|
+
await (0, import_promises.unlink)(claimPath).catch(() => void 0);
|
|
7541
|
+
}
|
|
7200
7542
|
return false;
|
|
7201
7543
|
}
|
|
7202
|
-
if (
|
|
7203
|
-
|
|
7204
|
-
|
|
7544
|
+
if (!moved) await (0, import_promises.rm)(ownerPath, { force: true });
|
|
7545
|
+
await waitForTestBarrier(TEST_LOCK_CLAIM_BARRIER_DIR, profile);
|
|
7546
|
+
await waitForTestBarrier(TEST_LOCK_CLAIMED_BARRIER_DIR, profile);
|
|
7547
|
+
for (const name of (await (0, import_promises.readdir)(lockPath).catch(() => [])).filter((entry) => RECOVERY_CLAIM.test(entry))) {
|
|
7548
|
+
if ((0, import_node_path.join)(lockPath, name) !== claimPath) await (0, import_promises.rm)((0, import_node_path.join)(lockPath, name), { force: true });
|
|
7205
7549
|
}
|
|
7206
|
-
await (0, import_promises.
|
|
7207
|
-
await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7208
|
-
if (!sameInstance) return false;
|
|
7550
|
+
if (await (0, import_promises.unlink)(claimPath).then(() => true, () => false)) await (0, import_promises.rmdir)(lockPath).catch(() => void 0);
|
|
7209
7551
|
await removeAgedOwnerFiles(lockPath, staleAfterMs);
|
|
7210
7552
|
return true;
|
|
7211
7553
|
}
|
|
7554
|
+
async function isAbandonedOwner(observed, ownerPath, staleAfterMs) {
|
|
7555
|
+
let owner;
|
|
7556
|
+
try {
|
|
7557
|
+
owner = JSON.parse(observed);
|
|
7558
|
+
} catch {
|
|
7559
|
+
owner = null;
|
|
7560
|
+
}
|
|
7561
|
+
if (typeof owner === "object" && owner !== null && "pid" in owner) {
|
|
7562
|
+
return isStaleOwner(owner, staleAfterMs);
|
|
7563
|
+
}
|
|
7564
|
+
const modifiedAt = await (0, import_promises.stat)(ownerPath).then(({ mtimeMs }) => mtimeMs, () => Number.NaN);
|
|
7565
|
+
return Date.now() - modifiedAt >= staleAfterMs;
|
|
7566
|
+
}
|
|
7212
7567
|
async function waitForTestBarrier(environmentName, profile) {
|
|
7213
7568
|
if (process.env.NODE_ENV !== "test") return;
|
|
7214
7569
|
const barrierDirectory = process.env[environmentName];
|
|
@@ -7235,6 +7590,15 @@ function isStaleOwner(owner, staleAfterMs) {
|
|
|
7235
7590
|
}
|
|
7236
7591
|
return !isProcessAlive(owner.pid);
|
|
7237
7592
|
}
|
|
7593
|
+
async function exists(path) {
|
|
7594
|
+
try {
|
|
7595
|
+
await (0, import_promises.stat)(path);
|
|
7596
|
+
return true;
|
|
7597
|
+
} catch (error) {
|
|
7598
|
+
if (isErrno(error, "ENOENT") || isErrno(error, "ENOTDIR")) return false;
|
|
7599
|
+
throw error;
|
|
7600
|
+
}
|
|
7601
|
+
}
|
|
7238
7602
|
function isProcessAlive(pid) {
|
|
7239
7603
|
try {
|
|
7240
7604
|
process.kill(pid, 0);
|
|
@@ -7323,6 +7687,7 @@ var DelegationImportArtifactSchema = import_zod2.z.object({
|
|
|
7323
7687
|
expiry: import_zod2.z.string().datetime({ offset: true }).optional(),
|
|
7324
7688
|
prompted: import_zod2.z.boolean().optional()
|
|
7325
7689
|
}).strict();
|
|
7690
|
+
var DelegationRequestBindingSchema = PermissionRequestArtifactSchema.pick({ requestId: true, requested: true }).strict();
|
|
7326
7691
|
function validatePermissionRequestArtifact(value) {
|
|
7327
7692
|
return PermissionRequestArtifactSchema.parse(value);
|
|
7328
7693
|
}
|
|
@@ -7536,6 +7901,7 @@ function defaultRequestId(now) {
|
|
|
7536
7901
|
// Annotate the CommonJS export names for ESM import in node:
|
|
7537
7902
|
0 && (module.exports = {
|
|
7538
7903
|
DelegationImportArtifactSchema,
|
|
7904
|
+
DelegationRequestBindingSchema,
|
|
7539
7905
|
PermissionEntrySchema,
|
|
7540
7906
|
PermissionRequestArtifactSchema,
|
|
7541
7907
|
buildPermissionRequestArtifact,
|