@neottia/repository-store 0.2.0 → 0.3.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/README.md +8 -5
- package/binding.gyp +12 -2
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +2 -2
- package/dist/files.js.map +1 -1
- package/dist/internal/filesystem.d.ts +3 -1
- package/dist/internal/filesystem.d.ts.map +1 -1
- package/dist/internal/filesystem.js +95 -13
- package/dist/internal/filesystem.js.map +1 -1
- package/dist/internal/model.d.ts +1 -0
- package/dist/internal/model.d.ts.map +1 -1
- package/dist/internal/model.js.map +1 -1
- package/dist/internal/platform-backend.d.ts +24 -0
- package/dist/internal/platform-backend.d.ts.map +1 -0
- package/dist/internal/platform-backend.js +120 -0
- package/dist/internal/platform-backend.js.map +1 -0
- package/dist/internal/publication.d.ts.map +1 -1
- package/dist/internal/publication.js +28 -16
- package/dist/internal/publication.js.map +1 -1
- package/dist/lease.d.ts +3 -0
- package/dist/lease.d.ts.map +1 -1
- package/dist/lease.js +241 -52
- package/dist/lease.js.map +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +4 -1
- package/dist/paths.js.map +1 -1
- package/dist/sqlite/adapter.d.ts +1 -0
- package/dist/sqlite/adapter.d.ts.map +1 -1
- package/dist/sqlite/adapter.js +9 -0
- package/dist/sqlite/adapter.js.map +1 -1
- package/dist/sqlite/cache-publication.d.ts.map +1 -1
- package/dist/sqlite/cache-publication.js +2 -2
- package/dist/sqlite/cache-publication.js.map +1 -1
- package/dist/sqlite/cache.d.ts.map +1 -1
- package/dist/sqlite/cache.js +1 -2
- package/dist/sqlite/cache.js.map +1 -1
- package/dist/transaction/apply.js +2 -2
- package/dist/transaction/apply.js.map +1 -1
- package/dist/transaction/journal.js +2 -2
- package/dist/transaction/journal.js.map +1 -1
- package/dist/transaction/recovery.js +2 -2
- package/dist/transaction/recovery.js.map +1 -1
- package/native/{rename_noreplace.cc → backend.cc} +47 -49
- package/native/freebsd_backend.cc +83 -0
- package/native/linux_backend.cc +45 -0
- package/native/macos_backend.cc +84 -0
- package/native/platform_backend.h +17 -0
- package/native/unsupported_backend.cc +28 -0
- package/native/windows_backend.cc +179 -0
- package/package.json +5 -5
- package/scripts/install-native.mjs +2 -2
- package/dist/internal/native-publication.d.ts +0 -9
- package/dist/internal/native-publication.d.ts.map +0 -1
- package/dist/internal/native-publication.js +0 -65
- package/dist/internal/native-publication.js.map +0 -1
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { closeSync, constants as fsConstants,
|
|
1
|
+
import { closeSync, constants as fsConstants, linkSync, openSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, join } from 'node:path';
|
|
3
3
|
import { PathSafetyError, RecoveryError, UnsupportedRuntimeError } from '../errors.js';
|
|
4
4
|
import { computeByteRevision } from '../revision.js';
|
|
5
5
|
import { emitFilesystemFault } from './fault-injection.js';
|
|
6
|
-
import { assertSafeRegular, captureDirectories, ensurePrivateDirectory, hasCode, noFollowFlag, readRegularFile, revalidateDirectories, sameIdentity, syncDirectory, syncFileDescriptor, } from './filesystem.js';
|
|
7
|
-
import {
|
|
6
|
+
import { assertSafeRegular, captureDirectories, ensurePrivateDirectory, fstatSync, hasCode, lstatSync, noFollowFlag, readRegularFile, revalidateDirectories, sameIdentity, syncDirectory, syncFileDescriptor, } from './filesystem.js';
|
|
7
|
+
import { assertPlatformFilesystem, platformRenameNoReplace, requireRepositoryPlatformBackend, } from './platform-backend.js';
|
|
8
8
|
export function publishExactRegularFile(path, bytes, expected, token) {
|
|
9
9
|
const parent = dirname(path);
|
|
10
10
|
ensurePrivateDirectory(parent);
|
|
11
11
|
const parents = captureDirectories(parent);
|
|
12
12
|
const temporary = join(parent, `.${basename(path)}.${token}.repository-store.tmp`);
|
|
13
13
|
const evacuated = join(parent, `.${basename(path)}.${token}.repository-store.evacuated`);
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
|
|
14
|
+
const platformBackend = expected === undefined ? undefined : requireRepositoryPlatformBackend();
|
|
15
|
+
if (platformBackend !== undefined)
|
|
16
|
+
assertPlatformFilesystem(platformBackend, parent);
|
|
17
17
|
let descriptor;
|
|
18
18
|
let temporaryIdentity;
|
|
19
19
|
let evacuatedIdentity;
|
|
@@ -35,17 +35,22 @@ export function publishExactRegularFile(path, bytes, expected, token) {
|
|
|
35
35
|
if (expected !== undefined) {
|
|
36
36
|
emitFilesystemFault(bytes === null ? 'before-exact-removal' : 'before-exact-publication', path);
|
|
37
37
|
emitFilesystemFault('before-exact-evacuation', evacuated);
|
|
38
|
-
moveNoReplace(
|
|
38
|
+
moveNoReplace(platformBackend, path, evacuated, 'evacuate destination');
|
|
39
39
|
emitFilesystemFault('destination-evacuated', path);
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const observedIdentity = lstatSync(evacuated);
|
|
41
|
+
evacuatedIdentity = observedIdentity;
|
|
42
|
+
assertSafeRegular(observedIdentity, evacuated);
|
|
42
43
|
const evacuatedBytes = readRegularFile(evacuated, expected.maxBytes);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
restoreEvacuated(
|
|
44
|
+
const evacuatedRevision = computeByteRevision(evacuatedBytes);
|
|
45
|
+
if (!sameIdentity(expected.identity, observedIdentity) || evacuatedRevision !== expected.revision) {
|
|
46
|
+
restoreEvacuated(platformBackend, evacuated, path, parent);
|
|
46
47
|
evacuatedIdentity = undefined;
|
|
47
48
|
throw new PathSafetyError(`Destination changed during publication: ${path}`, 'IDENTITY_CHANGED', {
|
|
48
49
|
path,
|
|
50
|
+
expectedIdentity: identityEvidence(expected.identity),
|
|
51
|
+
observedIdentity: identityEvidence(observedIdentity),
|
|
52
|
+
expectedRevision: expected.revision,
|
|
53
|
+
observedRevision: evacuatedRevision,
|
|
49
54
|
});
|
|
50
55
|
}
|
|
51
56
|
revalidateDirectories(parents);
|
|
@@ -61,7 +66,7 @@ export function publishExactRegularFile(path, bytes, expected, token) {
|
|
|
61
66
|
catch (error) {
|
|
62
67
|
if (hasCode(error, 'EEXIST')) {
|
|
63
68
|
if (evacuatedIdentity !== undefined)
|
|
64
|
-
restoreEvacuated(
|
|
69
|
+
restoreEvacuated(platformBackend, evacuated, path, parent);
|
|
65
70
|
evacuatedIdentity = undefined;
|
|
66
71
|
throw new PathSafetyError(`Destination appeared during publication: ${path}`, 'IDENTITY_CHANGED');
|
|
67
72
|
}
|
|
@@ -125,7 +130,7 @@ export function recoverExactPublication(path, token, allowedRevisions, maxBytes)
|
|
|
125
130
|
throw error;
|
|
126
131
|
}
|
|
127
132
|
if (current === undefined) {
|
|
128
|
-
restoreEvacuated(
|
|
133
|
+
restoreEvacuated(requireRepositoryPlatformBackend(), evacuated, path, parent);
|
|
129
134
|
return;
|
|
130
135
|
}
|
|
131
136
|
if (sameIdentity(evidence, current)) {
|
|
@@ -175,7 +180,7 @@ function recoverTemporaryPublication(path, temporary, token, allowedRevisions, m
|
|
|
175
180
|
}
|
|
176
181
|
function restoreEvacuated(backend, evacuated, path, parent) {
|
|
177
182
|
try {
|
|
178
|
-
|
|
183
|
+
platformRenameNoReplace(backend, evacuated, path);
|
|
179
184
|
}
|
|
180
185
|
catch (error) {
|
|
181
186
|
if (hasCode(error, 'EEXIST'))
|
|
@@ -186,7 +191,7 @@ function restoreEvacuated(backend, evacuated, path, parent) {
|
|
|
186
191
|
}
|
|
187
192
|
function moveNoReplace(backend, source, destination, operation) {
|
|
188
193
|
try {
|
|
189
|
-
|
|
194
|
+
platformRenameNoReplace(backend, source, destination);
|
|
190
195
|
}
|
|
191
196
|
catch (error) {
|
|
192
197
|
if (hasCode(error, 'EEXIST'))
|
|
@@ -209,6 +214,13 @@ function unsupportedLink(error, path) {
|
|
|
209
214
|
return new UnsupportedRuntimeError(`Filesystem does not support exclusive regular-file publication: ${path}`);
|
|
210
215
|
return error;
|
|
211
216
|
}
|
|
217
|
+
function identityEvidence(identity) {
|
|
218
|
+
return {
|
|
219
|
+
birthtimeMs: identity.birthtimeMs,
|
|
220
|
+
dev: identity.dev,
|
|
221
|
+
ino: identity.ino,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
212
224
|
function artifactExists(path) {
|
|
213
225
|
try {
|
|
214
226
|
lstatSync(path);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publication.js","sourceRoot":"","sources":["../../src/internal/publication.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"publication.js","sourceRoot":"","sources":["../../src/internal/publication.ts"],"names":[],"mappings":"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"}
|
package/dist/lease.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export declare function withRepositoryLease<T>(root: ManagedRoot, operation: (le
|
|
|
13
13
|
export declare function assertLiveLease(root: ManagedRoot, lease: RepositoryLease): void;
|
|
14
14
|
export declare function trackLeaseOperation<T>(lease: RepositoryLease, operation: Promise<T>): Promise<T>;
|
|
15
15
|
export declare function checkControl(control: OperationControl): void;
|
|
16
|
+
export declare function interruptedClaimPeer(path: string, observed?: Stats, allowReleasePeer?: boolean): {
|
|
17
|
+
readonly token: string;
|
|
18
|
+
} | undefined;
|
|
16
19
|
export interface ReleaseExpectation {
|
|
17
20
|
readonly kind: 'file' | 'directory';
|
|
18
21
|
readonly identity: Stats;
|
package/dist/lease.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../src/lease.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../src/lease.ts"],"names":[],"mappings":"AAEA,OAAO,EAWL,KAAK,KAAK,EACX,MAAM,SAAS,CAAC;AAmBjB,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAQ7B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAE9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAGD,MAAM,WAAW,sBAAuB,SAAQ,gBAAgB;IAC9D,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAEzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AA0BD,wBAAsB,mBAAmB,CAAC,CAAC,EACzC,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,OAAO,CAAC,CAAC,CAAC,EACjD,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,CAAC,CAAC,CAyIZ;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,GAAG,IAAI,CAkB/E;AAGD,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAUhG;AAGD,wBAAgB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAI5D;AA4fD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,KAAK,EAChB,gBAAgB,UAAQ,GACvB;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CA4BxC;AA4HD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;CAChC;AAGD,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,kBAAkB,EAC5B,aAAa,CAAC,EAAE,kBAAkB,GACjC,IAAI,CA6CN;AAgSD,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/lease.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { closeSync, constants as fsConstants,
|
|
3
|
+
import { closeSync, constants as fsConstants, linkSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { hostname } from 'node:os';
|
|
5
5
|
import { basename, dirname, join } from 'node:path';
|
|
6
6
|
import { LeaseContentionError, PathSafetyError, RepositoryStoreConfigError, ResourceLimitError } from './errors.js';
|
|
7
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';
|
|
8
|
+
import { assertSafeRegular, captureDirectories, ensurePrivateDirectory, fstatSync, hasCode, lstatSync, readRegularFile, readRegularFileWithIdentity, revalidateDirectories, sameIdentity, syncDirectory, syncFileDescriptor, } from './internal/filesystem.js';
|
|
9
9
|
import { createRepositoryLease, getLeaseState, getRootState, } from './internal/model.js';
|
|
10
|
-
import {
|
|
10
|
+
import { assertPlatformFilesystem, platformRenameNoReplace, requireRepositoryPlatformBackend, } from './internal/platform-backend.js';
|
|
11
11
|
const authorityContext = new AsyncLocalStorage();
|
|
12
12
|
const authorityQueueTails = new Map();
|
|
13
13
|
const DEFAULT_STALE_MS = 60_000;
|
|
@@ -40,18 +40,38 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
40
40
|
let ownerIdentity;
|
|
41
41
|
let leaseState;
|
|
42
42
|
let lockPath = '';
|
|
43
|
+
let lockKind = 'directory';
|
|
43
44
|
let token = '';
|
|
44
45
|
try {
|
|
45
46
|
checkAcquisitionControl(deadline, explicitDeadline !== undefined, options.signal);
|
|
46
47
|
ensurePrivateDirectory(controlRoot);
|
|
47
|
-
const cleanupBackend =
|
|
48
|
-
|
|
48
|
+
const cleanupBackend = requireRepositoryPlatformBackend();
|
|
49
|
+
assertPlatformFilesystem(cleanupBackend, controlRoot);
|
|
50
|
+
lockKind = cleanupBackend.leaseArtifact;
|
|
49
51
|
const hostId = ensureHostId(controlRoot);
|
|
50
52
|
lockPath = join(controlRoot, 'authority.lease');
|
|
51
53
|
const claimPath = join(controlRoot, 'authority.claim');
|
|
52
54
|
token = randomBytes(32).toString('hex');
|
|
53
55
|
while (!acquired) {
|
|
54
56
|
checkAcquisitionControl(deadline, explicitDeadline !== undefined, options.signal);
|
|
57
|
+
if (lockKind === 'file') {
|
|
58
|
+
if (!acquireClaim(lockPath, controlRoot, token, hostId)) {
|
|
59
|
+
if (tryReclaimFileLease(lockPath, controlRoot, hostId, staleMs))
|
|
60
|
+
continue;
|
|
61
|
+
await waitForLeasePoll(pollMs, deadline, explicitDeadline !== undefined, options.signal);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
emitLeaseFault('lease-directory-created');
|
|
65
|
+
const owner = readOwner(lockPath);
|
|
66
|
+
if (owner.metadata.token !== token)
|
|
67
|
+
throw new LeaseContentionError('Repository file lease ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
68
|
+
lockIdentity = owner.identity;
|
|
69
|
+
ownerIdentity = owner.identity;
|
|
70
|
+
emitLeaseFault('lease-owner-written');
|
|
71
|
+
emitLeaseFault('lease-claim-removed');
|
|
72
|
+
acquired = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
55
75
|
if (!acquireClaim(claimPath, controlRoot, token, hostId)) {
|
|
56
76
|
if (tryReclaimClaim(claimPath, lockPath, controlRoot, hostId, staleMs))
|
|
57
77
|
continue;
|
|
@@ -87,7 +107,7 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
87
107
|
syncDirectory(controlRoot);
|
|
88
108
|
if (!removeOwnedClaim(claimPath, token)) {
|
|
89
109
|
claimOwnershipUncertain = true;
|
|
90
|
-
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
110
|
+
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity, lockKind);
|
|
91
111
|
throw new LeaseContentionError('Repository initialization claim ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
92
112
|
}
|
|
93
113
|
emitLeaseFault('lease-claim-removed');
|
|
@@ -109,6 +129,7 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
109
129
|
authorityId: rootState.authorityId,
|
|
110
130
|
token,
|
|
111
131
|
lockPath,
|
|
132
|
+
lockKind,
|
|
112
133
|
lockIdentity,
|
|
113
134
|
ownerIdentity,
|
|
114
135
|
pendingOperations: new Set(),
|
|
@@ -131,7 +152,7 @@ export async function withRepositoryLease(root, operation, options = {}) {
|
|
|
131
152
|
leaseState.active = false;
|
|
132
153
|
}
|
|
133
154
|
if (acquired && lockIdentity !== undefined && ownerIdentity !== undefined)
|
|
134
|
-
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
155
|
+
releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity, lockKind);
|
|
135
156
|
turn.release();
|
|
136
157
|
}
|
|
137
158
|
}
|
|
@@ -142,7 +163,7 @@ export function assertLiveLease(root, lease) {
|
|
|
142
163
|
throw new RepositoryStoreConfigError('Lease and managed root have different authorities.', 'AUTHORITY_MISMATCH');
|
|
143
164
|
if (!leaseState.active)
|
|
144
165
|
throw new LeaseContentionError('Repository lease is no longer active.', 'LEASE_BUSY');
|
|
145
|
-
if (!ownedLeaseIdentityMatches(leaseState.lockPath, leaseState.token, leaseState.lockIdentity, leaseState.ownerIdentity)) {
|
|
166
|
+
if (!ownedLeaseIdentityMatches(leaseState.lockPath, leaseState.token, leaseState.lockIdentity, leaseState.ownerIdentity, leaseState.lockKind)) {
|
|
146
167
|
leaseState.active = false;
|
|
147
168
|
throw new LeaseContentionError('Repository lease ownership changed.', 'LEASE_OWNER_UNKNOWN');
|
|
148
169
|
}
|
|
@@ -196,25 +217,22 @@ function ownerMetadata(token, hostId) {
|
|
|
196
217
|
function ensureHostId(controlRoot) {
|
|
197
218
|
const path = join(controlRoot, 'host-id');
|
|
198
219
|
try {
|
|
199
|
-
|
|
200
|
-
if (!/^[0-9a-f]{64}$/u.test(value))
|
|
201
|
-
throw new PathSafetyError('Repository host-id is malformed.');
|
|
202
|
-
return value;
|
|
220
|
+
return readHostId(path, controlRoot);
|
|
203
221
|
}
|
|
204
222
|
catch (error) {
|
|
205
223
|
if (!hasCode(error, 'ENOENT'))
|
|
206
224
|
throw error;
|
|
207
225
|
}
|
|
208
226
|
const value = randomBytes(32).toString('hex');
|
|
227
|
+
const candidate = join(controlRoot, `.host-id-${randomBytes(16).toString('hex')}.tmp`);
|
|
228
|
+
let descriptor;
|
|
209
229
|
try {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
closeSync(descriptor);
|
|
217
|
-
}
|
|
230
|
+
descriptor = openSync(candidate, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL, 0o600);
|
|
231
|
+
writeFileSync(descriptor, `${value}\n`);
|
|
232
|
+
syncFileDescriptor(descriptor, candidate);
|
|
233
|
+
closeSync(descriptor);
|
|
234
|
+
descriptor = undefined;
|
|
235
|
+
platformRenameNoReplace(requireRepositoryPlatformBackend(), candidate, path);
|
|
218
236
|
syncDirectory(controlRoot);
|
|
219
237
|
return value;
|
|
220
238
|
}
|
|
@@ -223,6 +241,98 @@ function ensureHostId(controlRoot) {
|
|
|
223
241
|
throw error;
|
|
224
242
|
return ensureHostId(controlRoot);
|
|
225
243
|
}
|
|
244
|
+
finally {
|
|
245
|
+
if (descriptor !== undefined)
|
|
246
|
+
closeSync(descriptor);
|
|
247
|
+
rmSync(candidate, { force: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function readHostId(path, controlRoot) {
|
|
251
|
+
const identity = lstatSync(path);
|
|
252
|
+
if (identity.isSymbolicLink() || !identity.isFile())
|
|
253
|
+
throw new PathSafetyError('Repository host-id has an unsafe identity.', 'UNSAFE_LINK');
|
|
254
|
+
if (identity.nlink !== 1 && identity.nlink !== 2)
|
|
255
|
+
throw new PathSafetyError('Repository host-id has an unsafe hard-link count.', 'UNSAFE_HARD_LINK');
|
|
256
|
+
if (identity.nlink === 2) {
|
|
257
|
+
const prefix = '.host-id-';
|
|
258
|
+
const peers = readdirSync(controlRoot).filter((name) => {
|
|
259
|
+
if (!name.startsWith(prefix) || !name.endsWith('.tmp'))
|
|
260
|
+
return false;
|
|
261
|
+
const token = name.slice(prefix.length, -'.tmp'.length);
|
|
262
|
+
if (!/^[0-9a-f]{32}$/u.test(token))
|
|
263
|
+
return false;
|
|
264
|
+
try {
|
|
265
|
+
const peer = lstatSync(join(controlRoot, name));
|
|
266
|
+
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(identity, peer);
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (hasCode(error, 'ENOENT'))
|
|
270
|
+
return false;
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
if (peers.length === 1) {
|
|
275
|
+
rmSync(join(controlRoot, peers[0]));
|
|
276
|
+
syncDirectory(controlRoot);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
const stabilized = lstatSync(path);
|
|
280
|
+
if (stabilized.nlink !== 1 || !sameIdentity(identity, stabilized))
|
|
281
|
+
throw new PathSafetyError('Repository host-id hard link is not package-owned.', 'UNSAFE_HARD_LINK');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const value = new TextDecoder().decode(readRegularFile(path, MAX_LEASE_METADATA_BYTES)).trim();
|
|
285
|
+
if (!/^[0-9a-f]{64}$/u.test(value))
|
|
286
|
+
throw new PathSafetyError('Repository host-id is malformed.');
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
function tryReclaimFileLease(lockPath, controlRoot, hostId, staleMs) {
|
|
290
|
+
const controlDirectories = captureDirectories(controlRoot);
|
|
291
|
+
try {
|
|
292
|
+
const preliminary = lstatSync(lockPath);
|
|
293
|
+
if (preliminary.isSymbolicLink() || !preliminary.isFile() || (preliminary.nlink !== 1 && preliminary.nlink !== 2))
|
|
294
|
+
throw new PathSafetyError('Unsafe repository file lease.', 'UNSAFE_HARD_LINK');
|
|
295
|
+
const verifiedOwner = readOwner(lockPath, true, true);
|
|
296
|
+
if (!sameIdentity(preliminary, verifiedOwner.identity))
|
|
297
|
+
throw new LeaseContentionError('File lease identity changed before owner verification.', 'LEASE_OWNER_UNKNOWN');
|
|
298
|
+
if (Date.now() - verifiedOwner.identity.mtimeMs < staleMs)
|
|
299
|
+
return false;
|
|
300
|
+
if (!conclusivelyDead(verifiedOwner.metadata, hostId))
|
|
301
|
+
return false;
|
|
302
|
+
normalizeInterruptedClaimLink(lockPath, controlRoot, true, verifiedOwner.metadata.token);
|
|
303
|
+
const normalizedOwner = readOwner(lockPath);
|
|
304
|
+
if (!sameIdentity(preliminary, normalizedOwner.identity) ||
|
|
305
|
+
normalizedOwner.metadata.token !== verifiedOwner.metadata.token)
|
|
306
|
+
throw new LeaseContentionError('File lease owner changed during reclamation.', 'LEASE_OWNER_UNKNOWN');
|
|
307
|
+
const quarantine = `${lockPath}.release-${normalizedOwner.metadata.token}`;
|
|
308
|
+
const expected = {
|
|
309
|
+
kind: 'file',
|
|
310
|
+
identity: normalizedOwner.identity,
|
|
311
|
+
token: normalizedOwner.metadata.token,
|
|
312
|
+
};
|
|
313
|
+
if (!quarantineNoReplace(lockPath, quarantine))
|
|
314
|
+
return false;
|
|
315
|
+
const moved = readOwner(quarantine);
|
|
316
|
+
if (!sameIdentity(normalizedOwner.identity, moved.identity) ||
|
|
317
|
+
moved.metadata.token !== normalizedOwner.metadata.token) {
|
|
318
|
+
restoreQuarantinedLease(quarantine, lockPath, expected, {
|
|
319
|
+
kind: 'file',
|
|
320
|
+
identity: moved.identity,
|
|
321
|
+
token: moved.metadata.token,
|
|
322
|
+
});
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
rmSync(quarantine);
|
|
326
|
+
syncDirectory(controlRoot);
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (hasCode(error, 'ENOENT') || (error instanceof PathSafetyError && error.code === 'IDENTITY_CHANGED'))
|
|
331
|
+
return exactPathWasReleased(lockPath, controlDirectories, error, false);
|
|
332
|
+
if (error instanceof PathSafetyError || error instanceof ResourceLimitError)
|
|
333
|
+
throw error;
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
226
336
|
}
|
|
227
337
|
function tryReclaimLockedLease(lockPath, controlRoot, hostId, staleMs) {
|
|
228
338
|
const controlDirectories = captureDirectories(controlRoot);
|
|
@@ -454,17 +564,22 @@ function cleanupOwnedInitialization(lockPath, controlRoot, token, expectedLock)
|
|
|
454
564
|
restoreQuarantinedLease(quarantine, lockPath, releaseExpectation);
|
|
455
565
|
}
|
|
456
566
|
}
|
|
457
|
-
function normalizeInterruptedClaimLink(claimPath, controlRoot) {
|
|
567
|
+
function normalizeInterruptedClaimLink(claimPath, controlRoot, allowReleasePeer = false, expectedToken) {
|
|
458
568
|
const claim = lstatSync(claimPath);
|
|
459
569
|
if (!claim.isFile() || claim.isSymbolicLink() || claim.nlink === 1)
|
|
460
570
|
return;
|
|
461
571
|
if (claim.nlink !== 2)
|
|
462
572
|
throw new PathSafetyError('Unsafe repository claim hard-link count.', 'UNSAFE_HARD_LINK');
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
const
|
|
466
|
-
const
|
|
467
|
-
if (
|
|
573
|
+
const base = basename(claimPath);
|
|
574
|
+
const prefixes = [`${base}.prepare-`, ...(allowReleasePeer ? [`${base}.release-`] : [])];
|
|
575
|
+
const candidates = readdirSync(dirname(claimPath)).flatMap((name) => {
|
|
576
|
+
const prefix = prefixes.find((candidate) => name.startsWith(candidate));
|
|
577
|
+
if (prefix === undefined)
|
|
578
|
+
return [];
|
|
579
|
+
return [{ name, token: name.slice(prefix.length) }];
|
|
580
|
+
});
|
|
581
|
+
const matching = candidates.filter(({ name, token }) => {
|
|
582
|
+
if (!/^[0-9a-f-]{32,64}$/u.test(token) || (expectedToken !== undefined && token !== expectedToken))
|
|
468
583
|
return false;
|
|
469
584
|
const stat = lstatSync(join(dirname(claimPath), name));
|
|
470
585
|
return stat.isFile() && !stat.isSymbolicLink() && sameIdentity(claim, stat);
|
|
@@ -475,7 +590,7 @@ function normalizeInterruptedClaimLink(claimPath, controlRoot) {
|
|
|
475
590
|
return;
|
|
476
591
|
throw new PathSafetyError('Repository claim hard link is not package-owned.');
|
|
477
592
|
}
|
|
478
|
-
rmSync(join(dirname(claimPath), matching[0]));
|
|
593
|
+
rmSync(join(dirname(claimPath), matching[0].name));
|
|
479
594
|
syncDirectory(controlRoot);
|
|
480
595
|
}
|
|
481
596
|
function conclusivelyDead(owner, hostId) {
|
|
@@ -503,9 +618,11 @@ function readProcessStart(pid) {
|
|
|
503
618
|
return undefined;
|
|
504
619
|
}
|
|
505
620
|
}
|
|
506
|
-
function readOwner(path, allowInterruptedClaimLink = false) {
|
|
621
|
+
function readOwner(path, allowInterruptedClaimLink = false, allowInterruptedReleaseLink = false) {
|
|
507
622
|
const initialIdentity = allowInterruptedClaimLink ? lstatSync(path) : undefined;
|
|
508
|
-
const stagingPeer = allowInterruptedClaimLink
|
|
623
|
+
const stagingPeer = allowInterruptedClaimLink
|
|
624
|
+
? interruptedClaimPeer(path, initialIdentity, allowInterruptedReleaseLink)
|
|
625
|
+
: undefined;
|
|
509
626
|
let file;
|
|
510
627
|
try {
|
|
511
628
|
file = readRegularFileWithIdentity(path, MAX_LEASE_METADATA_BYTES, {
|
|
@@ -516,7 +633,15 @@ function readOwner(path, allowInterruptedClaimLink = false) {
|
|
|
516
633
|
const recoverableReadRace = error instanceof PathSafetyError && (error.code === 'IDENTITY_CHANGED' || error.code === 'UNSAFE_HARD_LINK');
|
|
517
634
|
if (initialIdentity === undefined || !recoverableReadRace)
|
|
518
635
|
throw error;
|
|
519
|
-
|
|
636
|
+
let stabilized;
|
|
637
|
+
try {
|
|
638
|
+
stabilized = lstatSync(path);
|
|
639
|
+
}
|
|
640
|
+
catch (stabilizationError) {
|
|
641
|
+
if (hasCode(stabilizationError, 'ENOENT'))
|
|
642
|
+
throw new PathSafetyError('Interrupted claim was released during owner verification.', 'IDENTITY_CHANGED');
|
|
643
|
+
throw stabilizationError;
|
|
644
|
+
}
|
|
520
645
|
if (stabilized.isSymbolicLink() || !stabilized.isFile())
|
|
521
646
|
throw error;
|
|
522
647
|
if (!sameIdentity(initialIdentity, stabilized))
|
|
@@ -538,27 +663,56 @@ function readOwner(path, allowInterruptedClaimLink = false) {
|
|
|
538
663
|
const finalIdentity = allowInterruptedClaimLink ? lstatSync(path) : undefined;
|
|
539
664
|
if (finalIdentity !== undefined && !sameIdentity(file.identity, finalIdentity))
|
|
540
665
|
throw new PathSafetyError('Interrupted claim identity changed after owner verification.', 'IDENTITY_CHANGED');
|
|
541
|
-
const finalStagingPeer = allowInterruptedClaimLink && finalIdentity !== undefined
|
|
666
|
+
const finalStagingPeer = allowInterruptedClaimLink && finalIdentity !== undefined
|
|
667
|
+
? interruptedClaimPeer(path, finalIdentity, allowInterruptedReleaseLink)
|
|
668
|
+
: undefined;
|
|
542
669
|
if ([stagingPeer, finalStagingPeer].some((peer) => peer !== undefined && owner.token !== peer.token))
|
|
543
670
|
throw new PathSafetyError('Interrupted claim token does not match its package-owned staging link.');
|
|
544
671
|
return { metadata: owner, identity: file.identity };
|
|
545
672
|
}
|
|
546
|
-
function interruptedClaimPeer(path, observed) {
|
|
673
|
+
export function interruptedClaimPeer(path, observed, allowReleasePeer = false) {
|
|
547
674
|
const stat = observed ?? lstatSync(path);
|
|
548
675
|
if (stat.isSymbolicLink() || !stat.isFile() || (stat.nlink !== 1 && stat.nlink !== 2))
|
|
549
676
|
throw new PathSafetyError('Unsafe interrupted repository claim.', 'UNSAFE_HARD_LINK');
|
|
550
677
|
if (stat.nlink === 1)
|
|
551
678
|
return undefined;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (
|
|
679
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
680
|
+
const peers = matchingInterruptedClaimPeers(path, stat, allowReleasePeer);
|
|
681
|
+
if (peers.length === 1)
|
|
682
|
+
return peers[0];
|
|
683
|
+
let stabilized;
|
|
684
|
+
try {
|
|
685
|
+
stabilized = lstatSync(path);
|
|
686
|
+
}
|
|
687
|
+
catch (stabilizationError) {
|
|
688
|
+
if (hasCode(stabilizationError, 'ENOENT'))
|
|
689
|
+
throw new PathSafetyError('Interrupted claim was released during peer verification.', 'IDENTITY_CHANGED');
|
|
690
|
+
throw stabilizationError;
|
|
691
|
+
}
|
|
692
|
+
if (stabilized.isSymbolicLink() || !stabilized.isFile())
|
|
693
|
+
throw new PathSafetyError('Interrupted claim path became unsafe during peer verification.', 'UNSAFE_HARD_LINK');
|
|
694
|
+
if (!sameIdentity(stat, stabilized))
|
|
695
|
+
throw new PathSafetyError('Interrupted claim identity changed during peer verification.', 'IDENTITY_CHANGED');
|
|
696
|
+
if (stabilized.nlink === 1)
|
|
697
|
+
return undefined;
|
|
698
|
+
if (stabilized.nlink !== 2)
|
|
699
|
+
break;
|
|
700
|
+
}
|
|
701
|
+
throw new PathSafetyError('Interrupted claim hard link is not package-owned.', 'UNSAFE_HARD_LINK');
|
|
702
|
+
}
|
|
703
|
+
function matchingInterruptedClaimPeers(path, identity, allowReleasePeer) {
|
|
704
|
+
const base = basename(path);
|
|
705
|
+
const prefixes = [`${base}.prepare-`, ...(allowReleasePeer ? [`${base}.release-`] : [])];
|
|
706
|
+
return readdirSync(dirname(path)).flatMap((name) => {
|
|
707
|
+
const prefix = prefixes.find((candidate) => name.startsWith(candidate));
|
|
708
|
+
if (prefix === undefined)
|
|
555
709
|
return [];
|
|
556
710
|
const token = name.slice(prefix.length);
|
|
557
711
|
if (!/^[0-9a-f-]{32,64}$/u.test(token))
|
|
558
712
|
return [];
|
|
559
713
|
try {
|
|
560
714
|
const peer = lstatSync(join(dirname(path), name));
|
|
561
|
-
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(
|
|
715
|
+
return peer.isFile() && !peer.isSymbolicLink() && sameIdentity(identity, peer) ? [{ token }] : [];
|
|
562
716
|
}
|
|
563
717
|
catch (error) {
|
|
564
718
|
if (hasCode(error, 'ENOENT'))
|
|
@@ -566,14 +720,12 @@ function interruptedClaimPeer(path, observed) {
|
|
|
566
720
|
throw error;
|
|
567
721
|
}
|
|
568
722
|
});
|
|
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
723
|
}
|
|
576
|
-
function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity) {
|
|
724
|
+
function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity, lockKind) {
|
|
725
|
+
if (lockKind === 'file') {
|
|
726
|
+
releaseOwnedFileLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
577
729
|
const releasePath = `${lockPath}.release-${token}`;
|
|
578
730
|
const releaseExpectation = {
|
|
579
731
|
kind: 'directory',
|
|
@@ -621,13 +773,44 @@ function releaseOwnedLease(lockPath, controlRoot, token, lockIdentity, ownerIden
|
|
|
621
773
|
restoreQuarantinedLease(releasePath, lockPath, releaseExpectation);
|
|
622
774
|
}
|
|
623
775
|
}
|
|
776
|
+
function releaseOwnedFileLease(lockPath, controlRoot, token, lockIdentity, ownerIdentity) {
|
|
777
|
+
const releasePath = `${lockPath}.release-${token}`;
|
|
778
|
+
const expected = { kind: 'file', identity: lockIdentity, token };
|
|
779
|
+
let restorationSafe = false;
|
|
780
|
+
try {
|
|
781
|
+
const current = readOwner(lockPath);
|
|
782
|
+
if (current.metadata.token !== token ||
|
|
783
|
+
!sameIdentity(current.identity, lockIdentity) ||
|
|
784
|
+
!sameIdentity(current.identity, ownerIdentity))
|
|
785
|
+
return;
|
|
786
|
+
if (!quarantineNoReplace(lockPath, releasePath))
|
|
787
|
+
return;
|
|
788
|
+
restorationSafe = true;
|
|
789
|
+
const moved = readOwner(releasePath);
|
|
790
|
+
if (moved.metadata.token !== token || !sameIdentity(moved.identity, lockIdentity)) {
|
|
791
|
+
restoreQuarantinedLease(releasePath, lockPath, expected, {
|
|
792
|
+
kind: 'file',
|
|
793
|
+
identity: moved.identity,
|
|
794
|
+
token: moved.metadata.token,
|
|
795
|
+
});
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
rmSync(releasePath);
|
|
799
|
+
restorationSafe = false;
|
|
800
|
+
syncDirectory(controlRoot);
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
if (restorationSafe)
|
|
804
|
+
restoreQuarantinedLease(releasePath, lockPath, expected);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
624
807
|
export function restoreQuarantinedLease(quarantine, lockPath, expected, movedFromLock) {
|
|
625
808
|
const verification = `${quarantine}.restore-${randomBytes(16).toString('hex')}`;
|
|
626
809
|
let staged = false;
|
|
627
810
|
let preserveUnverified = false;
|
|
628
811
|
let restorationExpectation;
|
|
629
812
|
try {
|
|
630
|
-
|
|
813
|
+
platformRenameNoReplace(requireRepositoryPlatformBackend(), quarantine, verification);
|
|
631
814
|
staged = true;
|
|
632
815
|
restorationExpectation = releaseArtifactMatches(verification, expected)
|
|
633
816
|
? expected
|
|
@@ -639,7 +822,7 @@ export function restoreQuarantinedLease(quarantine, lockPath, expected, movedFro
|
|
|
639
822
|
return;
|
|
640
823
|
}
|
|
641
824
|
try {
|
|
642
|
-
|
|
825
|
+
platformRenameNoReplace(requireRepositoryPlatformBackend(), verification, lockPath);
|
|
643
826
|
staged = false;
|
|
644
827
|
}
|
|
645
828
|
catch (error) {
|
|
@@ -660,7 +843,7 @@ export function restoreQuarantinedLease(quarantine, lockPath, expected, movedFro
|
|
|
660
843
|
if (preserveUnverified ||
|
|
661
844
|
releaseArtifactMatches(verification, expected) ||
|
|
662
845
|
(movedFromLock !== undefined && releaseArtifactMatches(verification, movedFromLock)))
|
|
663
|
-
|
|
846
|
+
platformRenameNoReplace(requireRepositoryPlatformBackend(), verification, quarantine);
|
|
664
847
|
}
|
|
665
848
|
catch {
|
|
666
849
|
}
|
|
@@ -705,7 +888,7 @@ function cleanupVerifiedReleaseArtifact(path, expected, releasePath = path) {
|
|
|
705
888
|
const match = /\.release-([0-9a-f-]{32,64})$/u.exec(basename(releasePath));
|
|
706
889
|
if (match?.[1] !== expected.token)
|
|
707
890
|
return;
|
|
708
|
-
|
|
891
|
+
platformRenameNoReplace(requireRepositoryPlatformBackend(), path, cleanup);
|
|
709
892
|
moved = true;
|
|
710
893
|
const identity = lstatSync(cleanup);
|
|
711
894
|
if (!sameIdentity(identity, expected.identity) || identity.isSymbolicLink())
|
|
@@ -748,7 +931,7 @@ function cleanupVerifiedReleaseArtifact(path, expected, releasePath = path) {
|
|
|
748
931
|
finally {
|
|
749
932
|
if (moved && !removed) {
|
|
750
933
|
try {
|
|
751
|
-
|
|
934
|
+
platformRenameNoReplace(requireRepositoryPlatformBackend(), cleanup, path);
|
|
752
935
|
}
|
|
753
936
|
catch {
|
|
754
937
|
}
|
|
@@ -780,8 +963,14 @@ function removeOwnedClaim(path, token) {
|
|
|
780
963
|
return false;
|
|
781
964
|
}
|
|
782
965
|
}
|
|
783
|
-
function ownedLeaseIdentityMatches(lockPath, token, lockIdentity, ownerIdentity) {
|
|
966
|
+
function ownedLeaseIdentityMatches(lockPath, token, lockIdentity, ownerIdentity, lockKind) {
|
|
784
967
|
try {
|
|
968
|
+
if (lockKind === 'file') {
|
|
969
|
+
const currentOwner = readOwner(lockPath);
|
|
970
|
+
return (currentOwner.metadata.token === token &&
|
|
971
|
+
sameIdentity(lockIdentity, currentOwner.identity) &&
|
|
972
|
+
sameIdentity(ownerIdentity, currentOwner.identity));
|
|
973
|
+
}
|
|
785
974
|
const currentLock = lstatSync(lockPath);
|
|
786
975
|
const currentOwner = readOwner(join(lockPath, 'owner.json'));
|
|
787
976
|
return (currentLock.isDirectory() &&
|
|
@@ -908,9 +1097,9 @@ function acquisitionTimeout(hasExplicitDeadline) {
|
|
|
908
1097
|
function quarantineNoReplace(source, destination) {
|
|
909
1098
|
emitFilesystemFault('before-lease-artifact-quarantine', source);
|
|
910
1099
|
try {
|
|
911
|
-
const backend =
|
|
912
|
-
|
|
913
|
-
|
|
1100
|
+
const backend = requireRepositoryPlatformBackend();
|
|
1101
|
+
assertPlatformFilesystem(backend, dirname(source));
|
|
1102
|
+
platformRenameNoReplace(backend, source, destination);
|
|
914
1103
|
return true;
|
|
915
1104
|
}
|
|
916
1105
|
catch (error) {
|