@neottia/repository-store 0.2.0
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/LICENSE +21 -0
- package/README.md +76 -0
- package/binding.gyp +9 -0
- package/dist/errors.d.ts +43 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +69 -0
- package/dist/errors.js.map +1 -0
- package/dist/files.d.ts +19 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +113 -0
- package/dist/files.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/fault-injection.d.ts +13 -0
- package/dist/internal/fault-injection.d.ts.map +1 -0
- package/dist/internal/fault-injection.js +68 -0
- package/dist/internal/fault-injection.js.map +1 -0
- package/dist/internal/filesystem.d.ts +25 -0
- package/dist/internal/filesystem.d.ts.map +1 -0
- package/dist/internal/filesystem.js +199 -0
- package/dist/internal/filesystem.js.map +1 -0
- package/dist/internal/model.d.ts +43 -0
- package/dist/internal/model.d.ts.map +1 -0
- package/dist/internal/model.js +49 -0
- package/dist/internal/model.js.map +1 -0
- package/dist/internal/native-publication.d.ts +9 -0
- package/dist/internal/native-publication.d.ts.map +1 -0
- package/dist/internal/native-publication.js +65 -0
- package/dist/internal/native-publication.js.map +1 -0
- package/dist/internal/publication.d.ts +10 -0
- package/dist/internal/publication.d.ts.map +1 -0
- package/dist/internal/publication.js +223 -0
- package/dist/internal/publication.js.map +1 -0
- package/dist/lease.d.ts +24 -0
- package/dist/lease.d.ts.map +1 -0
- package/dist/lease.js +932 -0
- package/dist/lease.js.map +1 -0
- package/dist/limits.d.ts +15 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +14 -0
- package/dist/limits.js.map +1 -0
- package/dist/paths.d.ts +15 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +58 -0
- package/dist/paths.js.map +1 -0
- package/dist/revision.d.ts +4 -0
- package/dist/revision.d.ts.map +1 -0
- package/dist/revision.js +8 -0
- package/dist/revision.js.map +1 -0
- package/dist/sqlite/adapter.d.ts +48 -0
- package/dist/sqlite/adapter.d.ts.map +1 -0
- package/dist/sqlite/adapter.js +183 -0
- package/dist/sqlite/adapter.js.map +1 -0
- package/dist/sqlite/bun.d.ts +2 -0
- package/dist/sqlite/bun.d.ts.map +1 -0
- package/dist/sqlite/bun.js +7 -0
- package/dist/sqlite/bun.js.map +1 -0
- package/dist/sqlite/cache-publication.d.ts +6 -0
- package/dist/sqlite/cache-publication.d.ts.map +1 -0
- package/dist/sqlite/cache-publication.js +191 -0
- package/dist/sqlite/cache-publication.js.map +1 -0
- package/dist/sqlite/cache.d.ts +26 -0
- package/dist/sqlite/cache.d.ts.map +1 -0
- package/dist/sqlite/cache.js +527 -0
- package/dist/sqlite/cache.js.map +1 -0
- package/dist/sqlite/node.d.ts +2 -0
- package/dist/sqlite/node.d.ts.map +1 -0
- package/dist/sqlite/node.js +6 -0
- package/dist/sqlite/node.js.map +1 -0
- package/dist/sqlite/runtime.d.ts +4 -0
- package/dist/sqlite/runtime.d.ts.map +1 -0
- package/dist/sqlite/runtime.js +34 -0
- package/dist/sqlite/runtime.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/transaction/apply.d.ts +8 -0
- package/dist/transaction/apply.d.ts.map +1 -0
- package/dist/transaction/apply.js +273 -0
- package/dist/transaction/apply.js.map +1 -0
- package/dist/transaction/journal.d.ts +17 -0
- package/dist/transaction/journal.d.ts.map +1 -0
- package/dist/transaction/journal.js +207 -0
- package/dist/transaction/journal.js.map +1 -0
- package/dist/transaction/recovery.d.ts +6 -0
- package/dist/transaction/recovery.d.ts.map +1 -0
- package/dist/transaction/recovery.js +138 -0
- package/dist/transaction/recovery.js.map +1 -0
- package/dist/transaction/types.d.ts +45 -0
- package/dist/transaction/types.d.ts.map +1 -0
- package/dist/transaction/types.js +2 -0
- package/dist/transaction/types.js.map +1 -0
- package/native/rename_noreplace.cc +145 -0
- package/package.json +71 -0
- package/scripts/install-native.mjs +10 -0
package/dist/lease.js
ADDED
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomBytes } from 'node:crypto';
|
|
3
|
+
import { closeSync, constants as fsConstants, fstatSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
6
|
+
import { LeaseContentionError, PathSafetyError, RepositoryStoreConfigError, ResourceLimitError } from './errors.js';
|
|
7
|
+
import { emitFilesystemFault, emitLeaseFault } from './internal/fault-injection.js';
|
|
8
|
+
import { assertSafeRegular, captureDirectories, ensurePrivateDirectory, hasCode, readRegularFile, readRegularFileWithIdentity, revalidateDirectories, sameIdentity, syncDirectory, syncFileDescriptor, } from './internal/filesystem.js';
|
|
9
|
+
import { createRepositoryLease, getLeaseState, getRootState, } from './internal/model.js';
|
|
10
|
+
import { assertNativePublicationPath, nativeRenameNoReplace, requireNativePublicationBackend, } from './internal/native-publication.js';
|
|
11
|
+
const authorityContext = new AsyncLocalStorage();
|
|
12
|
+
const authorityQueueTails = new Map();
|
|
13
|
+
const DEFAULT_STALE_MS = 60_000;
|
|
14
|
+
const DEFAULT_POLL_MS = 20;
|
|
15
|
+
const DEFAULT_WAIT_MS = 10_000;
|
|
16
|
+
const MAX_LEASE_METADATA_BYTES = 4 * 1024;
|
|
17
|
+
export async function withRepositoryLease(root, operation, options = {}) {
|
|
18
|
+
const rootState = getRootState(root);
|
|
19
|
+
if (rootState === undefined)
|
|
20
|
+
throw new RepositoryStoreConfigError('Managed root is not a repository-store handle.');
|
|
21
|
+
const inherited = authorityContext.getStore()?.get(rootState.authorityId);
|
|
22
|
+
if (inherited?.active === true)
|
|
23
|
+
throw new LeaseContentionError('Repository lease acquisition is non-reentrant.', 'LEASE_REENTRANT');
|
|
24
|
+
checkControl(options);
|
|
25
|
+
const staleMs = positiveInteger(options.staleMs ?? DEFAULT_STALE_MS, 'staleMs');
|
|
26
|
+
const pollMs = positiveInteger(options.pollMs ?? DEFAULT_POLL_MS, 'pollMs');
|
|
27
|
+
const explicitDeadline = options.deadline;
|
|
28
|
+
const deadline = explicitDeadline ?? Date.now() + nonnegativeInteger(options.waitMs ?? DEFAULT_WAIT_MS, 'waitMs');
|
|
29
|
+
const turn = enqueueAuthority(rootState.authorityId);
|
|
30
|
+
try {
|
|
31
|
+
await waitForAuthorityTurn(turn.predecessor, deadline, explicitDeadline !== undefined, options.signal);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
void turn.predecessor.finally(turn.release);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
const controlRoot = join(rootState.authorityRoot, '.neottia', 'repository-store');
|
|
38
|
+
let acquired = false;
|
|
39
|
+
let lockIdentity;
|
|
40
|
+
let ownerIdentity;
|
|
41
|
+
let leaseState;
|
|
42
|
+
let lockPath = '';
|
|
43
|
+
let token = '';
|
|
44
|
+
try {
|
|
45
|
+
checkAcquisitionControl(deadline, explicitDeadline !== undefined, options.signal);
|
|
46
|
+
ensurePrivateDirectory(controlRoot);
|
|
47
|
+
const cleanupBackend = requireNativePublicationBackend();
|
|
48
|
+
assertNativePublicationPath(cleanupBackend, controlRoot);
|
|
49
|
+
const hostId = ensureHostId(controlRoot);
|
|
50
|
+
lockPath = join(controlRoot, 'authority.lease');
|
|
51
|
+
const claimPath = join(controlRoot, 'authority.claim');
|
|
52
|
+
token = randomBytes(32).toString('hex');
|
|
53
|
+
while (!acquired) {
|
|
54
|
+
checkAcquisitionControl(deadline, explicitDeadline !== undefined, options.signal);
|
|
55
|
+
if (!acquireClaim(claimPath, controlRoot, token, hostId)) {
|
|
56
|
+
if (tryReclaimClaim(claimPath, lockPath, controlRoot, hostId, staleMs))
|
|
57
|
+
continue;
|
|
58
|
+
await waitForLeasePoll(pollMs, deadline, explicitDeadline !== undefined, options.signal);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let initializingLock;
|
|
62
|
+
let claimOwnershipUncertain = false;
|
|
63
|
+
try {
|
|
64
|
+
try {
|
|
65
|
+
mkdirSync(lockPath, { mode: 0o700 });
|
|
66
|
+
initializingLock = lstatSync(lockPath);
|
|
67
|
+
emitLeaseFault('lease-directory-created');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (!hasCode(error, 'EEXIST'))
|
|
71
|
+
throw error;
|
|
72
|
+
if (tryReclaimLockedLease(lockPath, controlRoot, hostId, staleMs))
|
|
73
|
+
continue;
|
|
74
|
+
await waitForLeasePoll(pollMs, deadline, explicitDeadline !== undefined, options.signal);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
ownerIdentity = writeOwner(lockPath, ownerMetadata(token, hostId));
|
|
78
|
+
emitLeaseFault('lease-owner-written');
|
|
79
|
+
lockIdentity = lstatSync(lockPath);
|
|
80
|
+
if (!lockIdentity.isDirectory() || lockIdentity.isSymbolicLink())
|
|
81
|
+
throw new PathSafetyError('Unsafe repository lease path.');
|
|
82
|
+
const ownerPath = join(lockPath, 'owner.json');
|
|
83
|
+
const ownerAtPath = lstatSync(ownerPath);
|
|
84
|
+
assertSafeRegular(ownerAtPath, ownerPath);
|
|
85
|
+
if (!sameLeaseIdentity(ownerIdentity, ownerAtPath))
|
|
86
|
+
throw new PathSafetyError('Repository lease owner changed during initialization.', 'IDENTITY_CHANGED');
|
|
87
|
+
syncDirectory(controlRoot);
|
|
88
|
+
if (!removeOwnedClaim(claimPath, token)) {
|
|
89
|
+
claimOwnershipUncertain = true;
|
|
90
|
+
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
91
|
+
throw new LeaseContentionError('Repository initialization claim ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
92
|
+
}
|
|
93
|
+
emitLeaseFault('lease-claim-removed');
|
|
94
|
+
acquired = true;
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (!acquired && initializingLock !== undefined)
|
|
98
|
+
cleanupOwnedInitialization(lockPath, controlRoot, token, initializingLock);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
if (!acquired && !claimOwnershipUncertain)
|
|
103
|
+
removeOwnedClaim(claimPath, token);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (lockIdentity === undefined || ownerIdentity === undefined)
|
|
107
|
+
throw new LeaseContentionError('Repository lease identity was not initialized.', 'LEASE_OWNER_UNKNOWN');
|
|
108
|
+
const acquiredState = {
|
|
109
|
+
authorityId: rootState.authorityId,
|
|
110
|
+
token,
|
|
111
|
+
lockPath,
|
|
112
|
+
lockIdentity,
|
|
113
|
+
ownerIdentity,
|
|
114
|
+
pendingOperations: new Set(),
|
|
115
|
+
active: true,
|
|
116
|
+
};
|
|
117
|
+
leaseState = acquiredState;
|
|
118
|
+
const lease = createRepositoryLease(rootState.authorityRoot, acquiredState);
|
|
119
|
+
const context = new Map(authorityContext.getStore() ?? []);
|
|
120
|
+
context.set(rootState.authorityId, acquiredState);
|
|
121
|
+
return await authorityContext.run(context, async () => {
|
|
122
|
+
const { recoverCanonicalTransactions } = await import('./transaction/recovery.js');
|
|
123
|
+
await recoverCanonicalTransactions(root, lease, options);
|
|
124
|
+
return operation(lease);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
if (leaseState !== undefined) {
|
|
129
|
+
while (leaseState.pendingOperations.size > 0)
|
|
130
|
+
await Promise.allSettled([...leaseState.pendingOperations]);
|
|
131
|
+
leaseState.active = false;
|
|
132
|
+
}
|
|
133
|
+
if (acquired && lockIdentity !== undefined && ownerIdentity !== undefined)
|
|
134
|
+
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
135
|
+
turn.release();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export function assertLiveLease(root, lease) {
|
|
139
|
+
const rootState = getRootState(root);
|
|
140
|
+
const leaseState = getLeaseState(lease);
|
|
141
|
+
if (rootState === undefined || leaseState === undefined || rootState.authorityId !== leaseState.authorityId)
|
|
142
|
+
throw new RepositoryStoreConfigError('Lease and managed root have different authorities.', 'AUTHORITY_MISMATCH');
|
|
143
|
+
if (!leaseState.active)
|
|
144
|
+
throw new LeaseContentionError('Repository lease is no longer active.', 'LEASE_BUSY');
|
|
145
|
+
if (!ownedLeaseIdentityMatches(leaseState.lockPath, leaseState.token, leaseState.lockIdentity, leaseState.ownerIdentity)) {
|
|
146
|
+
leaseState.active = false;
|
|
147
|
+
throw new LeaseContentionError('Repository lease ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function trackLeaseOperation(lease, operation) {
|
|
151
|
+
void operation.catch(() => undefined);
|
|
152
|
+
const state = getLeaseState(lease);
|
|
153
|
+
if (state === undefined || !state.active)
|
|
154
|
+
throw new LeaseContentionError('Repository lease is no longer active.', 'LEASE_BUSY');
|
|
155
|
+
state.pendingOperations.add(operation);
|
|
156
|
+
void operation.finally(() => state.pendingOperations.delete(operation)).catch(() => undefined);
|
|
157
|
+
return operation;
|
|
158
|
+
}
|
|
159
|
+
export function checkControl(control) {
|
|
160
|
+
if (control.signal?.aborted)
|
|
161
|
+
throw new LeaseContentionError('Repository operation was aborted.', 'ABORTED');
|
|
162
|
+
if (control.deadline !== undefined && Date.now() >= control.deadline)
|
|
163
|
+
throw new LeaseContentionError('Repository operation deadline was exceeded.', 'DEADLINE_EXCEEDED');
|
|
164
|
+
}
|
|
165
|
+
function writeOwner(directory, owner) {
|
|
166
|
+
const identity = writeOwnerFile(join(directory, 'owner.json'), owner);
|
|
167
|
+
syncDirectory(directory);
|
|
168
|
+
return identity;
|
|
169
|
+
}
|
|
170
|
+
function writeOwnerFile(path, owner) {
|
|
171
|
+
let descriptor;
|
|
172
|
+
try {
|
|
173
|
+
descriptor = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
|
|
174
|
+
writeFileSync(descriptor, `${JSON.stringify(owner)}\n`);
|
|
175
|
+
syncFileDescriptor(descriptor, path);
|
|
176
|
+
return fstatSync(descriptor);
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
if (descriptor !== undefined)
|
|
180
|
+
closeSync(descriptor);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function ownerMetadata(token, hostId) {
|
|
184
|
+
const processStart = readProcessStart(process.pid);
|
|
185
|
+
return {
|
|
186
|
+
version: 1,
|
|
187
|
+
token,
|
|
188
|
+
pid: process.pid,
|
|
189
|
+
acquiredAt: new Date().toISOString(),
|
|
190
|
+
hostname: hostname(),
|
|
191
|
+
hostId,
|
|
192
|
+
runtime: typeof Bun === 'undefined' ? `node:${process.version}` : `bun:${Bun.version}`,
|
|
193
|
+
...(processStart === undefined ? {} : { processStart }),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function ensureHostId(controlRoot) {
|
|
197
|
+
const path = join(controlRoot, 'host-id');
|
|
198
|
+
try {
|
|
199
|
+
const value = new TextDecoder().decode(readRegularFile(path, MAX_LEASE_METADATA_BYTES)).trim();
|
|
200
|
+
if (!/^[0-9a-f]{64}$/u.test(value))
|
|
201
|
+
throw new PathSafetyError('Repository host-id is malformed.');
|
|
202
|
+
return value;
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
if (!hasCode(error, 'ENOENT'))
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
const value = randomBytes(32).toString('hex');
|
|
209
|
+
try {
|
|
210
|
+
const descriptor = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
|
|
211
|
+
try {
|
|
212
|
+
writeFileSync(descriptor, `${value}\n`);
|
|
213
|
+
syncFileDescriptor(descriptor, path);
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
closeSync(descriptor);
|
|
217
|
+
}
|
|
218
|
+
syncDirectory(controlRoot);
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (!hasCode(error, 'EEXIST'))
|
|
223
|
+
throw error;
|
|
224
|
+
return ensureHostId(controlRoot);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function tryReclaimLockedLease(lockPath, controlRoot, hostId, staleMs) {
|
|
228
|
+
const controlDirectories = captureDirectories(controlRoot);
|
|
229
|
+
try {
|
|
230
|
+
const ownerPath = join(lockPath, 'owner.json');
|
|
231
|
+
const verifiedOwner = readOwner(ownerPath);
|
|
232
|
+
const owner = verifiedOwner.metadata;
|
|
233
|
+
const lockStat = lstatSync(lockPath);
|
|
234
|
+
const ownerStat = verifiedOwner.identity;
|
|
235
|
+
const releaseExpectation = {
|
|
236
|
+
kind: 'directory',
|
|
237
|
+
identity: lockStat,
|
|
238
|
+
ownerIdentity: ownerStat,
|
|
239
|
+
token: owner.token,
|
|
240
|
+
};
|
|
241
|
+
if (!lockStat.isDirectory() || lockStat.isSymbolicLink())
|
|
242
|
+
throw new PathSafetyError('Unsafe repository lease path.');
|
|
243
|
+
if (Date.now() - lockStat.mtimeMs < staleMs || !conclusivelyDead(owner, hostId))
|
|
244
|
+
return false;
|
|
245
|
+
const quarantine = `${lockPath}.release-${owner.token}`;
|
|
246
|
+
if (!quarantineNoReplace(lockPath, quarantine))
|
|
247
|
+
return false;
|
|
248
|
+
const quarantinedOwnerPath = join(quarantine, 'owner.json');
|
|
249
|
+
const movedOwner = readOwner(quarantinedOwnerPath);
|
|
250
|
+
const movedLock = lstatSync(quarantine);
|
|
251
|
+
if (!sameIdentity(lockStat, movedLock) ||
|
|
252
|
+
!sameLeaseIdentity(ownerStat, movedOwner.identity) ||
|
|
253
|
+
movedOwner.metadata.token !== owner.token) {
|
|
254
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation, {
|
|
255
|
+
kind: 'directory',
|
|
256
|
+
identity: movedLock,
|
|
257
|
+
ownerIdentity: movedOwner.identity,
|
|
258
|
+
token: movedOwner.metadata.token,
|
|
259
|
+
});
|
|
260
|
+
throw new LeaseContentionError('Stale lease identity changed during reclamation.', 'LEASE_OWNER_UNKNOWN');
|
|
261
|
+
}
|
|
262
|
+
if (!removeVerifiedOwnerFile(quarantinedOwnerPath, movedOwner)) {
|
|
263
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
rmdirSync(quarantine);
|
|
267
|
+
syncDirectory(controlRoot);
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
if (hasCode(error, 'ENOENT') || (error instanceof PathSafetyError && error.code === 'IDENTITY_CHANGED'))
|
|
272
|
+
return exactPathWasReleased(lockPath, controlDirectories, error);
|
|
273
|
+
if (error instanceof PathSafetyError || error instanceof ResourceLimitError)
|
|
274
|
+
throw error;
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function exactPathWasReleased(path, controlDirectories, originalError, failOnExistingIdentityChange = true) {
|
|
279
|
+
revalidateDirectories(controlDirectories);
|
|
280
|
+
try {
|
|
281
|
+
lstatSync(path);
|
|
282
|
+
}
|
|
283
|
+
catch (inspectionError) {
|
|
284
|
+
if (hasCode(inspectionError, 'ENOENT')) {
|
|
285
|
+
revalidateDirectories(controlDirectories);
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (originalError instanceof PathSafetyError)
|
|
289
|
+
throw originalError;
|
|
290
|
+
throw inspectionError;
|
|
291
|
+
}
|
|
292
|
+
if (failOnExistingIdentityChange && originalError instanceof PathSafetyError)
|
|
293
|
+
throw originalError;
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
function acquireClaim(claimPath, controlRoot, token, hostId) {
|
|
297
|
+
const staging = `${claimPath}.prepare-${token}`;
|
|
298
|
+
try {
|
|
299
|
+
writeOwnerFile(staging, ownerMetadata(token, hostId));
|
|
300
|
+
linkSync(staging, claimPath);
|
|
301
|
+
emitLeaseFault('claim-published');
|
|
302
|
+
rmSync(staging);
|
|
303
|
+
syncDirectory(controlRoot);
|
|
304
|
+
emitLeaseFault('claim-finalized');
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
try {
|
|
309
|
+
rmSync(staging);
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
}
|
|
313
|
+
if (hasCode(error, 'EEXIST'))
|
|
314
|
+
return false;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function tryReclaimClaim(claimPath, lockPath, controlRoot, hostId, staleMs) {
|
|
319
|
+
const controlDirectories = captureDirectories(controlRoot);
|
|
320
|
+
try {
|
|
321
|
+
const preliminary = lstatSync(claimPath);
|
|
322
|
+
if (preliminary.isSymbolicLink())
|
|
323
|
+
throw new PathSafetyError('Unsafe repository claim symbolic link.', 'UNSAFE_LINK');
|
|
324
|
+
if (!preliminary.isFile() || (preliminary.nlink !== 1 && preliminary.nlink !== 2))
|
|
325
|
+
throw new PathSafetyError('Unsafe repository claim artifact.', 'UNSAFE_HARD_LINK');
|
|
326
|
+
const verifiedOwner = readOwner(claimPath, true);
|
|
327
|
+
if (!sameIdentity(preliminary, verifiedOwner.identity))
|
|
328
|
+
throw new LeaseContentionError('Claim identity changed before owner verification.', 'LEASE_OWNER_UNKNOWN');
|
|
329
|
+
if (Date.now() - verifiedOwner.identity.mtimeMs < staleMs)
|
|
330
|
+
return false;
|
|
331
|
+
const owner = verifiedOwner.metadata;
|
|
332
|
+
if (!conclusivelyDead(owner, hostId))
|
|
333
|
+
return false;
|
|
334
|
+
emitFilesystemFault('after-stale-claim-owner-verified', claimPath);
|
|
335
|
+
normalizeInterruptedClaimLink(claimPath, controlRoot);
|
|
336
|
+
const normalizedOwner = readOwner(claimPath);
|
|
337
|
+
if (!sameIdentity(preliminary, normalizedOwner.identity) ||
|
|
338
|
+
!sameIdentity(verifiedOwner.identity, normalizedOwner.identity) ||
|
|
339
|
+
normalizedOwner.metadata.token !== owner.token)
|
|
340
|
+
throw new LeaseContentionError('Claim owner changed during reclamation.', 'LEASE_OWNER_UNKNOWN');
|
|
341
|
+
cleanupInterruptedInitialization(lockPath, controlRoot, owner, staleMs);
|
|
342
|
+
const quarantine = `${claimPath}.release-${owner.token}`;
|
|
343
|
+
const releaseExpectation = {
|
|
344
|
+
kind: 'file',
|
|
345
|
+
identity: normalizedOwner.identity,
|
|
346
|
+
token: owner.token,
|
|
347
|
+
};
|
|
348
|
+
if (!quarantineNoReplace(claimPath, quarantine))
|
|
349
|
+
return false;
|
|
350
|
+
const moved = readOwner(quarantine);
|
|
351
|
+
if (!sameIdentity(normalizedOwner.identity, moved.identity) || moved.metadata.token !== owner.token) {
|
|
352
|
+
restoreQuarantinedLease(quarantine, claimPath, releaseExpectation, {
|
|
353
|
+
kind: 'file',
|
|
354
|
+
identity: moved.identity,
|
|
355
|
+
token: moved.metadata.token,
|
|
356
|
+
});
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
rmSync(quarantine);
|
|
360
|
+
syncDirectory(controlRoot);
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
if (hasCode(error, 'ENOENT') || (error instanceof PathSafetyError && error.code === 'IDENTITY_CHANGED'))
|
|
365
|
+
return exactPathWasReleased(claimPath, controlDirectories, error, false);
|
|
366
|
+
if (error instanceof PathSafetyError || error instanceof ResourceLimitError)
|
|
367
|
+
throw error;
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function cleanupInterruptedInitialization(lockPath, controlRoot, owner, staleMs) {
|
|
372
|
+
let lockStat;
|
|
373
|
+
try {
|
|
374
|
+
lockStat = lstatSync(lockPath);
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (hasCode(error, 'ENOENT'))
|
|
378
|
+
return;
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (!lockStat.isDirectory() || lockStat.isSymbolicLink())
|
|
382
|
+
throw new PathSafetyError('Unsafe incomplete repository lease path.');
|
|
383
|
+
if (Date.now() - lockStat.mtimeMs < staleMs)
|
|
384
|
+
return;
|
|
385
|
+
try {
|
|
386
|
+
readOwner(join(lockPath, 'owner.json'));
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
if (!hasCode(error, 'ENOENT'))
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (readdirSync(lockPath).length !== 0)
|
|
394
|
+
return;
|
|
395
|
+
const quarantine = `${lockPath}.release-${owner.token}`;
|
|
396
|
+
const releaseExpectation = { kind: 'directory', identity: lockStat, token: owner.token };
|
|
397
|
+
if (!quarantineNoReplace(lockPath, quarantine))
|
|
398
|
+
return;
|
|
399
|
+
const quarantined = lstatSync(quarantine);
|
|
400
|
+
if (!sameIdentity(lockStat, quarantined) || readdirSync(quarantine).length !== 0) {
|
|
401
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
402
|
+
throw new LeaseContentionError('Incomplete lease identity changed during reclamation.', 'LEASE_OWNER_UNKNOWN');
|
|
403
|
+
}
|
|
404
|
+
rmdirSync(quarantine);
|
|
405
|
+
syncDirectory(controlRoot);
|
|
406
|
+
}
|
|
407
|
+
function cleanupOwnedInitialization(lockPath, controlRoot, token, expectedLock) {
|
|
408
|
+
const quarantine = `${lockPath}.release-${token}`;
|
|
409
|
+
let releaseExpectation = { kind: 'directory', identity: expectedLock, token };
|
|
410
|
+
let restorationSafe = false;
|
|
411
|
+
try {
|
|
412
|
+
const current = lstatSync(lockPath);
|
|
413
|
+
if (!current.isDirectory() || current.isSymbolicLink() || !sameIdentity(expectedLock, current))
|
|
414
|
+
return;
|
|
415
|
+
const currentEntries = readdirSync(lockPath);
|
|
416
|
+
if (currentEntries.some((entry) => entry !== 'owner.json'))
|
|
417
|
+
return;
|
|
418
|
+
if (currentEntries.includes('owner.json')) {
|
|
419
|
+
const owner = readOwner(join(lockPath, 'owner.json'));
|
|
420
|
+
if (owner.metadata.token !== token)
|
|
421
|
+
return;
|
|
422
|
+
releaseExpectation = { ...releaseExpectation, ownerIdentity: owner.identity };
|
|
423
|
+
}
|
|
424
|
+
if (!quarantineNoReplace(lockPath, quarantine))
|
|
425
|
+
return;
|
|
426
|
+
restorationSafe = true;
|
|
427
|
+
const moved = lstatSync(quarantine);
|
|
428
|
+
if (!sameIdentity(expectedLock, moved)) {
|
|
429
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const entries = readdirSync(quarantine);
|
|
433
|
+
if (entries.some((entry) => entry !== 'owner.json')) {
|
|
434
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (entries.includes('owner.json')) {
|
|
438
|
+
const ownerPath = join(quarantine, 'owner.json');
|
|
439
|
+
const owner = readOwner(ownerPath);
|
|
440
|
+
if (owner.metadata.token !== token ||
|
|
441
|
+
releaseExpectation.ownerIdentity === undefined ||
|
|
442
|
+
!sameIdentity(owner.identity, releaseExpectation.ownerIdentity) ||
|
|
443
|
+
!removeVerifiedOwnerFile(ownerPath, owner)) {
|
|
444
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
restorationSafe = false;
|
|
448
|
+
}
|
|
449
|
+
rmdirSync(quarantine);
|
|
450
|
+
syncDirectory(controlRoot);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
if (restorationSafe)
|
|
454
|
+
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function normalizeInterruptedClaimLink(claimPath, controlRoot) {
|
|
458
|
+
const claim = lstatSync(claimPath);
|
|
459
|
+
if (!claim.isFile() || claim.isSymbolicLink() || claim.nlink === 1)
|
|
460
|
+
return;
|
|
461
|
+
if (claim.nlink !== 2)
|
|
462
|
+
throw new PathSafetyError('Unsafe repository claim hard-link count.', 'UNSAFE_HARD_LINK');
|
|
463
|
+
const prefix = `${basename(claimPath)}.prepare-`;
|
|
464
|
+
const candidates = readdirSync(dirname(claimPath)).filter((name) => name.startsWith(prefix));
|
|
465
|
+
const matching = candidates.filter((name) => {
|
|
466
|
+
const suffix = name.slice(prefix.length);
|
|
467
|
+
if (!/^[0-9a-f-]{32,64}$/u.test(suffix))
|
|
468
|
+
return false;
|
|
469
|
+
const stat = lstatSync(join(dirname(claimPath), name));
|
|
470
|
+
return stat.isFile() && !stat.isSymbolicLink() && sameIdentity(claim, stat);
|
|
471
|
+
});
|
|
472
|
+
if (matching.length !== 1) {
|
|
473
|
+
const current = lstatSync(claimPath);
|
|
474
|
+
if (current.nlink === 1 && sameIdentity(claim, current))
|
|
475
|
+
return;
|
|
476
|
+
throw new PathSafetyError('Repository claim hard link is not package-owned.');
|
|
477
|
+
}
|
|
478
|
+
rmSync(join(dirname(claimPath), matching[0]));
|
|
479
|
+
syncDirectory(controlRoot);
|
|
480
|
+
}
|
|
481
|
+
function conclusivelyDead(owner, hostId) {
|
|
482
|
+
if (owner.hostname !== hostname() || owner.hostId !== hostId)
|
|
483
|
+
return false;
|
|
484
|
+
const currentStart = readProcessStart(owner.pid);
|
|
485
|
+
if (owner.processStart !== undefined && currentStart !== undefined && owner.processStart !== currentStart)
|
|
486
|
+
return true;
|
|
487
|
+
try {
|
|
488
|
+
process.kill(owner.pid, 0);
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
return hasCode(error, 'ESRCH');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function readProcessStart(pid) {
|
|
496
|
+
if (process.platform !== 'linux')
|
|
497
|
+
return undefined;
|
|
498
|
+
try {
|
|
499
|
+
const fields = readFileSync(`/proc/${pid}/stat`, 'utf8').trim().split(/\s+/u);
|
|
500
|
+
return fields[21];
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return undefined;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function readOwner(path, allowInterruptedClaimLink = false) {
|
|
507
|
+
const initialIdentity = allowInterruptedClaimLink ? lstatSync(path) : undefined;
|
|
508
|
+
const stagingPeer = allowInterruptedClaimLink ? interruptedClaimPeer(path, initialIdentity) : undefined;
|
|
509
|
+
let file;
|
|
510
|
+
try {
|
|
511
|
+
file = readRegularFileWithIdentity(path, MAX_LEASE_METADATA_BYTES, {
|
|
512
|
+
allowedLinkCounts: allowInterruptedClaimLink ? [1, 2] : [1],
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
catch (error) {
|
|
516
|
+
const recoverableReadRace = error instanceof PathSafetyError && (error.code === 'IDENTITY_CHANGED' || error.code === 'UNSAFE_HARD_LINK');
|
|
517
|
+
if (initialIdentity === undefined || !recoverableReadRace)
|
|
518
|
+
throw error;
|
|
519
|
+
const stabilized = lstatSync(path);
|
|
520
|
+
if (stabilized.isSymbolicLink() || !stabilized.isFile())
|
|
521
|
+
throw error;
|
|
522
|
+
if (!sameIdentity(initialIdentity, stabilized))
|
|
523
|
+
throw new PathSafetyError('Interrupted claim identity changed during owner verification.', 'IDENTITY_CHANGED');
|
|
524
|
+
if (stabilized.nlink !== 1)
|
|
525
|
+
throw error;
|
|
526
|
+
file = readRegularFileWithIdentity(path, MAX_LEASE_METADATA_BYTES);
|
|
527
|
+
}
|
|
528
|
+
const owner = JSON.parse(new TextDecoder().decode(file.bytes));
|
|
529
|
+
if (owner.version !== 1 ||
|
|
530
|
+
typeof owner.token !== 'string' ||
|
|
531
|
+
!/^[0-9a-f-]{32,64}$/u.test(owner.token) ||
|
|
532
|
+
!Number.isSafeInteger(owner.pid) ||
|
|
533
|
+
typeof owner.acquiredAt !== 'string' ||
|
|
534
|
+
typeof owner.hostname !== 'string' ||
|
|
535
|
+
typeof owner.hostId !== 'string' ||
|
|
536
|
+
typeof owner.runtime !== 'string')
|
|
537
|
+
throw new Error('Malformed lease owner.');
|
|
538
|
+
const finalIdentity = allowInterruptedClaimLink ? lstatSync(path) : undefined;
|
|
539
|
+
if (finalIdentity !== undefined && !sameIdentity(file.identity, finalIdentity))
|
|
540
|
+
throw new PathSafetyError('Interrupted claim identity changed after owner verification.', 'IDENTITY_CHANGED');
|
|
541
|
+
const finalStagingPeer = allowInterruptedClaimLink && finalIdentity !== undefined ? interruptedClaimPeer(path, finalIdentity) : undefined;
|
|
542
|
+
if ([stagingPeer, finalStagingPeer].some((peer) => peer !== undefined && owner.token !== peer.token))
|
|
543
|
+
throw new PathSafetyError('Interrupted claim token does not match its package-owned staging link.');
|
|
544
|
+
return { metadata: owner, identity: file.identity };
|
|
545
|
+
}
|
|
546
|
+
function interruptedClaimPeer(path, observed) {
|
|
547
|
+
const stat = observed ?? lstatSync(path);
|
|
548
|
+
if (stat.isSymbolicLink() || !stat.isFile() || (stat.nlink !== 1 && stat.nlink !== 2))
|
|
549
|
+
throw new PathSafetyError('Unsafe interrupted repository claim.', 'UNSAFE_HARD_LINK');
|
|
550
|
+
if (stat.nlink === 1)
|
|
551
|
+
return undefined;
|
|
552
|
+
const prefix = `${basename(path)}.prepare-`;
|
|
553
|
+
const peers = readdirSync(dirname(path)).flatMap((name) => {
|
|
554
|
+
if (!name.startsWith(prefix))
|
|
555
|
+
return [];
|
|
556
|
+
const token = name.slice(prefix.length);
|
|
557
|
+
if (!/^[0-9a-f-]{32,64}$/u.test(token))
|
|
558
|
+
return [];
|
|
559
|
+
try {
|
|
560
|
+
const peer = lstatSync(join(dirname(path), name));
|
|
561
|
+
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(stat, peer) ? [{ token }] : [];
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
if (hasCode(error, 'ENOENT'))
|
|
565
|
+
return [];
|
|
566
|
+
throw error;
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
if (peers.length === 1)
|
|
570
|
+
return peers[0];
|
|
571
|
+
const stabilized = lstatSync(path);
|
|
572
|
+
if (!stabilized.isSymbolicLink() && stabilized.isFile() && stabilized.nlink === 1 && sameIdentity(stat, stabilized))
|
|
573
|
+
return undefined;
|
|
574
|
+
throw new PathSafetyError('Interrupted claim hard link is not package-owned.', 'UNSAFE_HARD_LINK');
|
|
575
|
+
}
|
|
576
|
+
function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity) {
|
|
577
|
+
const releasePath = `${lockPath}.release-${token}`;
|
|
578
|
+
const releaseExpectation = {
|
|
579
|
+
kind: 'directory',
|
|
580
|
+
identity: lockIdentity,
|
|
581
|
+
ownerIdentity,
|
|
582
|
+
token,
|
|
583
|
+
};
|
|
584
|
+
let restorationSafe = false;
|
|
585
|
+
try {
|
|
586
|
+
const currentLock = lstatSync(lockPath);
|
|
587
|
+
const currentOwner = readOwner(join(lockPath, 'owner.json'));
|
|
588
|
+
if (!currentLock.isDirectory() ||
|
|
589
|
+
currentLock.isSymbolicLink() ||
|
|
590
|
+
!sameLeaseIdentity(lockIdentity, currentLock) ||
|
|
591
|
+
!sameLeaseIdentity(ownerIdentity, currentOwner.identity) ||
|
|
592
|
+
currentOwner.metadata.token !== token)
|
|
593
|
+
return;
|
|
594
|
+
if (!quarantineNoReplace(lockPath, releasePath))
|
|
595
|
+
return;
|
|
596
|
+
restorationSafe = true;
|
|
597
|
+
const movedLock = lstatSync(releasePath);
|
|
598
|
+
const movedOwnerPath = join(releasePath, 'owner.json');
|
|
599
|
+
const movedOwner = readOwner(movedOwnerPath);
|
|
600
|
+
if (!sameIdentity(lockIdentity, movedLock) ||
|
|
601
|
+
!sameLeaseIdentity(ownerIdentity, movedOwner.identity) ||
|
|
602
|
+
movedOwner.metadata.token !== token) {
|
|
603
|
+
restoreQuarantinedLease(releasePath, lockPath, releaseExpectation, {
|
|
604
|
+
kind: 'directory',
|
|
605
|
+
identity: movedLock,
|
|
606
|
+
ownerIdentity: movedOwner.identity,
|
|
607
|
+
token: movedOwner.metadata.token,
|
|
608
|
+
});
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (!removeVerifiedOwnerFile(movedOwnerPath, movedOwner)) {
|
|
612
|
+
restoreQuarantinedLease(releasePath, lockPath, releaseExpectation);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
restorationSafe = false;
|
|
616
|
+
rmdirSync(releasePath);
|
|
617
|
+
syncDirectory(controlRoot);
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
if (restorationSafe)
|
|
621
|
+
restoreQuarantinedLease(releasePath, lockPath, releaseExpectation);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
export function restoreQuarantinedLease(quarantine, lockPath, expected, movedFromLock) {
|
|
625
|
+
const verification = `${quarantine}.restore-${randomBytes(16).toString('hex')}`;
|
|
626
|
+
let staged = false;
|
|
627
|
+
let preserveUnverified = false;
|
|
628
|
+
let restorationExpectation;
|
|
629
|
+
try {
|
|
630
|
+
nativeRenameNoReplace(requireNativePublicationBackend(), quarantine, verification);
|
|
631
|
+
staged = true;
|
|
632
|
+
restorationExpectation = releaseArtifactMatches(verification, expected)
|
|
633
|
+
? expected
|
|
634
|
+
: movedFromLock !== undefined && releaseArtifactMatches(verification, movedFromLock)
|
|
635
|
+
? movedFromLock
|
|
636
|
+
: undefined;
|
|
637
|
+
if (restorationExpectation === undefined) {
|
|
638
|
+
preserveUnverified = true;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
nativeRenameNoReplace(requireNativePublicationBackend(), verification, lockPath);
|
|
643
|
+
staged = false;
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (!hasCode(error, 'EEXIST'))
|
|
647
|
+
throw error;
|
|
648
|
+
if (restorationExpectation === expected)
|
|
649
|
+
cleanupVerifiedReleaseArtifact(verification, expected, quarantine);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
if (!releaseArtifactMatches(lockPath, restorationExpectation))
|
|
653
|
+
throw new PathSafetyError('Restored repository lease identity changed.', 'IDENTITY_CHANGED');
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
if (staged) {
|
|
659
|
+
try {
|
|
660
|
+
if (preserveUnverified ||
|
|
661
|
+
releaseArtifactMatches(verification, expected) ||
|
|
662
|
+
(movedFromLock !== undefined && releaseArtifactMatches(verification, movedFromLock)))
|
|
663
|
+
nativeRenameNoReplace(requireNativePublicationBackend(), verification, quarantine);
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function releaseArtifactMatches(path, expected) {
|
|
671
|
+
try {
|
|
672
|
+
const identity = lstatSync(path);
|
|
673
|
+
if (!sameIdentity(identity, expected.identity) || identity.isSymbolicLink())
|
|
674
|
+
return false;
|
|
675
|
+
if (expected.kind === 'file') {
|
|
676
|
+
if (!identity.isFile())
|
|
677
|
+
return false;
|
|
678
|
+
const owner = readOwner(path);
|
|
679
|
+
return owner.metadata.token === expected.token && sameIdentity(owner.identity, expected.identity);
|
|
680
|
+
}
|
|
681
|
+
if (!identity.isDirectory())
|
|
682
|
+
return false;
|
|
683
|
+
const entries = readdirSync(path);
|
|
684
|
+
if (expected.ownerIdentity === undefined)
|
|
685
|
+
return entries.length === 0;
|
|
686
|
+
if (entries.length !== 1 || entries[0] !== 'owner.json')
|
|
687
|
+
return false;
|
|
688
|
+
const owner = readOwner(join(path, 'owner.json'));
|
|
689
|
+
const finalEntries = readdirSync(path);
|
|
690
|
+
return (owner.metadata.token === expected.token &&
|
|
691
|
+
sameIdentity(owner.identity, expected.ownerIdentity) &&
|
|
692
|
+
sameIdentity(lstatSync(path), expected.identity) &&
|
|
693
|
+
finalEntries.length === 1 &&
|
|
694
|
+
finalEntries[0] === 'owner.json');
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function cleanupVerifiedReleaseArtifact(path, expected, releasePath = path) {
|
|
701
|
+
const cleanup = `${path}.cleanup-${randomBytes(16).toString('hex')}`;
|
|
702
|
+
let moved = false;
|
|
703
|
+
let removed = false;
|
|
704
|
+
try {
|
|
705
|
+
const match = /\.release-([0-9a-f-]{32,64})$/u.exec(basename(releasePath));
|
|
706
|
+
if (match?.[1] !== expected.token)
|
|
707
|
+
return;
|
|
708
|
+
nativeRenameNoReplace(requireNativePublicationBackend(), path, cleanup);
|
|
709
|
+
moved = true;
|
|
710
|
+
const identity = lstatSync(cleanup);
|
|
711
|
+
if (!sameIdentity(identity, expected.identity) || identity.isSymbolicLink())
|
|
712
|
+
return;
|
|
713
|
+
if (expected.kind === 'file') {
|
|
714
|
+
if (!identity.isFile())
|
|
715
|
+
return;
|
|
716
|
+
const owner = readOwner(cleanup);
|
|
717
|
+
if (owner.metadata.token !== expected.token || !sameIdentity(owner.identity, expected.identity))
|
|
718
|
+
return;
|
|
719
|
+
rmSync(cleanup);
|
|
720
|
+
removed = true;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
if (!identity.isDirectory())
|
|
724
|
+
return;
|
|
725
|
+
const entries = readdirSync(cleanup);
|
|
726
|
+
if (expected.ownerIdentity === undefined) {
|
|
727
|
+
if (entries.length !== 0)
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
if (entries.length !== 1 || entries[0] !== 'owner.json')
|
|
732
|
+
return;
|
|
733
|
+
const ownerPath = join(cleanup, 'owner.json');
|
|
734
|
+
const owner = readOwner(ownerPath);
|
|
735
|
+
if (owner.metadata.token !== expected.token || !sameIdentity(owner.identity, expected.ownerIdentity))
|
|
736
|
+
return;
|
|
737
|
+
rmSync(ownerPath);
|
|
738
|
+
}
|
|
739
|
+
if (!sameIdentity(lstatSync(cleanup), expected.identity) || readdirSync(cleanup).length !== 0)
|
|
740
|
+
return;
|
|
741
|
+
rmdirSync(cleanup);
|
|
742
|
+
removed = true;
|
|
743
|
+
}
|
|
744
|
+
syncDirectory(dirname(path));
|
|
745
|
+
}
|
|
746
|
+
catch {
|
|
747
|
+
}
|
|
748
|
+
finally {
|
|
749
|
+
if (moved && !removed) {
|
|
750
|
+
try {
|
|
751
|
+
nativeRenameNoReplace(requireNativePublicationBackend(), cleanup, path);
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function removeOwnedClaim(path, token) {
|
|
759
|
+
try {
|
|
760
|
+
const expected = readOwner(path);
|
|
761
|
+
if (expected.metadata.token !== token)
|
|
762
|
+
return false;
|
|
763
|
+
const quarantine = `${path}.release-${token}`;
|
|
764
|
+
const releaseExpectation = { kind: 'file', identity: expected.identity, token };
|
|
765
|
+
if (!quarantineNoReplace(path, quarantine))
|
|
766
|
+
return false;
|
|
767
|
+
const moved = readOwner(quarantine);
|
|
768
|
+
if (moved.metadata.token !== token || !sameIdentity(expected.identity, moved.identity)) {
|
|
769
|
+
restoreQuarantinedLease(quarantine, path, releaseExpectation, {
|
|
770
|
+
kind: 'file',
|
|
771
|
+
identity: moved.identity,
|
|
772
|
+
token: moved.metadata.token,
|
|
773
|
+
});
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
rmSync(quarantine);
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function ownedLeaseIdentityMatches(lockPath, token, lockIdentity, ownerIdentity) {
|
|
784
|
+
try {
|
|
785
|
+
const currentLock = lstatSync(lockPath);
|
|
786
|
+
const currentOwner = readOwner(join(lockPath, 'owner.json'));
|
|
787
|
+
return (currentLock.isDirectory() &&
|
|
788
|
+
!currentLock.isSymbolicLink() &&
|
|
789
|
+
sameLeaseIdentity(lockIdentity, currentLock) &&
|
|
790
|
+
sameLeaseIdentity(ownerIdentity, currentOwner.identity) &&
|
|
791
|
+
currentOwner.metadata.token === token);
|
|
792
|
+
}
|
|
793
|
+
catch {
|
|
794
|
+
return false;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
function sameLeaseIdentity(left, right) {
|
|
798
|
+
return (sameIdentity(left, right) &&
|
|
799
|
+
left.birthtimeMs === right.birthtimeMs &&
|
|
800
|
+
left.ctimeMs === right.ctimeMs &&
|
|
801
|
+
left.mtimeMs === right.mtimeMs);
|
|
802
|
+
}
|
|
803
|
+
function removeVerifiedOwnerFile(path, expected) {
|
|
804
|
+
const quarantine = `${path}.release-${expected.metadata.token}`;
|
|
805
|
+
if (!quarantineNoReplace(path, quarantine))
|
|
806
|
+
return false;
|
|
807
|
+
try {
|
|
808
|
+
const moved = readOwner(quarantine);
|
|
809
|
+
const releaseExpectation = {
|
|
810
|
+
kind: 'file',
|
|
811
|
+
identity: expected.identity,
|
|
812
|
+
token: expected.metadata.token,
|
|
813
|
+
};
|
|
814
|
+
if (moved.metadata.token !== expected.metadata.token || !sameIdentity(moved.identity, expected.identity)) {
|
|
815
|
+
restoreQuarantinedLease(quarantine, path, releaseExpectation, {
|
|
816
|
+
kind: 'file',
|
|
817
|
+
identity: moved.identity,
|
|
818
|
+
token: moved.metadata.token,
|
|
819
|
+
});
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
rmSync(quarantine);
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
825
|
+
catch {
|
|
826
|
+
restoreQuarantinedLease(quarantine, path, {
|
|
827
|
+
kind: 'file',
|
|
828
|
+
identity: expected.identity,
|
|
829
|
+
token: expected.metadata.token,
|
|
830
|
+
});
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function enqueueAuthority(authorityId) {
|
|
835
|
+
const predecessor = authorityQueueTails.get(authorityId) ?? Promise.resolve();
|
|
836
|
+
let releaseGate = () => undefined;
|
|
837
|
+
const gate = new Promise((resolveGate) => {
|
|
838
|
+
releaseGate = resolveGate;
|
|
839
|
+
});
|
|
840
|
+
const tail = predecessor.then(() => gate);
|
|
841
|
+
authorityQueueTails.set(authorityId, tail);
|
|
842
|
+
let released = false;
|
|
843
|
+
return {
|
|
844
|
+
predecessor,
|
|
845
|
+
release() {
|
|
846
|
+
if (released)
|
|
847
|
+
return;
|
|
848
|
+
released = true;
|
|
849
|
+
releaseGate();
|
|
850
|
+
if (authorityQueueTails.get(authorityId) === tail)
|
|
851
|
+
authorityQueueTails.delete(authorityId);
|
|
852
|
+
},
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
async function waitForAuthorityTurn(predecessor, deadline, hasExplicitDeadline, signal) {
|
|
856
|
+
checkAcquisitionControl(deadline, hasExplicitDeadline, signal);
|
|
857
|
+
await new Promise((resolveTurn, rejectTurn) => {
|
|
858
|
+
let settled = false;
|
|
859
|
+
const timer = setTimeout(timedOut, Math.max(0, deadline - Date.now()));
|
|
860
|
+
const settle = (operation) => {
|
|
861
|
+
if (settled)
|
|
862
|
+
return;
|
|
863
|
+
settled = true;
|
|
864
|
+
clearTimeout(timer);
|
|
865
|
+
signal?.removeEventListener('abort', aborted);
|
|
866
|
+
operation();
|
|
867
|
+
};
|
|
868
|
+
const aborted = () => settle(() => rejectTurn(new LeaseContentionError('Repository operation was aborted.', 'ABORTED')));
|
|
869
|
+
function timedOut() {
|
|
870
|
+
settle(() => rejectTurn(acquisitionTimeout(hasExplicitDeadline)));
|
|
871
|
+
}
|
|
872
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
873
|
+
void predecessor.then(() => settle(resolveTurn));
|
|
874
|
+
});
|
|
875
|
+
checkAcquisitionControl(deadline, hasExplicitDeadline, signal);
|
|
876
|
+
}
|
|
877
|
+
async function waitForLeasePoll(pollMs, deadline, hasExplicitDeadline, signal) {
|
|
878
|
+
checkAcquisitionControl(deadline, hasExplicitDeadline, signal);
|
|
879
|
+
await interruptibleDelay(Math.min(pollMs, Math.max(1, deadline - Date.now())), signal);
|
|
880
|
+
checkAcquisitionControl(deadline, hasExplicitDeadline, signal);
|
|
881
|
+
}
|
|
882
|
+
async function interruptibleDelay(ms, signal) {
|
|
883
|
+
await new Promise((resolveDelay, rejectDelay) => {
|
|
884
|
+
const timer = setTimeout(done, ms);
|
|
885
|
+
function done() {
|
|
886
|
+
signal?.removeEventListener('abort', aborted);
|
|
887
|
+
resolveDelay();
|
|
888
|
+
}
|
|
889
|
+
function aborted() {
|
|
890
|
+
clearTimeout(timer);
|
|
891
|
+
signal?.removeEventListener('abort', aborted);
|
|
892
|
+
rejectDelay(new LeaseContentionError('Repository operation was aborted.', 'ABORTED'));
|
|
893
|
+
}
|
|
894
|
+
signal?.addEventListener('abort', aborted, { once: true });
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
function checkAcquisitionControl(deadline, hasExplicitDeadline, signal) {
|
|
898
|
+
if (signal?.aborted)
|
|
899
|
+
throw new LeaseContentionError('Repository operation was aborted.', 'ABORTED');
|
|
900
|
+
if (Date.now() >= deadline)
|
|
901
|
+
throw acquisitionTimeout(hasExplicitDeadline);
|
|
902
|
+
}
|
|
903
|
+
function acquisitionTimeout(hasExplicitDeadline) {
|
|
904
|
+
return hasExplicitDeadline
|
|
905
|
+
? new LeaseContentionError('Repository operation deadline was exceeded.', 'DEADLINE_EXCEEDED')
|
|
906
|
+
: new LeaseContentionError('Repository authority lease is busy.', 'LEASE_BUSY');
|
|
907
|
+
}
|
|
908
|
+
function quarantineNoReplace(source, destination) {
|
|
909
|
+
emitFilesystemFault('before-lease-artifact-quarantine', source);
|
|
910
|
+
try {
|
|
911
|
+
const backend = requireNativePublicationBackend();
|
|
912
|
+
assertNativePublicationPath(backend, dirname(source));
|
|
913
|
+
nativeRenameNoReplace(backend, source, destination);
|
|
914
|
+
return true;
|
|
915
|
+
}
|
|
916
|
+
catch (error) {
|
|
917
|
+
if (hasCode(error, 'EEXIST') || hasCode(error, 'ENOENT'))
|
|
918
|
+
return false;
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
function positiveInteger(value, name) {
|
|
923
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
924
|
+
throw new RepositoryStoreConfigError(`${name} must be a positive safe integer.`);
|
|
925
|
+
return value;
|
|
926
|
+
}
|
|
927
|
+
function nonnegativeInteger(value, name) {
|
|
928
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
929
|
+
throw new RepositoryStoreConfigError(`${name} must be a nonnegative safe integer.`);
|
|
930
|
+
return value;
|
|
931
|
+
}
|
|
932
|
+
//# sourceMappingURL=lease.js.map
|