@cortexkit/antigravity-auth-core 3.0.3 → 3.0.4
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/agy-request-metadata.d.ts.map +1 -1
- package/dist/agy-request-metadata.js +9 -0
- package/dist/agy-request-metadata.js.map +1 -1
- package/dist/common-auth-embedded/claustrum/enrollment.d.ts +3 -2
- package/dist/common-auth-embedded/claustrum/enrollment.js +84 -42
- package/dist/common-auth-embedded/claustrum/roster.js +2 -2
- package/dist/common-auth-embedded/fs/atomic-write.d.ts +5 -0
- package/dist/common-auth-embedded/fs/atomic-write.js +21 -0
- package/dist/common-auth-embedded/fs/index.d.ts +2 -0
- package/dist/common-auth-embedded/fs/index.js +1 -0
- package/dist/common-auth-embedded/fs/private-files.d.ts +66 -0
- package/dist/common-auth-embedded/fs/private-files.js +302 -0
- package/dist/common-auth-embedded/fs/refresh-file-lock.d.ts +1 -1
- package/dist/common-auth-embedded/fs/refresh-file-lock.js +254 -37
- package/dist/common-auth-embedded/fs/with-lock.d.ts +3 -0
- package/dist/common-auth-embedded/fs/with-lock.js +4 -0
- package/dist/common-auth-embedded/logger/engine.d.ts +6 -8
- package/dist/common-auth-embedded/logger/engine.js +126 -11
- package/dist/common-auth-embedded/pi-slot/slot.d.ts +2 -0
- package/dist/common-auth-embedded/pi-slot/slot.js +34 -6
- package/dist/common-auth-embedded/source-output.json +83 -67
- package/dist/common-auth-embedded/store/mutate.js +2 -2
- package/dist/common-auth-embedded/store/pool.d.ts +1 -0
- package/dist/common-auth-embedded/store/pool.js +3 -0
- package/dist/common-auth-embedded/store/refresh-lock.d.ts +4 -0
- package/dist/common-auth-embedded/store/refresh-lock.js +5 -0
- package/dist/fingerprint.d.ts +2 -2
- package/dist/fingerprint.js +2 -2
- package/dist/model-registry.d.ts +4 -1
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +61 -104
- package/dist/model-registry.js.map +1 -1
- package/dist/quota-manager.d.ts.map +1 -1
- package/dist/quota-manager.js +3 -1
- package/dist/quota-manager.js.map +1 -1
- package/dist/transform/agy-generation.d.ts.map +1 -1
- package/dist/transform/agy-generation.js +11 -2
- package/dist/transform/agy-generation.js.map +1 -1
- package/dist/transform/model-resolver.d.ts.map +1 -1
- package/dist/transform/model-resolver.js +18 -4
- package/dist/transform/model-resolver.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdir, open, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { createPrivateBytes, createPrivateDirectoryExclusive, ensurePrivateDirectory, PrivateFileError, privateOperation, readPrivateText, verifyPrivate, windowsPrivateFiles, writePrivateAtomic, } from './private-files.js';
|
|
4
5
|
import { LockOwnershipError, lockPathFor } from './with-lock.js';
|
|
5
6
|
const setRefreshLockRenewalTimeout = globalThis.setTimeout.bind(globalThis);
|
|
6
7
|
const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
@@ -10,7 +11,8 @@ const clearRefreshLockRenewalTimeout = globalThis.clearTimeout.bind(globalThis);
|
|
|
10
11
|
// to hold — a lost race the caller should retry, not a fatal lock error.
|
|
11
12
|
export function isLostMarkerRaceError(error) {
|
|
12
13
|
const code = error?.code;
|
|
13
|
-
return code === 'ENOENT' ||
|
|
14
|
+
return (code === 'ENOENT' ||
|
|
15
|
+
(!windowsPrivateFiles && (code === 'EINVAL' || code === 'ENOTDIR')));
|
|
14
16
|
}
|
|
15
17
|
export async function acquireRefreshFileLock(options) {
|
|
16
18
|
const lockPath = lockPathFor(options.path, options.name);
|
|
@@ -19,6 +21,7 @@ export async function acquireRefreshFileLock(options) {
|
|
|
19
21
|
const now = options.now ?? Date.now;
|
|
20
22
|
let renewTimer = null;
|
|
21
23
|
let released = false;
|
|
24
|
+
let releaseConfirmed = false;
|
|
22
25
|
let loss;
|
|
23
26
|
let resolveLoss;
|
|
24
27
|
const lostPromise = new Promise((resolve) => {
|
|
@@ -61,6 +64,7 @@ export async function acquireRefreshFileLock(options) {
|
|
|
61
64
|
cancelRenewalTimer();
|
|
62
65
|
resolveLoss(loss);
|
|
63
66
|
}
|
|
67
|
+
let terminalIOError;
|
|
64
68
|
let renewalInFlight = null;
|
|
65
69
|
// Only the owner of this exclusively-created marker may remove or renew a
|
|
66
70
|
// lock. A contender recovering a stale marker can accidentally rename a newer
|
|
@@ -72,6 +76,18 @@ export async function acquireRefreshFileLock(options) {
|
|
|
72
76
|
const EVICT_TTL = 5_000;
|
|
73
77
|
const MAX_STEAL_ATTEMPTS = 8;
|
|
74
78
|
async function readOwner() {
|
|
79
|
+
if (windowsPrivateFiles) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(await readPrivateText(lockPath, true));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.code !== 'EISDIR' &&
|
|
85
|
+
error.reason !== 'wrong-kind')
|
|
86
|
+
throw error;
|
|
87
|
+
await verifyPrivate(lockPath, 'directory', true);
|
|
88
|
+
return JSON.parse(await readPrivateText(legacyOwnerPath, true));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
75
91
|
try {
|
|
76
92
|
return JSON.parse(await readFile(lockPath, 'utf8'));
|
|
77
93
|
}
|
|
@@ -83,6 +99,25 @@ export async function acquireRefreshFileLock(options) {
|
|
|
83
99
|
}
|
|
84
100
|
}
|
|
85
101
|
async function writeOwner() {
|
|
102
|
+
if (windowsPrivateFiles) {
|
|
103
|
+
const owner = await readOwner();
|
|
104
|
+
const remaining = Number(owner?.expiresAt) - now();
|
|
105
|
+
await writePrivateAtomic(lockPath, `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`, {
|
|
106
|
+
deadline: performance.now() + Math.max(0, remaining),
|
|
107
|
+
beforeRename: async () => {
|
|
108
|
+
const current = await readOwner();
|
|
109
|
+
if (current?.ownerId !== ownerId ||
|
|
110
|
+
!(Number(current?.expiresAt) > now()) ||
|
|
111
|
+
!(await ownsEvictionMarker())) {
|
|
112
|
+
throw new LockOwnershipError({
|
|
113
|
+
target: options.path,
|
|
114
|
+
name: options.name,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
86
121
|
// Readers must see a complete lease, even while renewal is writing.
|
|
87
122
|
const tempPath = `${lockPath}.${randomUUID()}.tmp`;
|
|
88
123
|
let handle;
|
|
@@ -94,6 +129,15 @@ export async function acquireRefreshFileLock(options) {
|
|
|
94
129
|
await handle.chmod(0o600);
|
|
95
130
|
await handle.close();
|
|
96
131
|
handle = undefined;
|
|
132
|
+
const current = await readOwner();
|
|
133
|
+
if (current?.ownerId !== ownerId ||
|
|
134
|
+
!(Number(current?.expiresAt) > now()) ||
|
|
135
|
+
!(await ownsEvictionMarker())) {
|
|
136
|
+
throw new LockOwnershipError({
|
|
137
|
+
target: options.path,
|
|
138
|
+
name: options.name,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
97
141
|
await rename(tempPath, lockPath);
|
|
98
142
|
created = false;
|
|
99
143
|
}
|
|
@@ -103,25 +147,56 @@ export async function acquireRefreshFileLock(options) {
|
|
|
103
147
|
await rm(tempPath, { force: true }).catch(() => { });
|
|
104
148
|
}
|
|
105
149
|
}
|
|
150
|
+
async function createLease() {
|
|
151
|
+
const bytes = `${JSON.stringify({ ownerId, expiresAt: now() + options.ttlMs })}\n`;
|
|
152
|
+
if (windowsPrivateFiles)
|
|
153
|
+
await createPrivateBytes(lockPath, bytes);
|
|
154
|
+
else
|
|
155
|
+
await writeFile(lockPath, bytes, {
|
|
156
|
+
encoding: 'utf8',
|
|
157
|
+
mode: 0o600,
|
|
158
|
+
flag: 'wx',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async function leaseContention(error) {
|
|
162
|
+
const code = error.code;
|
|
163
|
+
if (code === 'EEXIST') {
|
|
164
|
+
if (windowsPrivateFiles)
|
|
165
|
+
await readOwner();
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (code === 'EISDIR') {
|
|
169
|
+
if (windowsPrivateFiles) {
|
|
170
|
+
await verifyPrivate(lockPath, 'directory', true);
|
|
171
|
+
await readPrivateText(legacyOwnerPath, true);
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
106
177
|
async function tryAcquire() {
|
|
178
|
+
if (windowsPrivateFiles)
|
|
179
|
+
await ensurePrivateDirectory(dirname(lockPath));
|
|
107
180
|
try {
|
|
108
|
-
await
|
|
181
|
+
await createLease();
|
|
109
182
|
return true;
|
|
110
183
|
}
|
|
111
184
|
catch (error) {
|
|
112
185
|
const code = error.code;
|
|
113
|
-
if (
|
|
186
|
+
if (await leaseContention(error))
|
|
114
187
|
return false;
|
|
115
188
|
if (code === 'ENOENT') {
|
|
116
189
|
// Reboots can clear runtime temp directories before the next lock acquisition.
|
|
117
|
-
|
|
190
|
+
if (windowsPrivateFiles)
|
|
191
|
+
await ensurePrivateDirectory(dirname(lockPath));
|
|
192
|
+
else
|
|
193
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
118
194
|
try {
|
|
119
|
-
await
|
|
195
|
+
await createLease();
|
|
120
196
|
return true;
|
|
121
197
|
}
|
|
122
198
|
catch (retryError) {
|
|
123
|
-
|
|
124
|
-
if (retryCode === 'EEXIST' || retryCode === 'EISDIR')
|
|
199
|
+
if (await leaseContention(retryError))
|
|
125
200
|
return false;
|
|
126
201
|
throw retryError;
|
|
127
202
|
}
|
|
@@ -132,41 +207,108 @@ export async function acquireRefreshFileLock(options) {
|
|
|
132
207
|
async function backoff() {
|
|
133
208
|
await new Promise((resolve) => setTimeout(resolve, Math.floor(Math.random() * 4)));
|
|
134
209
|
}
|
|
135
|
-
async function
|
|
210
|
+
async function readLockState(notifyReady = true) {
|
|
211
|
+
if (notifyReady && options.onStep)
|
|
212
|
+
await options.onStep('lock-read-ready');
|
|
213
|
+
let owner;
|
|
136
214
|
try {
|
|
137
|
-
|
|
138
|
-
return Number(currentOwner?.expiresAt) > now();
|
|
215
|
+
owner = await readOwner();
|
|
139
216
|
}
|
|
140
|
-
catch {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (windowsPrivateFiles &&
|
|
219
|
+
error.code !== 'ENOENT')
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
const expiresAt = Number(owner?.expiresAt);
|
|
223
|
+
if (Number.isFinite(expiresAt) && expiresAt > now())
|
|
224
|
+
return { status: 'live' };
|
|
225
|
+
if (typeof owner?.ownerId === 'string' && Number.isFinite(expiresAt)) {
|
|
226
|
+
return {
|
|
227
|
+
status: 'stale',
|
|
228
|
+
identity: { kind: 'owner', ownerId: owner.ownerId, expiresAt },
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const current = await stat(lockPath);
|
|
233
|
+
const legacyExpiry = Number.isFinite(expiresAt)
|
|
234
|
+
? expiresAt
|
|
235
|
+
: current.mtimeMs + options.ttlMs;
|
|
236
|
+
if (legacyExpiry > now())
|
|
237
|
+
return { status: 'live' };
|
|
238
|
+
return {
|
|
239
|
+
status: 'stale',
|
|
240
|
+
identity: {
|
|
241
|
+
kind: 'stat',
|
|
242
|
+
dev: current.dev,
|
|
243
|
+
ino: current.ino,
|
|
244
|
+
mtimeMs: current.mtimeMs,
|
|
245
|
+
expiresAt: legacyExpiry,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
if (error.code === 'ENOENT') {
|
|
251
|
+
if (options.onStep)
|
|
252
|
+
await options.onStep('lock-missing');
|
|
253
|
+
return { status: 'missing' };
|
|
148
254
|
}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function sameStaleLock(observed, current) {
|
|
259
|
+
if (current.status !== 'stale')
|
|
260
|
+
return false;
|
|
261
|
+
const identity = current.identity;
|
|
262
|
+
if (observed.kind === 'owner' && identity.kind === 'owner') {
|
|
263
|
+
return (observed.ownerId === identity.ownerId &&
|
|
264
|
+
observed.expiresAt === identity.expiresAt);
|
|
149
265
|
}
|
|
266
|
+
return (observed.kind === 'stat' &&
|
|
267
|
+
identity.kind === 'stat' &&
|
|
268
|
+
observed.dev === identity.dev &&
|
|
269
|
+
observed.ino === identity.ino &&
|
|
270
|
+
observed.mtimeMs === identity.mtimeMs &&
|
|
271
|
+
observed.expiresAt === identity.expiresAt);
|
|
150
272
|
}
|
|
151
273
|
// Fail-closed: any read error means we do NOT own the marker.
|
|
152
274
|
async function ownsEvictionMarker() {
|
|
153
275
|
try {
|
|
154
|
-
const owner = JSON.parse(await
|
|
276
|
+
const owner = JSON.parse(await readPrivateText(evictOwnerPath, windowsPrivateFiles));
|
|
155
277
|
return owner?.ownerId === evictOwnerId;
|
|
156
278
|
}
|
|
157
|
-
catch {
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (windowsPrivateFiles &&
|
|
281
|
+
error.code !== 'ENOENT')
|
|
282
|
+
throw error;
|
|
158
283
|
return false;
|
|
159
284
|
}
|
|
160
285
|
}
|
|
286
|
+
async function removeLeaseObject(path) {
|
|
287
|
+
if (windowsPrivateFiles)
|
|
288
|
+
await privateOperation(path, 'remove', () => rm(path, { recursive: true, force: true }));
|
|
289
|
+
else
|
|
290
|
+
await rm(path, { recursive: true, force: true }).catch(() => { });
|
|
291
|
+
}
|
|
161
292
|
async function releaseEvictionMarker() {
|
|
162
293
|
if (await ownsEvictionMarker()) {
|
|
163
|
-
await
|
|
294
|
+
await removeLeaseObject(evictPath);
|
|
164
295
|
}
|
|
165
296
|
}
|
|
166
297
|
async function tryAcquireEvictionMarker() {
|
|
167
|
-
|
|
298
|
+
if (windowsPrivateFiles)
|
|
299
|
+
await createPrivateDirectoryExclusive(evictPath);
|
|
300
|
+
else
|
|
301
|
+
await mkdir(evictPath);
|
|
168
302
|
try {
|
|
169
|
-
|
|
303
|
+
const bytes = `${JSON.stringify({ ownerId: evictOwnerId, createdAt: now() })}\n`;
|
|
304
|
+
if (windowsPrivateFiles)
|
|
305
|
+
await createPrivateBytes(evictOwnerPath, bytes);
|
|
306
|
+
else
|
|
307
|
+
await writeFile(evictOwnerPath, bytes, {
|
|
308
|
+
encoding: 'utf8',
|
|
309
|
+
mode: 0o600,
|
|
310
|
+
flag: 'wx',
|
|
311
|
+
});
|
|
170
312
|
}
|
|
171
313
|
catch (error) {
|
|
172
314
|
// A competing contender can rename our just-created marker directory
|
|
@@ -193,6 +335,8 @@ export async function acquireRefreshFileLock(options) {
|
|
|
193
335
|
return 'missing';
|
|
194
336
|
throw error;
|
|
195
337
|
}
|
|
338
|
+
if (windowsPrivateFiles)
|
|
339
|
+
await verifyPrivate(evictPath, 'directory', true);
|
|
196
340
|
if (evictStat.mtimeMs + EVICT_TTL > now())
|
|
197
341
|
return 'fresh';
|
|
198
342
|
if (options.onStep)
|
|
@@ -208,7 +352,7 @@ export async function acquireRefreshFileLock(options) {
|
|
|
208
352
|
}
|
|
209
353
|
if (options.onStep)
|
|
210
354
|
await options.onStep('stale-marker-claimed');
|
|
211
|
-
await
|
|
355
|
+
await removeLeaseObject(claimedPath);
|
|
212
356
|
return 'recovered';
|
|
213
357
|
}
|
|
214
358
|
async function withEvictionMarker(action) {
|
|
@@ -217,8 +361,19 @@ export async function acquireRefreshFileLock(options) {
|
|
|
217
361
|
return false;
|
|
218
362
|
}
|
|
219
363
|
catch (error) {
|
|
220
|
-
if (error.code === 'EEXIST')
|
|
364
|
+
if (error.code === 'EEXIST') {
|
|
365
|
+
if (windowsPrivateFiles) {
|
|
366
|
+
await verifyPrivate(evictPath, 'directory', true);
|
|
367
|
+
try {
|
|
368
|
+
await readPrivateText(evictOwnerPath, true);
|
|
369
|
+
}
|
|
370
|
+
catch (ownerError) {
|
|
371
|
+
if (ownerError.code !== 'ENOENT')
|
|
372
|
+
throw ownerError;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
221
375
|
return false;
|
|
376
|
+
}
|
|
222
377
|
throw error;
|
|
223
378
|
}
|
|
224
379
|
try {
|
|
@@ -241,7 +396,9 @@ export async function acquireRefreshFileLock(options) {
|
|
|
241
396
|
try {
|
|
242
397
|
owner = await readOwner();
|
|
243
398
|
}
|
|
244
|
-
catch {
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (windowsPrivateFiles)
|
|
401
|
+
throw error;
|
|
245
402
|
return;
|
|
246
403
|
}
|
|
247
404
|
if (owner?.ownerId !== ownerId)
|
|
@@ -250,7 +407,9 @@ export async function acquireRefreshFileLock(options) {
|
|
|
250
407
|
await rm(lockPath, { recursive: true, force: true });
|
|
251
408
|
return;
|
|
252
409
|
}
|
|
253
|
-
catch {
|
|
410
|
+
catch (error) {
|
|
411
|
+
if (windowsPrivateFiles)
|
|
412
|
+
throw error;
|
|
254
413
|
await backoff();
|
|
255
414
|
}
|
|
256
415
|
}
|
|
@@ -306,6 +465,8 @@ export async function acquireRefreshFileLock(options) {
|
|
|
306
465
|
return;
|
|
307
466
|
}
|
|
308
467
|
await writeOwner();
|
|
468
|
+
if (options.onStep)
|
|
469
|
+
await options.onStep('renewal-write-finished');
|
|
309
470
|
if (!(await ownsEvictionMarker())) {
|
|
310
471
|
// If marker ownership cannot be read, stop claiming the lease
|
|
311
472
|
// and remove only a record that still carries our owner id.
|
|
@@ -319,7 +480,12 @@ export async function acquireRefreshFileLock(options) {
|
|
|
319
480
|
await options.onStep('renewal-marker-unavailable');
|
|
320
481
|
}
|
|
321
482
|
}
|
|
322
|
-
catch {
|
|
483
|
+
catch (error) {
|
|
484
|
+
if (windowsPrivateFiles && error instanceof PrivateFileError) {
|
|
485
|
+
terminalIOError = error;
|
|
486
|
+
recordLoss('renewal-failed');
|
|
487
|
+
shouldReschedule = false;
|
|
488
|
+
}
|
|
323
489
|
// Retry transient failures only while the lease can still be verified.
|
|
324
490
|
try {
|
|
325
491
|
const owner = await readOwner();
|
|
@@ -376,8 +542,12 @@ export async function acquireRefreshFileLock(options) {
|
|
|
376
542
|
acquired = await tryAcquire();
|
|
377
543
|
if (acquired)
|
|
378
544
|
break;
|
|
379
|
-
|
|
545
|
+
const observed = await readLockState();
|
|
546
|
+
if (observed.status === 'live')
|
|
380
547
|
return contended();
|
|
548
|
+
// Absence is a create opportunity, never evidence of an expired lease.
|
|
549
|
+
if (observed.status === 'missing')
|
|
550
|
+
continue;
|
|
381
551
|
if (options.onStep)
|
|
382
552
|
await options.onStep('stale-lock-observed');
|
|
383
553
|
try {
|
|
@@ -397,8 +567,11 @@ export async function acquireRefreshFileLock(options) {
|
|
|
397
567
|
continue;
|
|
398
568
|
}
|
|
399
569
|
try {
|
|
400
|
-
|
|
570
|
+
const confirmed = await readLockState();
|
|
571
|
+
if (confirmed.status === 'live')
|
|
401
572
|
return contended();
|
|
573
|
+
if (!sameStaleLock(observed.identity, confirmed))
|
|
574
|
+
continue;
|
|
402
575
|
// Verify marker ownership before removing a lock found not live.
|
|
403
576
|
if (!(await ownsEvictionMarker()))
|
|
404
577
|
return null;
|
|
@@ -408,7 +581,18 @@ export async function acquireRefreshFileLock(options) {
|
|
|
408
581
|
// our marker, so verify ownership again after the callback.
|
|
409
582
|
if (!(await ownsEvictionMarker()))
|
|
410
583
|
return null;
|
|
411
|
-
await
|
|
584
|
+
// Do not await a callback between this ownership check and the lock read.
|
|
585
|
+
const current = await readLockState(false);
|
|
586
|
+
if (!sameStaleLock(observed.identity, current))
|
|
587
|
+
continue;
|
|
588
|
+
// The final read and removal are not atomic. While this stale file
|
|
589
|
+
// exists, wx cannot create a successor; the marker excludes other
|
|
590
|
+
// evictors and release/renewal. An expired holder cannot renew: renewal
|
|
591
|
+
// rechecks expiry and marker ownership immediately before publishing.
|
|
592
|
+
// With the marker still owned, cooperating users cannot replace it in
|
|
593
|
+
// this window. Arbitrary suspension across marker expiry or filesystem
|
|
594
|
+
// changes still need a kernel lock for an atomic guarantee.
|
|
595
|
+
await removeLeaseObject(lockPath);
|
|
412
596
|
// Fence check 3: re-verify ownership after removing the stale lock.
|
|
413
597
|
if (!(await ownsEvictionMarker()))
|
|
414
598
|
return null;
|
|
@@ -442,6 +626,8 @@ export async function acquireRefreshFileLock(options) {
|
|
|
442
626
|
whenLost: () => lostPromise,
|
|
443
627
|
hasLost: () => loss !== undefined,
|
|
444
628
|
assertOwned: async () => {
|
|
629
|
+
if (terminalIOError)
|
|
630
|
+
throw terminalIOError;
|
|
445
631
|
let observed;
|
|
446
632
|
try {
|
|
447
633
|
const owner = await readOwner();
|
|
@@ -453,9 +639,11 @@ export async function acquireRefreshFileLock(options) {
|
|
|
453
639
|
return;
|
|
454
640
|
recordLoss(owner?.ownerId !== ownerId ? 'taken-over' : 'expired', owner);
|
|
455
641
|
}
|
|
456
|
-
catch {
|
|
642
|
+
catch (error) {
|
|
457
643
|
// Unreadable ownership is not evidence of a valid lease.
|
|
458
644
|
recordLoss('unreadable');
|
|
645
|
+
if (windowsPrivateFiles && error instanceof PrivateFileError)
|
|
646
|
+
throw error;
|
|
459
647
|
}
|
|
460
648
|
throw new LockOwnershipError({
|
|
461
649
|
target: options.path,
|
|
@@ -470,32 +658,61 @@ export async function acquireRefreshFileLock(options) {
|
|
|
470
658
|
});
|
|
471
659
|
},
|
|
472
660
|
release: async () => {
|
|
661
|
+
if (windowsPrivateFiles && releaseConfirmed)
|
|
662
|
+
return;
|
|
473
663
|
released = true;
|
|
474
664
|
cancelRenewalTimer();
|
|
475
665
|
await renewalInFlight;
|
|
476
666
|
for (let attempt = 0; attempt < MAX_STEAL_ATTEMPTS; attempt++) {
|
|
477
667
|
try {
|
|
668
|
+
let removalConfirmed = false;
|
|
478
669
|
const markerAcquired = await withEvictionMarker(async () => {
|
|
479
|
-
|
|
480
|
-
|
|
670
|
+
let owner;
|
|
671
|
+
try {
|
|
672
|
+
owner = await readOwner();
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
if (windowsPrivateFiles &&
|
|
676
|
+
error.code === 'ENOENT') {
|
|
677
|
+
removalConfirmed = true;
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
if (owner?.ownerId !== ownerId) {
|
|
683
|
+
removalConfirmed = true;
|
|
481
684
|
return;
|
|
685
|
+
}
|
|
482
686
|
if (options.onStep)
|
|
483
687
|
await options.onStep('release-owner-confirmed');
|
|
484
688
|
if (!(await ownsEvictionMarker()))
|
|
485
689
|
return;
|
|
486
|
-
await
|
|
690
|
+
await removeLeaseObject(lockPath);
|
|
691
|
+
removalConfirmed = true;
|
|
487
692
|
});
|
|
488
|
-
if (markerAcquired)
|
|
693
|
+
if (markerAcquired) {
|
|
694
|
+
if (windowsPrivateFiles && !removalConfirmed)
|
|
695
|
+
throw new PrivateFileError(lockPath, 'release-not-confirmed', {
|
|
696
|
+
code: 'EIO',
|
|
697
|
+
});
|
|
698
|
+
releaseConfirmed = removalConfirmed;
|
|
489
699
|
return;
|
|
700
|
+
}
|
|
490
701
|
await recoverStaleEvictionMarker();
|
|
491
702
|
}
|
|
492
|
-
catch {
|
|
703
|
+
catch (error) {
|
|
704
|
+
if (windowsPrivateFiles)
|
|
705
|
+
throw error;
|
|
493
706
|
return;
|
|
494
707
|
}
|
|
495
708
|
await backoff();
|
|
496
709
|
}
|
|
497
710
|
// Do not delete by pathname without the marker: bounded retries leave the
|
|
498
711
|
// lease to expire rather than risking removal of a successor's lock.
|
|
712
|
+
if (windowsPrivateFiles)
|
|
713
|
+
throw new PrivateFileError(lockPath, 'release-not-confirmed', {
|
|
714
|
+
code: 'ETIMEDOUT',
|
|
715
|
+
});
|
|
499
716
|
},
|
|
500
717
|
};
|
|
501
718
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { acquireRefreshFileLock } from './refresh-file-lock.js';
|
|
1
2
|
export declare function lockPathFor(target: string, name: string): string;
|
|
2
3
|
export declare class LockContentionError extends Error {
|
|
3
4
|
readonly details: {
|
|
@@ -27,6 +28,8 @@ export interface LockOptions {
|
|
|
27
28
|
ttlMs: number;
|
|
28
29
|
timeoutMs: number;
|
|
29
30
|
renew?: boolean;
|
|
31
|
+
onStep?: Parameters<typeof acquireRefreshFileLock>[0]['onStep'];
|
|
32
|
+
onRenewalTimer?: Parameters<typeof acquireRefreshFileLock>[0]['onRenewalTimer'];
|
|
30
33
|
}
|
|
31
34
|
export declare function withLock<T>(target: string, options: LockOptions, fn: (lock: {
|
|
32
35
|
assertOwned(): Promise<void>;
|
|
@@ -26,6 +26,10 @@ export async function withLock(target, options, fn) {
|
|
|
26
26
|
name: options.name,
|
|
27
27
|
ttlMs: options.ttlMs,
|
|
28
28
|
renew: options.renew ?? false,
|
|
29
|
+
...(options.onStep ? { onStep: options.onStep } : {}),
|
|
30
|
+
...(options.onRenewalTimer
|
|
31
|
+
? { onRenewalTimer: options.onRenewalTimer }
|
|
32
|
+
: {}),
|
|
29
33
|
});
|
|
30
34
|
if (lock) {
|
|
31
35
|
try {
|
|
@@ -62,11 +62,10 @@ export interface LoggerInstance {
|
|
|
62
62
|
/** Operator override of the level floor; undefined removes it. */
|
|
63
63
|
setLogLevel(level: Level | undefined): void;
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
* process-exit
|
|
67
|
-
* shutdown.
|
|
65
|
+
* On Windows this returns a promise and must be awaited before shutdown;
|
|
66
|
+
* synchronous process-exit flushing remains available on POSIX only.
|
|
68
67
|
*/
|
|
69
|
-
flushLogs(): void
|
|
68
|
+
flushLogs(): void | Promise<void>;
|
|
70
69
|
}
|
|
71
70
|
/**
|
|
72
71
|
* A logger instance of its own, configured with `options`. A plugin whose
|
|
@@ -84,11 +83,10 @@ export declare function createLoggerInstance(options: LoggerOptions): LoggerInst
|
|
|
84
83
|
export declare function initLogger(options: LoggerOptions): LoggerInstance;
|
|
85
84
|
export declare function setLogLevel(l: Level | undefined): void;
|
|
86
85
|
/**
|
|
87
|
-
* Write whatever the default logger has buffered.
|
|
88
|
-
*
|
|
89
|
-
* shutdown.
|
|
86
|
+
* Write whatever the default logger has buffered. On Windows await the
|
|
87
|
+
* promise before shutdown; POSIX keeps synchronous process-exit flushing.
|
|
90
88
|
*/
|
|
91
|
-
export declare function flushLogs(): void
|
|
89
|
+
export declare function flushLogs(): void | Promise<void>;
|
|
92
90
|
export declare function createLogger(channel: string): ChannelLogger;
|
|
93
91
|
export declare function flushForTest(): Promise<void>;
|
|
94
92
|
/**
|