@tinycloud/operations 0.6.1-beta.2 → 0.6.1-beta.20
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.d.cts +54 -54
- package/dist/artifacts.d.ts +54 -54
- 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 +4 -4
package/dist/state.d.ts
CHANGED
|
@@ -43,6 +43,7 @@ declare function tinycloudHomePath(): string;
|
|
|
43
43
|
declare function withTinyCloudStateRoot<T>(stateRoot: string | undefined, action: () => Promise<T>): Promise<T>;
|
|
44
44
|
declare function tinycloudConfigPath(): string;
|
|
45
45
|
declare function profilesPath(): string;
|
|
46
|
+
declare function namedProfilePath(profile: string): string;
|
|
46
47
|
declare function profilePath(profile: string): string;
|
|
47
48
|
declare function profileConfigPath(profile: string): string;
|
|
48
49
|
declare function sessionPath(profile: string): string;
|
|
@@ -75,6 +76,16 @@ declare function readAdditionalDelegations<T = Record<string, unknown>>(profile:
|
|
|
75
76
|
declare function readAuthRequests<T = Record<string, unknown>>(profile: string): Promise<T[]>;
|
|
76
77
|
declare function readStoreMetadata(profile: string, store: ProfileStoreName): Promise<StoreMetadata>;
|
|
77
78
|
declare function readProfileStore<T>(profile: string, store: Exclude<ProfileStoreName, "session">): Promise<ProfileStoreContents<T>>;
|
|
79
|
+
declare class ProfileSymlinkError extends Error {
|
|
80
|
+
readonly code = "PROFILE_SYMLINK";
|
|
81
|
+
constructor(profile: string, detail?: string);
|
|
82
|
+
}
|
|
83
|
+
/** Missing profiles remain missing; existing profiles are always opened without following aliases. */
|
|
84
|
+
declare function withPinnedProfileDirectory<T>(profile: string, action: (directory: string | null) => Promise<T>, options?: {
|
|
85
|
+
create?: boolean;
|
|
86
|
+
}): Promise<T>;
|
|
87
|
+
/** Missing profiles retain their existing read/create behavior; aliases never do. */
|
|
88
|
+
declare function assertRealProfileDirectory(profile: string): Promise<void>;
|
|
78
89
|
/**
|
|
79
90
|
* Runs a small critical section under the one lock shared by all profile
|
|
80
91
|
* stores (see acquireProfileLock). Lock release verifies its ownership token
|
|
@@ -126,4 +137,4 @@ declare function updateProfileStore<T, Result>(profile: string, store: Exclude<P
|
|
|
126
137
|
declare function writeSession<T extends object>(profile: string, session: T, options?: ProfileLockOptions): Promise<void>;
|
|
127
138
|
declare function removeSession(profile: string, options?: ProfileLockOptions): Promise<void>;
|
|
128
139
|
|
|
129
|
-
export { DEFAULT_PROFILE, ProfileDeletedError, type ProfileLockOptions, ProfileLockTimeoutError, type ProfileStoreContents, type ProfileStoreName, type StoreMetadata, additionalDelegationsPath, authRequestsPath, profileConfigPath, profileLockMetadataPath, profileLockPath, profilePath, profileStoreMetadataPath, profileStorePath, profileTurnLockPath, profilesPath, readAdditionalDelegations, readAuthRequests, readJson, readProfile, readProfileStore, readSession, readStoreMetadata, recordProfileDeletion, refuseWriteToDeletedProfile, removeSession, sessionPath, tinycloudConfigPath, tinycloudHomePath, updateProfileStore, upsertProfileRecord, withProfileLock, withTinyCloudStateRoot, writeJsonAtomic, writeSession };
|
|
140
|
+
export { DEFAULT_PROFILE, ProfileDeletedError, type ProfileLockOptions, ProfileLockTimeoutError, type ProfileStoreContents, type ProfileStoreName, ProfileSymlinkError, type StoreMetadata, additionalDelegationsPath, assertRealProfileDirectory, authRequestsPath, namedProfilePath, profileConfigPath, profileLockMetadataPath, profileLockPath, profilePath, profileStoreMetadataPath, profileStorePath, profileTurnLockPath, profilesPath, readAdditionalDelegations, readAuthRequests, readJson, readProfile, readProfileStore, readSession, readStoreMetadata, recordProfileDeletion, refuseWriteToDeletedProfile, removeSession, sessionPath, tinycloudConfigPath, tinycloudHomePath, updateProfileStore, upsertProfileRecord, withPinnedProfileDirectory, withProfileLock, withTinyCloudStateRoot, writeJsonAtomic, writeSession };
|
package/dist/state.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// src/state.ts
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
-
import { readFileSync, readlinkSync } from "fs";
|
|
4
|
+
import { constants as fsConstants, fstatSync, readFileSync, readlinkSync } from "fs";
|
|
5
5
|
import {
|
|
6
6
|
chmod,
|
|
7
7
|
link,
|
|
8
|
-
lstat,
|
|
9
8
|
mkdir,
|
|
9
|
+
lstat,
|
|
10
10
|
open,
|
|
11
11
|
readFile,
|
|
12
12
|
readdir,
|
|
13
|
+
realpath,
|
|
13
14
|
rename,
|
|
14
15
|
rmdir,
|
|
15
16
|
rm,
|
|
@@ -40,14 +41,18 @@ var TEST_LOCK_TURN_SETTLE_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_SETTLE_BARRIE
|
|
|
40
41
|
var TEST_LOCK_TURN_COLLECT_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_TURN_COLLECT_BARRIER_DIR";
|
|
41
42
|
var TEST_LOCK_LIVENESS_BARRIER_DIR = "TC_TEST_PROFILE_LOCK_LIVENESS_BARRIER_DIR";
|
|
42
43
|
function processWideContext(key) {
|
|
43
|
-
var
|
|
44
|
+
var _a4;
|
|
44
45
|
const registry = globalThis;
|
|
45
|
-
return registry[
|
|
46
|
+
return registry[_a4 = Symbol.for(key)] ?? (registry[_a4] = new AsyncLocalStorage());
|
|
46
47
|
}
|
|
47
48
|
var invocationStateRoot = processWideContext("tinycloud.operations.invocationStateRoot.v1");
|
|
48
49
|
var heldProfileLocks = processWideContext("tinycloud.operations.heldProfileLocks.v2");
|
|
50
|
+
var pinnedProfiles = processWideContext("tinycloud.operations.pinnedProfiles.v2");
|
|
51
|
+
var pinnedHomes = processWideContext("tinycloud.operations.pinnedHomes.v2");
|
|
49
52
|
var _a;
|
|
50
|
-
var
|
|
53
|
+
var canonicalHomes = globalThis[_a = /* @__PURE__ */ Symbol.for("tinycloud.operations.canonicalHomes.v1")] ?? (globalThis[_a] = /* @__PURE__ */ new Map());
|
|
54
|
+
var _a2;
|
|
55
|
+
var processInstanceId = globalThis[_a2 = /* @__PURE__ */ Symbol.for("tinycloud.operations.profileLockInstance.v1")] ?? (globalThis[_a2] = randomUUID());
|
|
51
56
|
var cachedProcessOrigin;
|
|
52
57
|
function processOrigin() {
|
|
53
58
|
if (cachedProcessOrigin !== void 0) return cachedProcessOrigin;
|
|
@@ -235,9 +240,14 @@ function tinycloudConfigPath() {
|
|
|
235
240
|
function profilesPath() {
|
|
236
241
|
return join(tinycloudHomePath(), "profiles");
|
|
237
242
|
}
|
|
238
|
-
function
|
|
243
|
+
function namedProfilePath(profile) {
|
|
239
244
|
return join(profilesPath(), validateProfileName(profile));
|
|
240
245
|
}
|
|
246
|
+
function profilePath(profile) {
|
|
247
|
+
const named = namedProfilePath(profile);
|
|
248
|
+
const pinned = pinnedProfiles.getStore()?.get(named);
|
|
249
|
+
return pinned ? livePinnedPath(profile, pinned) : named;
|
|
250
|
+
}
|
|
241
251
|
function profileConfigPath(profile) {
|
|
242
252
|
return join(profilePath(profile), "profile.json");
|
|
243
253
|
}
|
|
@@ -257,13 +267,15 @@ function profileStoreMetadataPath(profile, store) {
|
|
|
257
267
|
return `${profileStorePath(profile, store)}.metadata.json`;
|
|
258
268
|
}
|
|
259
269
|
function profileLockPath(profile) {
|
|
260
|
-
return join(
|
|
270
|
+
return join(namedProfilePath(profile), ".lock");
|
|
261
271
|
}
|
|
262
272
|
function profileLockMetadataPath(profile) {
|
|
263
273
|
return join(profileLockPath(profile), "owner.json");
|
|
264
274
|
}
|
|
265
275
|
function profileTurnLockPath(profile) {
|
|
266
|
-
|
|
276
|
+
const home = tinycloudHomePath();
|
|
277
|
+
const pinned = pinnedHomes.getStore()?.get(home);
|
|
278
|
+
return join(pinned ? livePinnedPath(profile, pinned) : home, "profile-locks", validateProfileName(profile));
|
|
267
279
|
}
|
|
268
280
|
async function readJson(filePath) {
|
|
269
281
|
try {
|
|
@@ -273,6 +285,44 @@ async function readJson(filePath) {
|
|
|
273
285
|
throw error;
|
|
274
286
|
}
|
|
275
287
|
}
|
|
288
|
+
async function readProfileJson(profile, filePath) {
|
|
289
|
+
const before = process.platform !== "linux" ? await lstat(filePath, { bigint: true }).catch((error) => {
|
|
290
|
+
if (error.code === "ENOENT") return null;
|
|
291
|
+
throw error;
|
|
292
|
+
}) : null;
|
|
293
|
+
if (process.platform !== "linux" && !before) return null;
|
|
294
|
+
if (before && !before.isFile()) throw new ProfileSymlinkError(profile);
|
|
295
|
+
const dev = before && exactFileId(before.dev);
|
|
296
|
+
const ino = before && exactFileId(before.ino);
|
|
297
|
+
try {
|
|
298
|
+
const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW ?? 0));
|
|
299
|
+
try {
|
|
300
|
+
if (before) {
|
|
301
|
+
const opened = await handle.stat({ bigint: true });
|
|
302
|
+
if (!opened.isFile() || dev === null || ino === null || dev !== exactFileId(opened.dev) || ino !== exactFileId(opened.ino)) {
|
|
303
|
+
throw new ProfileSymlinkError(profile, "changed while opening its file");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const contents = await handle.readFile("utf8");
|
|
307
|
+
if (before) {
|
|
308
|
+
const named = await lstat(filePath, { bigint: true }).catch((error) => {
|
|
309
|
+
if (error.code === "ENOENT") return null;
|
|
310
|
+
throw error;
|
|
311
|
+
});
|
|
312
|
+
if (!named?.isFile() || dev !== exactFileId(named.dev) || ino !== exactFileId(named.ino)) {
|
|
313
|
+
throw new ProfileSymlinkError(profile, "changed while reading its file");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return JSON.parse(contents);
|
|
317
|
+
} finally {
|
|
318
|
+
await handle.close();
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
if (isErrno(error, "ENOENT")) return null;
|
|
322
|
+
if (isErrno(error, "ELOOP") || isErrno(error, "ENOTDIR")) throw new ProfileSymlinkError(profile);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
276
326
|
async function writeJsonAtomic(filePath, value) {
|
|
277
327
|
await replaceJson(filePath, value, false);
|
|
278
328
|
}
|
|
@@ -306,11 +356,14 @@ async function writeFileDurably(path, contents) {
|
|
|
306
356
|
}
|
|
307
357
|
}
|
|
308
358
|
async function readProfile(profile) {
|
|
309
|
-
return
|
|
359
|
+
return withPinnedProfileDirectory(profile, (directory) => directory ? readProfileJson(profile, join(directory, "profile.json")) : Promise.resolve(null));
|
|
310
360
|
}
|
|
311
361
|
async function readSession(profile) {
|
|
312
|
-
|
|
313
|
-
|
|
362
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
363
|
+
if (!directory) return null;
|
|
364
|
+
await readStoreMetadata(profile, "session");
|
|
365
|
+
return readProfileJson(profile, sessionPath(profile));
|
|
366
|
+
});
|
|
314
367
|
}
|
|
315
368
|
async function readAdditionalDelegations(profile) {
|
|
316
369
|
return (await readProfileStore(profile, "additional-delegations")).records;
|
|
@@ -319,48 +372,200 @@ async function readAuthRequests(profile) {
|
|
|
319
372
|
return (await readProfileStore(profile, "auth-requests")).records;
|
|
320
373
|
}
|
|
321
374
|
async function readStoreMetadata(profile, store) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
375
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
376
|
+
const metadata = directory ? await readProfileJson(profile, profileStoreMetadataPath(profile, store)) : null;
|
|
377
|
+
if (metadata === null) return { formatVersion: 1 };
|
|
378
|
+
if (typeof metadata !== "object" || metadata.formatVersion !== 1) {
|
|
379
|
+
throw new TypeError(`Unsupported store format for "${store}".`);
|
|
380
|
+
}
|
|
381
|
+
return metadata;
|
|
382
|
+
});
|
|
328
383
|
}
|
|
329
384
|
async function readProfileStore(profile, store) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
385
|
+
return withPinnedProfileDirectory(profile, async (directory) => {
|
|
386
|
+
if (!directory) return { formatVersion: 1, records: [] };
|
|
387
|
+
const [metadata, raw] = await Promise.all([
|
|
388
|
+
readStoreMetadata(profile, store),
|
|
389
|
+
readProfileJson(profile, profileStorePath(profile, store))
|
|
390
|
+
]);
|
|
391
|
+
return {
|
|
392
|
+
formatVersion: metadata.formatVersion,
|
|
393
|
+
records: Array.isArray(raw) ? raw : []
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
var ProfileSymlinkError = class extends Error {
|
|
398
|
+
constructor(profile, detail = "must be a real directory, not a symlink or another file type") {
|
|
399
|
+
super(`Profile "${profile}" ${detail}.`);
|
|
400
|
+
this.code = "PROFILE_SYMLINK";
|
|
401
|
+
this.name = "ProfileSymlinkError";
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
function exactFileId(value) {
|
|
405
|
+
if (typeof value === "bigint") return value >= 0n ? value : null;
|
|
406
|
+
return Number.isSafeInteger(value) && value >= 0 ? BigInt(value) : null;
|
|
407
|
+
}
|
|
408
|
+
function isPinnedLive(pinned) {
|
|
409
|
+
if (!pinned.active) return false;
|
|
410
|
+
try {
|
|
411
|
+
const stats = fstatSync(pinned.fd, { bigint: true });
|
|
412
|
+
return exactFileId(stats.dev) === pinned.dev && exactFileId(stats.ino) === pinned.ino;
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function livePinnedPath(profile, pinned) {
|
|
418
|
+
if (isPinnedLive(pinned)) return pinned.path;
|
|
419
|
+
throw new ProfileSymlinkError(profile, "its pinned directory is no longer active or changed during use");
|
|
420
|
+
}
|
|
421
|
+
async function withPinnedProfileDirectory(profile, action, options = {}) {
|
|
422
|
+
const named = namedProfilePath(profile);
|
|
423
|
+
const existing = pinnedProfiles.getStore()?.get(named);
|
|
424
|
+
if (existing) return action(livePinnedPath(profile, existing));
|
|
425
|
+
if (process.platform !== "linux") {
|
|
426
|
+
if (options.create) await mkdir(named, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
427
|
+
const stats = await lstat(named).catch((error) => {
|
|
428
|
+
if (error.code === "ENOENT") return null;
|
|
429
|
+
throw error;
|
|
430
|
+
});
|
|
431
|
+
if (!stats) return action(null);
|
|
432
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) throw new ProfileSymlinkError(profile);
|
|
433
|
+
return action(named);
|
|
434
|
+
}
|
|
435
|
+
const home = tinycloudHomePath();
|
|
436
|
+
const handles = [];
|
|
437
|
+
const fdPath = (fd) => `/proc/self/fd/${fd}`;
|
|
438
|
+
const flags = fsConstants.O_RDONLY | fsConstants.O_DIRECTORY | fsConstants.O_NOFOLLOW;
|
|
439
|
+
try {
|
|
440
|
+
if (options.create) await mkdir(home, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
441
|
+
const resolved = canonicalHomes.get(home) ?? await realpath(home).catch((error) => {
|
|
442
|
+
if (error.code === "ENOENT" && !options.create) return null;
|
|
443
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
|
|
444
|
+
throw error;
|
|
445
|
+
});
|
|
446
|
+
if (!resolved) return await action(null);
|
|
447
|
+
const root = canonicalHomes.get(home) ?? resolved;
|
|
448
|
+
canonicalHomes.set(home, root);
|
|
449
|
+
const segments = [root, "profiles", validateProfileName(profile)];
|
|
450
|
+
for (let index = 0; index < segments.length; index++) {
|
|
451
|
+
const path = handles.length ? join(fdPath(handles[handles.length - 1].fd), segments[index]) : segments[index];
|
|
452
|
+
if (options.create && handles.length) {
|
|
453
|
+
await mkdir(path, { mode: PRIVATE_DIR_MODE }).catch((error) => {
|
|
454
|
+
if (error.code !== "EEXIST") throw error;
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
const handle = await open(path, index === 1 ? flags & ~fsConstants.O_NOFOLLOW : flags).catch((error) => {
|
|
458
|
+
if (error.code === "ENOENT" && !options.create) return null;
|
|
459
|
+
if (error.code === "ELOOP" || error.code === "ENOTDIR") throw new ProfileSymlinkError(profile);
|
|
460
|
+
throw error;
|
|
461
|
+
});
|
|
462
|
+
if (!handle) return await action(null);
|
|
463
|
+
handles.push(handle);
|
|
464
|
+
if (options.create) await handle.chmod(PRIVATE_DIR_MODE);
|
|
465
|
+
}
|
|
466
|
+
const opened = handles[2];
|
|
467
|
+
const [actual, namedStats] = await Promise.all([opened.stat({ bigint: true }), lstat(named, { bigint: true })]);
|
|
468
|
+
const dev = exactFileId(actual.dev);
|
|
469
|
+
const ino = exactFileId(actual.ino);
|
|
470
|
+
if (!namedStats.isDirectory() || dev === null || ino === null || dev !== exactFileId(namedStats.dev) || ino !== exactFileId(namedStats.ino)) {
|
|
471
|
+
throw new ProfileSymlinkError(profile, "changed while opening its directory");
|
|
472
|
+
}
|
|
473
|
+
const directory = fdPath(opened.fd);
|
|
474
|
+
const pinned = {
|
|
475
|
+
path: directory,
|
|
476
|
+
fd: opened.fd,
|
|
477
|
+
dev,
|
|
478
|
+
ino,
|
|
479
|
+
active: true
|
|
480
|
+
};
|
|
481
|
+
const homeHandle = handles[0];
|
|
482
|
+
const homeStats = await homeHandle.stat({ bigint: true });
|
|
483
|
+
const homeDev = exactFileId(homeStats.dev);
|
|
484
|
+
const homeIno = exactFileId(homeStats.ino);
|
|
485
|
+
if (homeDev === null || homeIno === null) throw new ProfileSymlinkError(profile, "has an unverified home directory identity");
|
|
486
|
+
const homePinned = {
|
|
487
|
+
path: fdPath(homeHandle.fd),
|
|
488
|
+
fd: homeHandle.fd,
|
|
489
|
+
dev: homeDev,
|
|
490
|
+
ino: homeIno,
|
|
491
|
+
active: true
|
|
492
|
+
};
|
|
493
|
+
const context = new Map(pinnedProfiles.getStore());
|
|
494
|
+
context.set(named, pinned);
|
|
495
|
+
const homes = new Map(pinnedHomes.getStore());
|
|
496
|
+
homes.set(home, homePinned);
|
|
497
|
+
try {
|
|
498
|
+
return await pinnedHomes.run(homes, () => pinnedProfiles.run(context, () => action(directory)));
|
|
499
|
+
} finally {
|
|
500
|
+
pinned.active = false;
|
|
501
|
+
homePinned.active = false;
|
|
502
|
+
}
|
|
503
|
+
} finally {
|
|
504
|
+
for (const handle of handles.reverse()) await handle.close();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async function assertRealProfileDirectory(profile) {
|
|
508
|
+
const named = namedProfilePath(profile);
|
|
509
|
+
const pinned = pinnedProfiles.getStore()?.get(named);
|
|
510
|
+
if (pinned) {
|
|
511
|
+
livePinnedPath(profile, pinned);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const stats = await lstat(named).catch((error) => {
|
|
515
|
+
if (error.code === "ENOENT") return null;
|
|
516
|
+
throw error;
|
|
517
|
+
});
|
|
518
|
+
if (stats && (!stats.isDirectory() || stats.isSymbolicLink())) throw new ProfileSymlinkError(profile);
|
|
338
519
|
}
|
|
339
520
|
async function withProfileLock(profile, action, options = {}) {
|
|
340
521
|
const normalizedProfile = validateProfileName(profile);
|
|
341
522
|
const lockPath = profileLockPath(normalizedProfile);
|
|
342
|
-
const held = (heldProfileLocks.getStore() ?? []).filter((
|
|
343
|
-
if (held.some((
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
await acquired.release();
|
|
356
|
-
if (acquired.deletedWhileWaiting) await rmdir(profilePath(normalizedProfile)).catch(() => void 0);
|
|
523
|
+
const held = (heldProfileLocks.getStore() ?? []).filter((ownership) => ownership.active);
|
|
524
|
+
if (held.some((ownership) => ownership.lockPath === lockPath)) {
|
|
525
|
+
await assertRealProfileDirectory(normalizedProfile);
|
|
526
|
+
return action();
|
|
527
|
+
}
|
|
528
|
+
const named = namedProfilePath(normalizedProfile);
|
|
529
|
+
const inherited = pinnedProfiles.getStore()?.get(named);
|
|
530
|
+
if (inherited && !isPinnedLive(inherited)) {
|
|
531
|
+
const profiles = new Map(pinnedProfiles.getStore());
|
|
532
|
+
profiles.delete(named);
|
|
533
|
+
const homes = new Map(pinnedHomes.getStore());
|
|
534
|
+
homes.delete(tinycloudHomePath());
|
|
535
|
+
return pinnedHomes.run(homes, () => pinnedProfiles.run(profiles, () => withProfileLock(normalizedProfile, action, options)));
|
|
357
536
|
}
|
|
537
|
+
return withPinnedProfileDirectory(normalizedProfile, async (directory) => {
|
|
538
|
+
if (!directory) throw new ProfileDeletedError(normalizedProfile);
|
|
539
|
+
const acquired = await acquireProfileLock(normalizedProfile, options);
|
|
540
|
+
const ownership = {
|
|
541
|
+
lockPath,
|
|
542
|
+
turnSlot: acquired.turnSlot,
|
|
543
|
+
deletedWhileWaiting: acquired.deletedWhileWaiting,
|
|
544
|
+
active: true
|
|
545
|
+
};
|
|
546
|
+
try {
|
|
547
|
+
return await heldProfileLocks.run([...held, ownership], action);
|
|
548
|
+
} finally {
|
|
549
|
+
ownership.active = false;
|
|
550
|
+
await acquired.release();
|
|
551
|
+
if (ownership.deletedProfile || acquired.deletedWhileWaiting) {
|
|
552
|
+
const current = await lstat(namedProfilePath(normalizedProfile), { bigint: true }).catch(() => null);
|
|
553
|
+
const original = await stat(directory, { bigint: true }).catch(() => null);
|
|
554
|
+
const currentDev = current && exactFileId(current.dev);
|
|
555
|
+
const currentIno = current && exactFileId(current.ino);
|
|
556
|
+
if (currentDev !== null && currentIno !== null && original && currentDev === exactFileId(original.dev) && currentIno === exactFileId(original.ino)) {
|
|
557
|
+
await rmdir(namedProfilePath(normalizedProfile)).catch(() => void 0);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}, { create: !options.requireProfile });
|
|
358
562
|
}
|
|
359
563
|
async function recordProfileDeletion(profile) {
|
|
360
564
|
const lockPath = profileLockPath(profile);
|
|
361
565
|
const ownership = (heldProfileLocks.getStore() ?? []).find((candidate) => candidate.active && candidate.lockPath === lockPath);
|
|
362
566
|
if (!ownership) throw new Error(`Recording the deletion of profile "${profile}" requires holding its lock.`);
|
|
363
567
|
await replaceJson(join(profileTurnLockPath(profile), "deleted.json"), { slot: ownership.turnSlot }, true);
|
|
568
|
+
ownership.deletedProfile = true;
|
|
364
569
|
}
|
|
365
570
|
async function refuseWriteToDeletedProfile(profile) {
|
|
366
571
|
const lockPath = profileLockPath(profile);
|
|
@@ -420,17 +625,15 @@ async function acquireProfileLock(profile, options) {
|
|
|
420
625
|
retryMs: options.retryMs ?? DEFAULT_LOCK_RETRY_MS
|
|
421
626
|
};
|
|
422
627
|
const staleAfterMs = options.staleAfterMs ?? DEFAULT_STALE_LOCK_MS;
|
|
423
|
-
const lockPath =
|
|
628
|
+
const lockPath = join(profilePath(profile), ".lock");
|
|
424
629
|
const turnDirectory = profileTurnLockPath(profile);
|
|
425
630
|
const directory = profilePath(profile);
|
|
631
|
+
await assertRealProfileDirectory(profile);
|
|
426
632
|
if (options.requireProfile && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
|
|
427
633
|
if (!options.requireProfile) await mkdir(directory, { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
634
|
+
await assertRealProfileDirectory(profile);
|
|
428
635
|
await mkdir(dirname(turnDirectory), { recursive: true, mode: PRIVATE_DIR_MODE });
|
|
429
|
-
|
|
430
|
-
await chmod(path, PRIVATE_DIR_MODE).catch((error) => {
|
|
431
|
-
if (path !== directory || !isErrno(error, "ENOENT")) throw error;
|
|
432
|
-
});
|
|
433
|
-
}
|
|
636
|
+
await chmod(directory, PRIVATE_DIR_MODE);
|
|
434
637
|
const liveness = { directory: turnDirectory, held: void 0 };
|
|
435
638
|
const closeAcquisitionLiveness = async () => {
|
|
436
639
|
if (liveness.held) await closeLiveness(liveness.held);
|
|
@@ -450,6 +653,7 @@ async function acquireProfileLock(profile, options) {
|
|
|
450
653
|
}
|
|
451
654
|
const deletion = await readJson(join(turnDirectory, "deleted.json")).catch(() => null);
|
|
452
655
|
const deletedWhileWaiting = typeof deletion?.slot === "number" && deletion.slot >= firstWaitedTurn;
|
|
656
|
+
if (deletedWhileWaiting && !await exists(profileConfigPath(profile))) throw new ProfileDeletedError(profile);
|
|
453
657
|
while (true) {
|
|
454
658
|
const token = randomUUID();
|
|
455
659
|
if (await publishProfileLock(profile, lockPath, token, options.requireProfile === true, liveness.held ?? null)) {
|
|
@@ -486,8 +690,8 @@ async function waitOrTimeOut(profile, deadline, detail) {
|
|
|
486
690
|
await sleep(Math.min(deadline.retryMs, deadline.timeoutMs - (Date.now() - deadline.startedAt)));
|
|
487
691
|
}
|
|
488
692
|
var TURN = /^(?:0|[1-9][0-9]*)$/;
|
|
489
|
-
var
|
|
490
|
-
var relinquishedTurns = globalThis[
|
|
693
|
+
var _a3;
|
|
694
|
+
var relinquishedTurns = globalThis[_a3 = /* @__PURE__ */ Symbol.for("tinycloud.operations.relinquishedProfileTurns.v1")] ?? (globalThis[_a3] = /* @__PURE__ */ new Set());
|
|
491
695
|
var MARKER_RETRY_DELAYS_MS = [10, 50, 250];
|
|
492
696
|
var SETTLE_RETRY_MS = 1e3;
|
|
493
697
|
var SETTLE_RETRIES = 300;
|
|
@@ -601,7 +805,7 @@ async function takeTurn(profile, directory, latest, liveness) {
|
|
|
601
805
|
if (published) await markTurn(path, token, "done");
|
|
602
806
|
return null;
|
|
603
807
|
} catch (error) {
|
|
604
|
-
if (published) settleOwnTurn(directory, slot, token);
|
|
808
|
+
if (published) await settleOwnTurn(directory, slot, token);
|
|
605
809
|
throw error;
|
|
606
810
|
}
|
|
607
811
|
}
|
|
@@ -612,17 +816,25 @@ async function settleAbandonedTurn(directory, abandoned) {
|
|
|
612
816
|
}
|
|
613
817
|
await markTurn(path, abandoned.token, "done");
|
|
614
818
|
}
|
|
615
|
-
function settleOwnTurn(directory, slot, token
|
|
819
|
+
async function settleOwnTurn(directory, slot, token) {
|
|
616
820
|
relinquishedTurns.add(token);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
821
|
+
const pinned = await pinDirectory(directory).catch(() => null);
|
|
822
|
+
const path = pinned?.path(".") ?? directory;
|
|
823
|
+
let retries = SETTLE_RETRIES;
|
|
824
|
+
const attempt = () => {
|
|
825
|
+
void (async () => {
|
|
826
|
+
try {
|
|
827
|
+
const turn = await readTurn(path, slot);
|
|
828
|
+
if (turn !== null && turn !== "damaged" && turn.token === token && !turn.done) await settleAbandonedTurn(path, turn);
|
|
829
|
+
relinquishedTurns.delete(token);
|
|
830
|
+
await pinned?.close();
|
|
831
|
+
} catch {
|
|
832
|
+
if (retries-- > 0) setTimeout(attempt, SETTLE_RETRY_MS).unref();
|
|
833
|
+
else await pinned?.close();
|
|
834
|
+
}
|
|
835
|
+
})();
|
|
836
|
+
};
|
|
837
|
+
attempt();
|
|
626
838
|
}
|
|
627
839
|
async function markTurn(path, token, kind) {
|
|
628
840
|
for (let attempt = 0; ; attempt++) {
|
|
@@ -667,7 +879,7 @@ async function releaseTurn(profile, turn, liveness) {
|
|
|
667
879
|
try {
|
|
668
880
|
await markTurn(join(turn.directory, String(turn.slot)), turn.token, "done");
|
|
669
881
|
} catch {
|
|
670
|
-
settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
882
|
+
await settleOwnTurn(turn.directory, turn.slot, turn.token);
|
|
671
883
|
return;
|
|
672
884
|
}
|
|
673
885
|
await collectTurns(profile, turn, liveness).catch(() => void 0);
|
|
@@ -959,8 +1171,11 @@ export {
|
|
|
959
1171
|
DEFAULT_PROFILE,
|
|
960
1172
|
ProfileDeletedError,
|
|
961
1173
|
ProfileLockTimeoutError,
|
|
1174
|
+
ProfileSymlinkError,
|
|
962
1175
|
additionalDelegationsPath,
|
|
1176
|
+
assertRealProfileDirectory,
|
|
963
1177
|
authRequestsPath,
|
|
1178
|
+
namedProfilePath,
|
|
964
1179
|
profileConfigPath,
|
|
965
1180
|
profileLockMetadataPath,
|
|
966
1181
|
profileLockPath,
|
|
@@ -984,6 +1199,7 @@ export {
|
|
|
984
1199
|
tinycloudHomePath,
|
|
985
1200
|
updateProfileStore,
|
|
986
1201
|
upsertProfileRecord,
|
|
1202
|
+
withPinnedProfileDirectory,
|
|
987
1203
|
withProfileLock,
|
|
988
1204
|
withTinyCloudStateRoot,
|
|
989
1205
|
writeJsonAtomic,
|