@tinycloud/operations 0.6.1-beta.3 → 0.6.1-beta.30
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/coverage.json +1 -0
- package/dist/artifacts.cjs +263 -56
- package/dist/artifacts.cjs.map +1 -1
- package/dist/artifacts.js +266 -58
- package/dist/artifacts.js.map +1 -1
- package/dist/cli-runtime.cjs +284 -69
- package/dist/cli-runtime.cjs.map +1 -1
- package/dist/cli-runtime.js +287 -71
- package/dist/cli-runtime.js.map +1 -1
- package/dist/delegation-binding.cjs +268 -58
- package/dist/delegation-binding.cjs.map +1 -1
- package/dist/delegation-binding.js +271 -60
- package/dist/delegation-binding.js.map +1 -1
- package/dist/index.cjs +284 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +287 -71
- package/dist/index.js.map +1 -1
- package/dist/profile.cjs +160 -10
- package/dist/profile.cjs.map +1 -1
- package/dist/profile.js +163 -12
- package/dist/profile.js.map +1 -1
- package/dist/state.cjs +277 -58
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.cts +12 -1
- package/dist/state.d.ts +12 -1
- package/dist/state.js +276 -60
- package/dist/state.js.map +1 -1
- package/package.json +5 -5
package/dist/state.cjs
CHANGED
|
@@ -23,8 +23,11 @@ __export(state_exports, {
|
|
|
23
23
|
DEFAULT_PROFILE: () => DEFAULT_PROFILE,
|
|
24
24
|
ProfileDeletedError: () => ProfileDeletedError,
|
|
25
25
|
ProfileLockTimeoutError: () => ProfileLockTimeoutError,
|
|
26
|
+
ProfileSymlinkError: () => ProfileSymlinkError,
|
|
26
27
|
additionalDelegationsPath: () => additionalDelegationsPath,
|
|
28
|
+
assertRealProfileDirectory: () => assertRealProfileDirectory,
|
|
27
29
|
authRequestsPath: () => authRequestsPath,
|
|
30
|
+
namedProfilePath: () => namedProfilePath,
|
|
28
31
|
profileConfigPath: () => profileConfigPath,
|
|
29
32
|
profileLockMetadataPath: () => profileLockMetadataPath,
|
|
30
33
|
profileLockPath: () => profileLockPath,
|
|
@@ -48,6 +51,7 @@ __export(state_exports, {
|
|
|
48
51
|
tinycloudHomePath: () => tinycloudHomePath,
|
|
49
52
|
updateProfileStore: () => updateProfileStore,
|
|
50
53
|
upsertProfileRecord: () => upsertProfileRecord,
|
|
54
|
+
withPinnedProfileDirectory: () => withPinnedProfileDirectory,
|
|
51
55
|
withProfileLock: () => withProfileLock,
|
|
52
56
|
withTinyCloudStateRoot: () => withTinyCloudStateRoot,
|
|
53
57
|
writeJsonAtomic: () => writeJsonAtomic,
|
|
@@ -81,14 +85,18 @@ var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIE
|
|
|
81
85
|
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
82
86
|
var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
83
87
|
function processWideContext(key) {
|
|
84
|
-
var
|
|
88
|
+
var _a4;
|
|
85
89
|
const registry = globalThis;
|
|
86
|
-
return registry[
|
|
90
|
+
return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new import_node_async_hooks.AsyncLocalStorage());
|
|
87
91
|
}
|
|
88
92
|
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
89
93
|
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
94
|
+
var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
|
|
95
|
+
var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
|
|
90
96
|
var _a;
|
|
91
|
-
var
|
|
97
|
+
var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
|
|
98
|
+
var _a2;
|
|
99
|
+
var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = (0, import_node_crypto.randomUUID)());
|
|
92
100
|
var cachedProcessOrigin;
|
|
93
101
|
function processOrigin() {
|
|
94
102
|
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
@@ -276,9 +284,14 @@ function tinycloudConfigPath() {
|
|
|
276
284
|
function profilesPath() {
|
|
277
285
|
return (0, import_node_path.join)(tinycloudHomePath(), "profiles");
|
|
278
286
|
}
|
|
279
|
-
function
|
|
287
|
+
function namedProfilePath(profile) {
|
|
280
288
|
return (0, import_node_path.join)(profilesPath(), validateProfileName(profile));
|
|
281
289
|
}
|
|
290
|
+
function profilePath(profile) {
|
|
291
|
+
const named = namedProfilePath(profile);
|
|
292
|
+
const pinned = pinnedProfiles.getStore()?.get(named);
|
|
293
|
+
return pinned ? livePinnedPath(profile, pinned) : named;
|
|
294
|
+
}
|
|
282
295
|
function profileConfigPath(profile) {
|
|
283
296
|
return (0, import_node_path.join)(profilePath(profile), "profile.json");
|
|
284
297
|
}
|
|
@@ -298,13 +311,15 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
298
311
|
return `${profileStorePath(profile, store)}.metadata.json`;
|
|
299
312
|
}
|
|
300
313
|
function profileLockPath(profile) {
|
|
301
|
-
return (0, import_node_path.join)(
|
|
314
|
+
return (0, import_node_path.join)(namedProfilePath(profile), ".lock");
|
|
302
315
|
}
|
|
303
316
|
function profileLockMetadataPath(profile) {
|
|
304
317
|
return (0, import_node_path.join)(profileLockPath(profile), "owner.json");
|
|
305
318
|
}
|
|
306
319
|
function profileTurnLockPath(profile) {
|
|
307
|
-
|
|
320
|
+
const home = tinycloudHomePath();
|
|
321
|
+
const pinned = pinnedHomes.getStore()?.get(home);
|
|
322
|
+
return (0, import_node_path.join)(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
|
|
308
323
|
}
|
|
309
324
|
async function readJson(filePath) {
|
|
310
325
|
try {
|
|
@@ -314,6 +329,44 @@ async function readJson(filePath) {
|
|
|
314
329
|
throw error;
|
|
315
330
|
}
|
|
316
331
|
}
|
|
332
|
+
async function readProfileJson(profile, filePath) {
|
|
333
|
+
const before = process.platform !== "linux" ? await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
|
|
334
|
+
if (error.code === "ENOENT") return null;
|
|
335
|
+
throw error;
|
|
336
|
+
}) : null;
|
|
337
|
+
if (process.platform !== "linux" && !before) return null;
|
|
338
|
+
if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
|
|
339
|
+
const dev = before && exactFileId(before.dev);
|
|
340
|
+
const ino = before && exactFileId(before.ino);
|
|
341
|
+
try {
|
|
342
|
+
const handle = await (0, import_promises.open)(filePath, import_node_fs.constants.O_RDONLY | (import_node_fs.constants.O_NOFOLLOW ?? 0));
|
|
343
|
+
try {
|
|
344
|
+
if (before) {
|
|
345
|
+
const opened = await handle.stat({ bigint: true });
|
|
346
|
+
if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
|
|
347
|
+
throw new ProfileSymlinkError(profile, "changed while opening its file");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const contents = await handle.readFile("utf8");
|
|
351
|
+
if (before) {
|
|
352
|
+
const named = await (0, import_promises.lstat)(filePath, { bigint: true }).catch((error) => {
|
|
353
|
+
if (error.code === "ENOENT") return null;
|
|
354
|
+
throw error;
|
|
355
|
+
});
|
|
356
|
+
if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
|
|
357
|
+
throw new ProfileSymlinkError(profile, "changed while reading its file");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return JSON.parse(contents);
|
|
361
|
+
} finally {
|
|
362
|
+
await handle.close();
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (isErrno(error, "ENOENT")) return null;
|
|
366
|
+
if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
317
370
|
async function writeJsonAtomic(filePath, value) {
|
|
318
371
|
await replaceJson(filePath, value, false);
|
|
319
372
|
}
|
|
@@ -347,11 +400,14 @@ async function writeFileDurably(path, contents) {
|
|
|
347
400
|
}
|
|
348
401
|
}
|
|
349
402
|
async function readProfile(profile) {
|
|
350
|
-
return
|
|
403
|
+
return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, (0, import_node_path.join)(directory, "profile.json")) : Promise.resolve(null));
|
|
351
404
|
}
|
|
352
405
|
async function readSession(profile) {
|
|
353
|
-
|
|
354
|
-
|
|
406
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
407
|
+
if (!directory) return null;
|
|
408
|
+
await readStoreMetadata(profile, "session");
|
|
409
|
+
return readProfileJson(profile, sessionPath(profile));
|
|
410
|
+
});
|
|
355
411
|
}
|
|
356
412
|
async function readAdditionalDelegations(profile) {
|
|
357
413
|
return (await readProfileStore(profile, "additional-delegations")).records;
|
|
@@ -360,48 +416,200 @@ async function readAuthRequests(profile) {
|
|
|
360
416
|
return (await readProfileStore(profile, "auth-requests")).records;
|
|
361
417
|
}
|
|
362
418
|
async function readStoreMetadata(profile, store) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
419
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
420
|
+
const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
|
|
421
|
+
if (metadata === null) return { formatVersion: 1 };
|
|
422
|
+
if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
|
|
423
|
+
throw new TypeError(`Unsupported store format for "${store}".`);
|
|
424
|
+
}
|
|
425
|
+
return metadata;
|
|
426
|
+
});
|
|
369
427
|
}
|
|
370
428
|
async function readProfileStore(profile, store) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
429
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
430
|
+
if (!directory) return { formatVersion: 1, records: [] };
|
|
431
|
+
const [metadata, raw] = await Promise.all([
|
|
432
|
+
readStoreMetadata(profile, store),
|
|
433
|
+
readProfileJson(profile, profileStorePath(profile, store))
|
|
434
|
+
]);
|
|
435
|
+
return {
|
|
436
|
+
formatVersion: metadata.formatVersion,
|
|
437
|
+
records: Array.isArray(raw) ? raw : []
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
var ProfileSymlinkError = class extends Error {
|
|
442
|
+
constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
|
|
443
|
+
super(`Profile "${profile}" ${detail}.`);
|
|
444
|
+
this.code = "PROFILE_SYMLINK";
|
|
445
|
+
this.name = "ProfileSymlinkError";
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
function exactFileId(value) {
|
|
449
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
450
|
+
return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
451
|
+
}
|
|
452
|
+
function isPinnedLive(pinned) {
|
|
453
|
+
if (!pinned.active) return false;
|
|
454
|
+
try {
|
|
455
|
+
const stats = (0, import_node_fs.fstatSync)(pinned.fd, { bigint: true });
|
|
456
|
+
return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
function livePinnedPath(profile, pinned) {
|
|
462
|
+
if (isPinnedLive(pinned)) return pinned.path;
|
|
463
|
+
throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
|
|
464
|
+
}
|
|
465
|
+
async function withPinnedProfileDirectory(profile, action, options = {}) {
|
|
466
|
+
const named = namedProfilePath(profile);
|
|
467
|
+
const existing = pinnedProfiles.getStore()?.get(named);
|
|
468
|
+
if (existing) return action(livePinnedPath(profile, existing));
|
|
469
|
+
if (process.platform !== "linux") {
|
|
470
|
+
if (options.create) await (0, import_promises.mkdir)(named, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
471
|
+
const stats = await (0, import_promises.lstat)(named).catch((error) => {
|
|
472
|
+
if (error.code === "ENOENT") return null;
|
|
473
|
+
throw error;
|
|
474
|
+
});
|
|
475
|
+
if (!stats) return action(null);
|
|
476
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
|
|
477
|
+
return action(named);
|
|
478
|
+
}
|
|
479
|
+
const home = tinycloudHomePath();
|
|
480
|
+
const handles = [];
|
|
481
|
+
const fdPath = (fd) => `/proc/self/fd/${fd}`;
|
|
482
|
+
const flags = import_node_fs.constants.O_RDONLY | import_node_fs.constants.O_DIRECTORY | import_node_fs.constants.O_NOFOLLOW;
|
|
483
|
+
try {
|
|
484
|
+
if (options.create) await (0, import_promises.mkdir)(home, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
485
|
+
const resolved = canonicalHomes.get(home) ?? await (0, import_promises.realpath)(home).catch((error) => {
|
|
486
|
+
if (error.code === "ENOENT" && !options.create) return null;
|
|
487
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
|
|
488
|
+
throw error;
|
|
489
|
+
});
|
|
490
|
+
if (!resolved) return await action(null);
|
|
491
|
+
const root = canonicalHomes.get(home) ?? resolved;
|
|
492
|
+
canonicalHomes.set(home, root);
|
|
493
|
+
const segments = [root, "profiles", validateProfileName(profile)];
|
|
494
|
+
for (let index = 0; index < segments.length; index++) {
|
|
495
|
+
const path = handles.length ? (0, import_node_path.join)(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
|
|
496
|
+
if (options.create && handles.length) {
|
|
497
|
+
await (0, import_promises.mkdir)(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
|
|
498
|
+
if (error.code !== "EEXIST") throw error;
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const handle = await (0, import_promises.open)(path, index === 1 ? flags & ~import_node_fs.constants.O_NOFOLLOW : flags).catch((error) => {
|
|
502
|
+
if (error.code === "ENOENT" && !options.create) return null;
|
|
503
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
|
|
504
|
+
throw error;
|
|
505
|
+
});
|
|
506
|
+
if (!handle) return await action(null);
|
|
507
|
+
handles.push(handle);
|
|
508
|
+
if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
|
|
509
|
+
}
|
|
510
|
+
const opened = handles[2];
|
|
511
|
+
const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), (0, import_promises.lstat)(named, { bigint: true })]);
|
|
512
|
+
const dev = exactFileId(actual.dev);
|
|
513
|
+
const ino = exactFileId(actual.ino);
|
|
514
|
+
if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
|
|
515
|
+
throw new ProfileSymlinkError(profile, "changed while opening its directory");
|
|
516
|
+
}
|
|
517
|
+
const directory = fdPath(opened.fd);
|
|
518
|
+
const pinned = {
|
|
519
|
+
path: directory,
|
|
520
|
+
fd: opened.fd,
|
|
521
|
+
dev,
|
|
522
|
+
ino,
|
|
523
|
+
active: true
|
|
524
|
+
};
|
|
525
|
+
const homeHandle = handles[0];
|
|
526
|
+
const homeStats = await homeHandle.stat({ bigint: true });
|
|
527
|
+
const homeDev = exactFileId(homeStats.dev);
|
|
528
|
+
const homeIno = exactFileId(homeStats.ino);
|
|
529
|
+
if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
|
|
530
|
+
const homePinned = {
|
|
531
|
+
path: fdPath(homeHandle.fd),
|
|
532
|
+
fd: homeHandle.fd,
|
|
533
|
+
dev: homeDev,
|
|
534
|
+
ino: homeIno,
|
|
535
|
+
active: true
|
|
536
|
+
};
|
|
537
|
+
const context = new Map(pinnedProfiles.getStore());
|
|
538
|
+
context.set(named, pinned);
|
|
539
|
+
const homes = new Map(pinnedHomes.getStore());
|
|
540
|
+
homes.set(home, homePinned);
|
|
541
|
+
try {
|
|
542
|
+
return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
|
|
543
|
+
} finally {
|
|
544
|
+
pinned.active = false;
|
|
545
|
+
homePinned.active = false;
|
|
546
|
+
}
|
|
547
|
+
} finally {
|
|
548
|
+
for (const handle of handles.reverse()) await handle.close();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async function assertRealProfileDirectory(profile) {
|
|
552
|
+
const named = namedProfilePath(profile);
|
|
553
|
+
const pinned = pinnedProfiles.getStore()?.get(named);
|
|
554
|
+
if (pinned) {
|
|
555
|
+
livePinnedPath(profile, pinned);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const stats = await (0, import_promises.lstat)(named).catch((error) => {
|
|
559
|
+
if (error.code === "ENOENT") return null;
|
|
560
|
+
throw error;
|
|
561
|
+
});
|
|
562
|
+
if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
|
|
379
563
|
}
|
|
380
564
|
async function withProfileLock(profile, action, options = {}) {
|
|
381
565
|
const normalizedProfile = validateProfileName(profile);
|
|
382
566
|
const lockPath = profileLockPath(normalizedProfile);
|
|
383
|
-
const held = (heldProfileLocks.getStore() ?? []).filter((
|
|
384
|
-
if (held.some((
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
lockPath,
|
|
388
|
-
turnSlot: acquired.turnSlot,
|
|
389
|
-
deletedWhileWaiting: acquired.deletedWhileWaiting,
|
|
390
|
-
active: true
|
|
391
|
-
};
|
|
392
|
-
try {
|
|
393
|
-
return await heldProfileLocks.run([...held, ownership], action);
|
|
394
|
-
} finally {
|
|
395
|
-
ownership.active = false;
|
|
396
|
-
await acquired.release();
|
|
397
|
-
if (acquired.deletedWhileWaiting) await (0, import_promises.rmdir)(profilePath(normalizedProfile)).catch(() => void 0);
|
|
567
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
|
|
568
|
+
if (held.some((ownership) => ownership.lockPath === lockPath)) {
|
|
569
|
+
await assertRealProfileDirectory(normalizedProfile);
|
|
570
|
+
return action();
|
|
398
571
|
}
|
|
572
|
+
const named = namedProfilePath(normalizedProfile);
|
|
573
|
+
const inherited = pinnedProfiles.getStore()?.get(named);
|
|
574
|
+
if (inherited && !isPinnedLive(inherited)) {
|
|
575
|
+
const profiles = new Map(pinnedProfiles.getStore());
|
|
576
|
+
profiles.delete(named);
|
|
577
|
+
const homes = new Map(pinnedHomes.getStore());
|
|
578
|
+
homes.delete(tinycloudHomePath());
|
|
579
|
+
return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
|
|
580
|
+
}
|
|
581
|
+
return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
|
|
582
|
+
if (!directory) throw new ProfileDeletedError(normalizedProfile);
|
|
583
|
+
const acquired = await acquireProfileLock(normalizedProfile, options);
|
|
584
|
+
const ownership = {
|
|
585
|
+
lockPath,
|
|
586
|
+
turnSlot: acquired.turnSlot,
|
|
587
|
+
deletedWhileWaiting: acquired.deletedWhileWaiting,
|
|
588
|
+
active: true
|
|
589
|
+
};
|
|
590
|
+
try {
|
|
591
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
592
|
+
} finally {
|
|
593
|
+
ownership.active = false;
|
|
594
|
+
await acquired.release();
|
|
595
|
+
if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
|
|
596
|
+
const current = await (0, import_promises.lstat)(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
|
|
597
|
+
const original = await (0, import_promises.stat)(directory, { bigint: true }).catch(() => null);
|
|
598
|
+
const currentDev = current && exactFileId(current.dev);
|
|
599
|
+
const currentIno = current && exactFileId(current.ino);
|
|
600
|
+
if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
|
|
601
|
+
await (0, import_promises.rmdir)(namedProfilePath(normalizedProfile)).catch(() => void 0);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}, { create: !options.requireProfile });
|
|
399
606
|
}
|
|
400
607
|
async function recordProfileDeletion(profile) {
|
|
401
608
|
const lockPath = profileLockPath(profile);
|
|
402
609
|
const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
|
|
403
610
|
if (!ownership) throw new Error(`Recording the deletion of profile "${profile}" requires holding its lock.`);
|
|
404
611
|
await replaceJson((0, import_node_path.join)(profileTurnLockPath(profile), "deleted.json"), { slot: ownership.turnSlot }, true);
|
|
612
|
+
ownership.deletedProfile = true;
|
|
405
613
|
}
|
|
406
614
|
async function refuseWriteToDeletedProfile(profile) {
|
|
407
615
|
const lockPath = profileLockPath(profile);
|
|
@@ -461,17 +669,15 @@ async function acquireProfileLock(profile, options) {
|
|
|
461
669
|
retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
|
|
462
670
|
};
|
|
463
671
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
464
|
-
const lockPath =
|
|
672
|
+
const lockPath = (0, import_node_path.join)(profilePath(profile), ".lock");
|
|
465
673
|
const turnDirectory = profileTurnLockPath(profile);
|
|
466
674
|
const directory = profilePath(profile);
|
|
675
|
+
await assertRealProfileDirectory(profile);
|
|
467
676
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
|
|
468
677
|
if (!options.requireProfile) await (0, import_promises.mkdir)(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
678
|
+
await assertRealProfileDirectory(profile);
|
|
469
679
|
await (0, import_promises.mkdir)((0, import_node_path.dirname)(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
470
|
-
|
|
471
|
-
await (0, import_promises.chmod)(path, PRIVATE_DIR_MODE).catch((error) => {
|
|
472
|
-
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
473
|
-
});
|
|
474
|
-
}
|
|
680
|
+
await (0, import_promises.chmod)(directory, PRIVATE_DIR_MODE);
|
|
475
681
|
const liveness = { directory: turnDirectory, held: void 0 };
|
|
476
682
|
const closeAcquisitionLiveness = async () => {
|
|
477
683
|
if (liveness.held) await closeLiveness(liveness.held);
|
|
@@ -491,6 +697,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
491
697
|
}
|
|
492
698
|
const deletion = await readJson((0, import_node_path.join)(turnDirectory, "deleted.json")).catch(() => null);
|
|
493
699
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
700
|
+
if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
|
|
494
701
|
while (true) {
|
|
495
702
|
const token = (0, import_node_crypto.randomUUID)();
|
|
496
703
|
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
@@ -527,8 +734,8 @@ async function waitOrTimeOut(profile, deadline, detail) {
|
|
|
527
734
|
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
528
735
|
}
|
|
529
736
|
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
530
|
-
var
|
|
531
|
-
var relinquishedTurns = globalThis[
|
|
737
|
+
var _a3;
|
|
738
|
+
var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
|
|
532
739
|
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
533
740
|
var SETTLE_RETRY_MS = 1e3;
|
|
534
741
|
var SETTLE_RETRIES = 300;
|
|
@@ -642,7 +849,7 @@ async function takeTurn(profile, directory, latest, liveness) {
|
|
|
642
849
|
if (published) await markTurn(path, token, "done");
|
|
643
850
|
return null;
|
|
644
851
|
} catch (error) {
|
|
645
|
-
if (published) settleOwnTurn(directory, slot, token);
|
|
852
|
+
if (published) await settleOwnTurn(directory, slot, token);
|
|
646
853
|
throw error;
|
|
647
854
|
}
|
|
648
855
|
}
|
|
@@ -653,17 +860,25 @@ async function settleAbandonedTurn(directory, abandoned) {
|
|
|
653
860
|
}
|
|
654
861
|
await markTurn(path, abandoned.token, "done");
|
|
655
862
|
}
|
|
656
|
-
function settleOwnTurn(directory, slot, token
|
|
863
|
+
async function settleOwnTurn(directory, slot, token) {
|
|
657
864
|
relinquishedTurns.add(token);
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
865
|
+
const pinned = await pinDirectory(directory).catch(() => null);
|
|
866
|
+
const path = pinned?.path(".") ?? directory;
|
|
867
|
+
let retries = SETTLE_RETRIES;
|
|
868
|
+
const attempt = () => {
|
|
869
|
+
void (async () => {
|
|
870
|
+
try {
|
|
871
|
+
const turn = await readTurn(path, slot);
|
|
872
|
+
if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
|
|
873
|
+
relinquishedTurns.delete(token);
|
|
874
|
+
await pinned?.close();
|
|
875
|
+
} catch {
|
|
876
|
+
if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
|
|
877
|
+
else await pinned?.close();
|
|
878
|
+
}
|
|
879
|
+
})();
|
|
880
|
+
};
|
|
881
|
+
attempt();
|
|
667
882
|
}
|
|
668
883
|
async function markTurn(path, token, kind) {
|
|
669
884
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -708,7 +923,7 @@ async function releaseTurn(profile, turn, liveness) {
|
|
|
708
923
|
try {
|
|
709
924
|
await markTurn((0, import_node_path.join)(turn.directory, String(turn.slot)), turn.token, "done");
|
|
710
925
|
} catch {
|
|
711
|
-
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
926
|
+
await settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
712
927
|
return;
|
|
713
928
|
}
|
|
714
929
|
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
@@ -1001,8 +1216,11 @@ function sleep(milliseconds) {
|
|
|
1001
1216
|
DEFAULT_PROFILE,
|
|
1002
1217
|
ProfileDeletedError,
|
|
1003
1218
|
ProfileLockTimeoutError,
|
|
1219
|
+
ProfileSymlinkError,
|
|
1004
1220
|
additionalDelegationsPath,
|
|
1221
|
+
assertRealProfileDirectory,
|
|
1005
1222
|
authRequestsPath,
|
|
1223
|
+
namedProfilePath,
|
|
1006
1224
|
profileConfigPath,
|
|
1007
1225
|
profileLockMetadataPath,
|
|
1008
1226
|
profileLockPath,
|
|
@@ -1026,6 +1244,7 @@ function sleep(milliseconds) {
|
|
|
1026
1244
|
tinycloudHomePath,
|
|
1027
1245
|
updateProfileStore,
|
|
1028
1246
|
upsertProfileRecord,
|
|
1247
|
+
withPinnedProfileDirectory,
|
|
1029
1248
|
withProfileLock,
|
|
1030
1249
|
withTinyCloudStateRoot,
|
|
1031
1250
|
writeJsonAtomic,
|