agl 21.0.2 → 22.0.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.
Files changed (65) hide show
  1. package/changelog.md +9 -0
  2. package/dist_serve/bundle.js +1 -1
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.aglhome.d.ts +25 -0
  5. package/dist_ts/classes.aglhome.js +65 -0
  6. package/dist_ts/classes.authmodels.js +7 -12
  7. package/dist_ts/classes.authstore.js +2 -29
  8. package/dist_ts/classes.cli.js +93 -35
  9. package/dist_ts/classes.config.d.ts +2 -11
  10. package/dist_ts/classes.config.js +14 -80
  11. package/dist_ts/classes.controller.d.ts +4 -1
  12. package/dist_ts/classes.controller.js +64 -26
  13. package/dist_ts/classes.embeddeddb.js +6 -5
  14. package/dist_ts/classes.gitreversion.js +3 -2
  15. package/dist_ts/classes.upgradecoordinator.d.ts +6 -1
  16. package/dist_ts/classes.upgradecoordinator.js +588 -177
  17. package/dist_ts/classes.upgradetransaction.js +46 -12
  18. package/dist_ts/classes.uploadmanager.d.ts +12 -0
  19. package/dist_ts/classes.uploadmanager.js +204 -2
  20. package/dist_ts/constants.upgradeenvironment.d.ts +2 -0
  21. package/dist_ts/constants.upgradeenvironment.js +3 -0
  22. package/dist_ts/functions.controllerdataroot.d.ts +4 -5
  23. package/dist_ts/functions.controllerdataroot.js +4 -29
  24. package/dist_ts/functions.embeddeddb.d.ts +1 -1
  25. package/dist_ts/functions.embeddeddb.js +8 -3
  26. package/dist_ts/functions.runtimeenvironment.js +2 -1
  27. package/dist_ts/index.d.ts +1 -0
  28. package/dist_ts/index.js +2 -1
  29. package/dist_ts/interfaces.config.d.ts +5 -7
  30. package/dist_ts_migration/classes.documentmigrationrunner.js +3 -1
  31. package/dist_ts_migration/index.d.ts +2 -0
  32. package/dist_ts_migration/index.js +3 -1
  33. package/dist_ts_migration/v23_aglhome.d.ts +91 -0
  34. package/dist_ts_migration/v23_aglhome.js +1775 -0
  35. package/dist_ts_migration/v23_runtimeconfig.d.ts +11 -0
  36. package/dist_ts_migration/v23_runtimeconfig.js +87 -0
  37. package/dist_ts_migration/v2_controllerdataroot.d.ts +5 -0
  38. package/dist_ts_migration/v2_controllerdataroot.js +176 -15
  39. package/package.json +1 -1
  40. package/readme.md +116 -35
  41. package/readme.plan.md +24 -9
  42. package/ts/00_commitinfo_data.ts +1 -1
  43. package/ts/classes.aglhome.ts +116 -0
  44. package/ts/classes.authmodels.ts +5 -13
  45. package/ts/classes.authstore.ts +1 -39
  46. package/ts/classes.cli.ts +92 -34
  47. package/ts/classes.config.ts +20 -117
  48. package/ts/classes.controller.ts +92 -26
  49. package/ts/classes.embeddeddb.ts +5 -4
  50. package/ts/classes.gitreversion.ts +3 -2
  51. package/ts/classes.upgradecoordinator.ts +668 -191
  52. package/ts/classes.upgradetransaction.ts +46 -11
  53. package/ts/classes.uploadmanager.ts +231 -1
  54. package/ts/constants.upgradeenvironment.ts +2 -0
  55. package/ts/functions.controllerdataroot.ts +11 -47
  56. package/ts/functions.embeddeddb.ts +13 -2
  57. package/ts/functions.runtimeenvironment.ts +1 -0
  58. package/ts/index.ts +1 -0
  59. package/ts/interfaces.config.ts +5 -7
  60. package/ts_migration/classes.documentmigrationrunner.ts +2 -0
  61. package/ts_migration/index.ts +2 -0
  62. package/ts_migration/v23_aglhome.ts +2101 -0
  63. package/ts_migration/v23_runtimeconfig.ts +112 -0
  64. package/ts_migration/v2_controllerdataroot.ts +174 -16
  65. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -9,6 +9,11 @@ import {
9
9
  readControllerProcessIdentity,
10
10
  readProcessGroupMemberPids,
11
11
  } from './classes.processinspection.js';
12
+ import { resolveAGLHomePaths } from './classes.aglhome.js';
13
+ import {
14
+ upgradeCoordinationRootEnvironmentVariable,
15
+ upgradeTokenEnvironmentVariable,
16
+ } from './constants.upgradeenvironment.js';
12
17
 
13
18
  export const upgradeCoordinationVersion = 2 as const;
14
19
  const coordinationVersion = upgradeCoordinationVersion;
@@ -42,15 +47,17 @@ const transactionMetadataPattern = /^transaction-[a-f0-9]{20}\.json$/;
42
47
  const transactionTemporaryMetadataPattern = /^transaction-[a-f0-9]{20}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{8}$/;
43
48
  const transactionMutationDirectoryPattern = /^transaction-[a-f0-9]{20}\.json\.lock$/;
44
49
  const ownershipGuardDirectoryPattern = /^[a-f0-9]{64}\.guard$/;
50
+ const ownershipGuardTemporaryPattern = /^[a-f0-9]{64}\.guard\.tmp-[1-9][0-9]*-[a-f0-9]{32}$/;
51
+ const ownerRemovalPattern = /^\.owner-removing-([1-9][0-9]*)-([a-f0-9]{64})-([a-f0-9]{16})$/;
52
+ const legacyOwnerRemovalPattern = /^\.owner-removing-([1-9][0-9]*)-([a-f0-9]{16})$/;
45
53
  const tokenMetadataRetentionMs = 10 * 60 * 1000;
46
54
  const transactionRetentionMs = 24 * 60 * 60 * 1000;
47
55
  const terminalStatusRetentionMs = 10 * 60 * 1000;
48
56
  const transactionMutationTimeoutMs = 10_000;
49
57
  const ownershipGuardTimeoutMs = 10_000;
58
+ const legacyOwnerRemovalGraceMs = 30_000;
50
59
  const workerTerminationGraceMs = 5_000;
51
60
 
52
- export const upgradeTokenEnvironmentVariable = 'HARNESS_CONTROLLER_UPGRADE_TOKEN';
53
-
54
61
  type TUpgradeOwnerKind = 'upgrade' | 'start';
55
62
 
56
63
  interface IUpgradeProcessOwner {
@@ -207,6 +214,13 @@ interface IUpgradeOwnershipMutationGuardOwner {
207
214
  createdAt: number;
208
215
  }
209
216
 
217
+ interface IUpgradeOwnershipMutationGuardState {
218
+ state: 'absent' | 'empty' | 'live' | 'stale';
219
+ identity?: { dev: number; ino: number };
220
+ owner?: IUpgradeOwnershipMutationGuardOwner;
221
+ ownerMetadata?: IOwnedDirectoryOwnerMetadata<IUpgradeOwnershipMutationGuardOwner>;
222
+ }
223
+
210
224
  const assertUid = (): number => {
211
225
  if (typeof process.getuid !== 'function') {
212
226
  throw new Error(`${currentCliName} upgrade requires a POSIX effective user identity.`);
@@ -986,6 +1000,16 @@ interface IOwnedDirectoryRemovalExpectation {
986
1000
  assertRemovalAuthorized?: () => void;
987
1001
  }
988
1002
 
1003
+ interface IOwnedDirectoryOwnerMetadata<TOwner> {
1004
+ owner: TOwner;
1005
+ shape: 'owner' | 'removing' | 'restoring';
1006
+ tombstoneName?: string;
1007
+ tombstoneIdentity?: { dev: number; ino: number };
1008
+ tombstoneCtimeMs?: number;
1009
+ removalPid?: number;
1010
+ removalFingerprintHash?: string;
1011
+ }
1012
+
989
1013
  const sameOwnedDirectoryIdentity = (
990
1014
  statsArg: plugins.fs.Stats,
991
1015
  identityArg: IOwnedDirectoryRemovalExpectation['identity'],
@@ -998,6 +1022,114 @@ const exactPrimitiveRecordMatches = (leftArg: object, rightArg: object): boolean
998
1022
  return exactKeys(right, keys) && keys.every((keyArg) => left[keyArg] === right[keyArg]);
999
1023
  };
1000
1024
 
1025
+ const parseOwnerRemovalName = (nameArg: string): {
1026
+ pid: number;
1027
+ fingerprintHash?: string;
1028
+ } | undefined => {
1029
+ const current = ownerRemovalPattern.exec(nameArg);
1030
+ if (current) return { pid: Number(current[1]), fingerprintHash: current[2] };
1031
+ const legacy = legacyOwnerRemovalPattern.exec(nameArg);
1032
+ return legacy ? { pid: Number(legacy[1]) } : undefined;
1033
+ };
1034
+
1035
+ const inspectOwnedDirectoryOwnerMetadata = async <TOwner>(
1036
+ directoryArg: string,
1037
+ parseOwnerArg: (valueArg: unknown) => TOwner,
1038
+ ): Promise<IOwnedDirectoryOwnerMetadata<TOwner> | undefined> => {
1039
+ const entries = await plugins.fs.promises.readdir(directoryArg, { withFileTypes: true });
1040
+ if (entries.length === 0) return undefined;
1041
+ if (entries.length > 2) {
1042
+ throw new Error(`Upgrade coordination owner directory contains unsafe entries: ${directoryArg}`);
1043
+ }
1044
+ const ownerEntry = entries.find((entryArg) => entryArg.name === ownerFileName);
1045
+ const tombstoneEntry = entries.find((entryArg) => parseOwnerRemovalName(entryArg.name));
1046
+ if (
1047
+ entries.some((entryArg) => entryArg !== ownerEntry && entryArg !== tombstoneEntry)
1048
+ || !ownerEntry && !tombstoneEntry
1049
+ ) throw new Error(`Upgrade coordination owner directory contains unsafe entries: ${directoryArg}`);
1050
+ for (const entry of entries) {
1051
+ if (!entry.isFile() || entry.isSymbolicLink()) {
1052
+ throw new Error(`Upgrade coordination owner metadata is unsafe: ${directoryArg}`);
1053
+ }
1054
+ }
1055
+ const ownerPath = ownerEntry ? plugins.path.join(directoryArg, ownerFileName) : undefined;
1056
+ const tombstonePath = tombstoneEntry
1057
+ ? plugins.path.join(directoryArg, tombstoneEntry.name)
1058
+ : undefined;
1059
+ const [ownerStats, tombstoneStats] = await Promise.all([
1060
+ ownerPath ? plugins.fs.promises.lstat(ownerPath) : undefined,
1061
+ tombstonePath ? plugins.fs.promises.lstat(tombstonePath) : undefined,
1062
+ ]);
1063
+ for (const metadata of [ownerStats, tombstoneStats]) {
1064
+ if (!metadata) continue;
1065
+ if (
1066
+ !metadata.isFile()
1067
+ || metadata.isSymbolicLink()
1068
+ || (typeof process.getuid === 'function' && metadata.uid !== process.getuid())
1069
+ || (metadata.mode & 0o077) !== 0
1070
+ ) throw new Error(`Upgrade coordination owner metadata is unsafe: ${directoryArg}`);
1071
+ }
1072
+ let shape: IOwnedDirectoryOwnerMetadata<TOwner>['shape'];
1073
+ if (ownerStats && tombstoneStats) {
1074
+ if (
1075
+ ownerStats.dev !== tombstoneStats.dev
1076
+ || ownerStats.ino !== tombstoneStats.ino
1077
+ || ownerStats.nlink !== 2
1078
+ || tombstoneStats.nlink !== 2
1079
+ ) throw new Error(`Upgrade coordination owner restoration is invalid: ${directoryArg}`);
1080
+ shape = 'restoring';
1081
+ } else if (ownerStats) {
1082
+ if (ownerStats.nlink !== 1) {
1083
+ throw new Error(`Upgrade coordination owner link count is invalid: ${directoryArg}`);
1084
+ }
1085
+ shape = 'owner';
1086
+ } else {
1087
+ if (tombstoneStats!.nlink !== 1) {
1088
+ throw new Error(`Upgrade coordination owner tombstone link count is invalid: ${directoryArg}`);
1089
+ }
1090
+ shape = 'removing';
1091
+ }
1092
+ return {
1093
+ owner: parseOwnerArg(await readPrivateJson(ownerPath ?? tombstonePath!)),
1094
+ shape,
1095
+ ...(tombstoneEntry
1096
+ ? (() => {
1097
+ const removal = parseOwnerRemovalName(tombstoneEntry.name)!;
1098
+ return {
1099
+ tombstoneName: tombstoneEntry.name,
1100
+ tombstoneIdentity: { dev: tombstoneStats!.dev, ino: tombstoneStats!.ino },
1101
+ tombstoneCtimeMs: tombstoneStats!.ctimeMs,
1102
+ removalPid: removal.pid,
1103
+ ...(removal.fingerprintHash
1104
+ ? { removalFingerprintHash: removal.fingerprintHash }
1105
+ : {}),
1106
+ };
1107
+ })()
1108
+ : {}),
1109
+ };
1110
+ };
1111
+
1112
+ const restoreOwnedDirectoryOwnerMetadata = async <TOwner>(
1113
+ directoryArg: string,
1114
+ metadataArg: IOwnedDirectoryOwnerMetadata<TOwner>,
1115
+ ): Promise<void> => {
1116
+ if (metadataArg.shape === 'owner') return;
1117
+ const tombstonePath = plugins.path.join(directoryArg, metadataArg.tombstoneName!);
1118
+ const tombstoneStats = await plugins.fs.promises.lstat(tombstonePath);
1119
+ if (!sameOwnedDirectoryIdentity(tombstoneStats, metadataArg.tombstoneIdentity!)) {
1120
+ throw new Error('Upgrade coordination owner tombstone changed during recovery.');
1121
+ }
1122
+ if (metadataArg.shape === 'restoring') {
1123
+ await plugins.fs.promises.unlink(tombstonePath);
1124
+ } else {
1125
+ await plugins.fs.promises.rename(
1126
+ tombstonePath,
1127
+ plugins.path.join(directoryArg, ownerFileName),
1128
+ );
1129
+ }
1130
+ await syncDirectory(directoryArg);
1131
+ };
1132
+
1001
1133
  const removeExactOwnedDirectory = async (
1002
1134
  directoryArg: string,
1003
1135
  expectationArg: IOwnedDirectoryRemovalExpectation,
@@ -1020,9 +1152,17 @@ const removeExactOwnedDirectory = async (
1020
1152
  }
1021
1153
  if (entries.length === 1) {
1022
1154
  const ownerPath = plugins.path.join(directoryArg, ownerFileName);
1155
+ const removerIdentity = await readControllerProcessIdentity(process.pid);
1156
+ if (!removerIdentity) {
1157
+ throw new Error('Unable to establish upgrade ownership-removal identity.');
1158
+ }
1159
+ const removerFingerprintHash = plugins.crypto
1160
+ .createHash('sha256')
1161
+ .update(removerIdentity.fingerprint, 'utf8')
1162
+ .digest('hex');
1023
1163
  const tombstonePath = plugins.path.join(
1024
1164
  directoryArg,
1025
- `.owner-removing-${process.pid}-${plugins.crypto.randomBytes(8).toString('hex')}`,
1165
+ `.owner-removing-${process.pid}-${removerFingerprintHash}-${plugins.crypto.randomBytes(8).toString('hex')}`,
1026
1166
  );
1027
1167
  expectationArg.assertRemovalAuthorized?.();
1028
1168
  await plugins.fs.promises.rename(ownerPath, tombstonePath);
@@ -1065,6 +1205,51 @@ const removeExactOwnedDirectory = async (
1065
1205
  await syncDirectory(plugins.path.dirname(directoryArg));
1066
1206
  };
1067
1207
 
1208
+ const removeExactFailedOwnerPublication = async (
1209
+ directoryArg: string,
1210
+ identityArg: { dev: number; ino: number },
1211
+ ): Promise<void> => {
1212
+ const initialStats = await lstatIfPresent(directoryArg);
1213
+ if (!initialStats) return;
1214
+ if (!sameOwnedDirectoryIdentity(initialStats, identityArg)) {
1215
+ throw new Error('Upgrade coordination publication changed before cleanup.');
1216
+ }
1217
+ const entries = await plugins.fs.promises.readdir(directoryArg, { withFileTypes: true });
1218
+ if (
1219
+ entries.length > 1
1220
+ || entries.some((entryArg) => (
1221
+ entryArg.name !== ownerFileName
1222
+ || !entryArg.isFile()
1223
+ || entryArg.isSymbolicLink()
1224
+ ))
1225
+ ) throw new Error(`Upgrade coordination publication contains unsafe entries: ${directoryArg}`);
1226
+ if (entries.length === 1) {
1227
+ const ownerPath = plugins.path.join(directoryArg, ownerFileName);
1228
+ const ownerStats = await plugins.fs.promises.lstat(ownerPath);
1229
+ if (
1230
+ !ownerStats.isFile()
1231
+ || ownerStats.isSymbolicLink()
1232
+ || (typeof process.getuid === 'function' && ownerStats.uid !== process.getuid())
1233
+ || (ownerStats.mode & 0o077) !== 0
1234
+ || ownerStats.nlink !== 1
1235
+ ) throw new Error(`Upgrade coordination publication metadata is unsafe: ${ownerPath}`);
1236
+ await plugins.fs.promises.unlink(ownerPath);
1237
+ await syncDirectory(directoryArg);
1238
+ }
1239
+ const currentStats = await lstatIfPresent(directoryArg);
1240
+ if (!currentStats) return;
1241
+ if (!sameOwnedDirectoryIdentity(currentStats, identityArg)) {
1242
+ throw new Error('Upgrade coordination publication changed during cleanup.');
1243
+ }
1244
+ try {
1245
+ await plugins.fs.promises.rmdir(directoryArg);
1246
+ } catch (errorArg) {
1247
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return;
1248
+ throw errorArg;
1249
+ }
1250
+ await syncDirectory(plugins.path.dirname(directoryArg));
1251
+ };
1252
+
1068
1253
  class UpgradeOwnershipMutationGuard {
1069
1254
  private released = false;
1070
1255
  private releaseTask?: Promise<void>;
@@ -1072,6 +1257,7 @@ class UpgradeOwnershipMutationGuard {
1072
1257
  constructor(
1073
1258
  public readonly directory: string,
1074
1259
  public readonly owner: IUpgradeOwnershipMutationGuardOwner,
1260
+ private readonly identity: { dev: number; ino: number },
1075
1261
  ) {}
1076
1262
 
1077
1263
  public release(): Promise<void> {
@@ -1079,21 +1265,34 @@ class UpgradeOwnershipMutationGuard {
1079
1265
  if (!this.releaseTask) {
1080
1266
  let task: Promise<void>;
1081
1267
  task = (async () => {
1082
- const stats = await plugins.fs.promises.lstat(this.directory);
1083
- const owner = parseOwnershipMutationGuardOwner(await readPrivateJson(
1084
- plugins.path.join(this.directory, ownerFileName),
1085
- ));
1086
- if (!exactPrimitiveRecordMatches(owner, this.owner)) {
1268
+ const stats = await lstatIfPresent(this.directory);
1269
+ if (!stats) {
1270
+ this.released = true;
1271
+ return;
1272
+ }
1273
+ if (!sameOwnedDirectoryIdentity(stats, this.identity)) {
1274
+ throw new Error('Upgrade ownership mutation guard changed unexpectedly.');
1275
+ }
1276
+ const metadata = await inspectOwnedDirectoryOwnerMetadata(
1277
+ this.directory,
1278
+ parseOwnershipMutationGuardOwner,
1279
+ );
1280
+ if (metadata && !exactPrimitiveRecordMatches(metadata.owner, this.owner)) {
1087
1281
  throw new Error('Upgrade ownership mutation guard changed unexpectedly.');
1088
1282
  }
1283
+ if (metadata) await restoreOwnedDirectoryOwnerMetadata(this.directory, metadata);
1089
1284
  await removeExactOwnedDirectory(this.directory, {
1090
- identity: { dev: stats.dev, ino: stats.ino },
1091
- assertOwner: (valueArg) => {
1092
- const removedOwner = parseOwnershipMutationGuardOwner(valueArg);
1093
- if (!exactPrimitiveRecordMatches(removedOwner, this.owner)) {
1094
- throw new Error('Upgrade ownership mutation guard changed during release.');
1095
- }
1096
- },
1285
+ identity: this.identity,
1286
+ ...(metadata
1287
+ ? {
1288
+ assertOwner: (valueArg: unknown) => {
1289
+ const removedOwner = parseOwnershipMutationGuardOwner(valueArg);
1290
+ if (!exactPrimitiveRecordMatches(removedOwner, this.owner)) {
1291
+ throw new Error('Upgrade ownership mutation guard changed during release.');
1292
+ }
1293
+ },
1294
+ }
1295
+ : {}),
1097
1296
  });
1098
1297
  this.released = true;
1099
1298
  })().finally(() => {
@@ -1165,17 +1364,48 @@ export class UpgradeCoordinator {
1165
1364
  public readonly startLeasesDirectory: string;
1166
1365
  public readonly transactionsDirectory: string;
1167
1366
  public readonly ownershipGuardsDirectory: string;
1367
+ private readonly usesLegacyLocation: boolean;
1368
+ private readonly usesInheritedLocation: boolean;
1168
1369
  private initializationTask?: Promise<void>;
1169
1370
 
1170
- constructor(public readonly canonicalGlobalRoot: string) {
1371
+ constructor(
1372
+ public readonly canonicalGlobalRoot: string,
1373
+ ) {
1171
1374
  if (!plugins.path.isAbsolute(canonicalGlobalRoot)) {
1172
1375
  throw new Error('The canonical pnpm global root must be absolute.');
1173
1376
  }
1174
1377
  this.uid = assertUid();
1175
- this.baseDirectory = plugins.path.join(
1176
- '/tmp',
1177
- `harness-controller-upgrade-${this.uid}-${coordinationPathHash(canonicalGlobalRoot)}`,
1378
+ const canonicalBaseDirectory = plugins.path.join(
1379
+ resolveAGLHomePaths().upgrade,
1380
+ `coordination-${this.uid}-${coordinationPathHash(canonicalGlobalRoot)}`,
1178
1381
  );
1382
+ const inheritedBaseDirectory = process.env[upgradeCoordinationRootEnvironmentVariable];
1383
+ if (inheritedBaseDirectory !== undefined) {
1384
+ if (
1385
+ !plugins.path.isAbsolute(inheritedBaseDirectory)
1386
+ || plugins.path.normalize(inheritedBaseDirectory) !== inheritedBaseDirectory
1387
+ || plugins.path.parse(inheritedBaseDirectory).root === inheritedBaseDirectory
1388
+ || inheritedBaseDirectory.includes('\0')
1389
+ || Buffer.byteLength(inheritedBaseDirectory, 'utf8') > 4096
1390
+ ) throw new Error('The inherited AGL upgrade coordination root is invalid.');
1391
+ }
1392
+ const inheritedUpgradeToken = process.env[upgradeTokenEnvironmentVariable];
1393
+ const usesLegacyLocation = inheritedUpgradeToken !== undefined
1394
+ && process.env[upgradeCoordinationRootEnvironmentVariable] === undefined;
1395
+ if (
1396
+ usesLegacyLocation
1397
+ && (inheritedUpgradeToken === undefined || !upgradeTokenPattern.test(inheritedUpgradeToken))
1398
+ ) {
1399
+ throw new Error('Legacy upgrade coordination requires the exact inherited upgrade token.');
1400
+ }
1401
+ this.usesLegacyLocation = usesLegacyLocation;
1402
+ this.usesInheritedLocation = inheritedBaseDirectory !== undefined;
1403
+ this.baseDirectory = this.usesLegacyLocation
1404
+ ? plugins.path.join(
1405
+ '/tmp',
1406
+ `harness-controller-upgrade-${this.uid}-${coordinationPathHash(canonicalGlobalRoot)}`,
1407
+ )
1408
+ : inheritedBaseDirectory ?? canonicalBaseDirectory;
1179
1409
  this.upgradeLockDirectory = plugins.path.join(this.baseDirectory, 'upgrade.lock');
1180
1410
  this.startLeasesDirectory = plugins.path.join(this.baseDirectory, 'start-leases');
1181
1411
  this.transactionsDirectory = plugins.path.join(this.baseDirectory, 'transactions');
@@ -1195,19 +1425,17 @@ export class UpgradeCoordinator {
1195
1425
  }
1196
1426
 
1197
1427
  private async performInitialization(): Promise<void> {
1428
+ if (!this.usesLegacyLocation && !this.usesInheritedLocation) {
1429
+ const home = resolveAGLHomePaths();
1430
+ await ensurePrivateDirectory(home.root);
1431
+ await ensurePrivateDirectory(home.upgrade);
1432
+ }
1198
1433
  await ensurePrivateDirectory(this.baseDirectory);
1199
1434
  await ensurePrivateDirectory(this.startLeasesDirectory);
1200
1435
  await ensurePrivateDirectory(this.transactionsDirectory);
1201
1436
  await ensurePrivateDirectory(this.ownershipGuardsDirectory);
1202
- for (const entry of await plugins.fs.promises.readdir(this.ownershipGuardsDirectory, {
1203
- withFileTypes: true,
1204
- })) {
1205
- const guardPath = plugins.path.join(this.ownershipGuardsDirectory, entry.name);
1206
- if (!entry.isDirectory() || !ownershipGuardDirectoryPattern.test(entry.name)) {
1207
- throw new Error(`Upgrade ownership guard directory contains an unexpected entry: ${guardPath}`);
1208
- }
1209
- await assertPrivateDirectory(guardPath);
1210
- }
1437
+ await this.cleanupOwnershipGuardTemporaryDirectories();
1438
+ await this.cleanupOwnershipGuardDirectories();
1211
1439
  const entries = await plugins.fs.promises.readdir(this.baseDirectory, { withFileTypes: true });
1212
1440
  for (const entry of entries) {
1213
1441
  if (!entry.isFile() || !tokenMetadataPattern.test(entry.name)) continue;
@@ -1293,6 +1521,205 @@ export class UpgradeCoordinator {
1293
1521
  );
1294
1522
  }
1295
1523
 
1524
+ private async inspectOwnershipGuardDirectory(
1525
+ directoryArg: string,
1526
+ expectedTargetHashArg: string,
1527
+ attemptArg = 0,
1528
+ ): Promise<IUpgradeOwnershipMutationGuardState> {
1529
+ const stats = await lstatIfPresent(directoryArg);
1530
+ if (!stats) return { state: 'absent' };
1531
+ if (
1532
+ !stats.isDirectory()
1533
+ || stats.isSymbolicLink()
1534
+ || stats.uid !== this.uid
1535
+ || (stats.mode & 0o077) !== 0
1536
+ ) throw new Error(`Upgrade ownership mutation guard is unsafe: ${directoryArg}`);
1537
+ let ownerMetadata: IOwnedDirectoryOwnerMetadata<IUpgradeOwnershipMutationGuardOwner> | undefined;
1538
+ try {
1539
+ ownerMetadata = await inspectOwnedDirectoryOwnerMetadata(
1540
+ directoryArg,
1541
+ parseOwnershipMutationGuardOwner,
1542
+ );
1543
+ } catch (errorArg) {
1544
+ const currentStats = await lstatIfPresent(directoryArg);
1545
+ if (!currentStats) return { state: 'absent' };
1546
+ if (!sameOwnedDirectoryIdentity(currentStats, { dev: stats.dev, ino: stats.ino })) {
1547
+ if (attemptArg < 2) {
1548
+ return await this.inspectOwnershipGuardDirectory(
1549
+ directoryArg,
1550
+ expectedTargetHashArg,
1551
+ attemptArg + 1,
1552
+ );
1553
+ }
1554
+ throw new Error(`Upgrade ownership mutation guard changed repeatedly: ${directoryArg}`, {
1555
+ cause: errorArg,
1556
+ });
1557
+ }
1558
+ if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
1559
+ return {
1560
+ state: Date.now() - currentStats.mtimeMs < initializationGraceMs ? 'live' : 'empty',
1561
+ identity: { dev: currentStats.dev, ino: currentStats.ino },
1562
+ };
1563
+ }
1564
+ throw errorArg;
1565
+ }
1566
+ if (!ownerMetadata) {
1567
+ return {
1568
+ state: Date.now() - stats.mtimeMs < initializationGraceMs ? 'live' : 'empty',
1569
+ identity: { dev: stats.dev, ino: stats.ino },
1570
+ };
1571
+ }
1572
+ const owner = ownerMetadata.owner;
1573
+ if (owner.uid !== this.uid || owner.targetHash !== expectedTargetHashArg) {
1574
+ throw new Error(`Upgrade ownership mutation guard binding is invalid: ${directoryArg}`);
1575
+ }
1576
+ let processIdentity: Awaited<ReturnType<typeof readControllerProcessIdentity>>;
1577
+ let removalProcessIdentity: Awaited<ReturnType<typeof readControllerProcessIdentity>>;
1578
+ try {
1579
+ processIdentity = await readControllerProcessIdentity(owner.pid);
1580
+ removalProcessIdentity = ownerMetadata.removalPid === undefined
1581
+ ? null
1582
+ : ownerMetadata.removalPid === owner.pid
1583
+ ? processIdentity
1584
+ : await readControllerProcessIdentity(ownerMetadata.removalPid);
1585
+ } catch (errorArg) {
1586
+ throw new Error(`The upgrade ownership mutation guard cannot be verified safely: ${directoryArg}`, {
1587
+ cause: errorArg,
1588
+ });
1589
+ }
1590
+ const ownerIsStillLive = processIdentity?.fingerprint === owner.fingerprint;
1591
+ const removerIsStillLive = ownerMetadata.removalFingerprintHash
1592
+ ? removalProcessIdentity !== null
1593
+ && plugins.crypto.createHash('sha256')
1594
+ .update(removalProcessIdentity.fingerprint, 'utf8')
1595
+ .digest('hex') === ownerMetadata.removalFingerprintHash
1596
+ : ownerMetadata.removalPid !== undefined
1597
+ && removalProcessIdentity !== null
1598
+ && Date.now() - ownerMetadata.tombstoneCtimeMs! < legacyOwnerRemovalGraceMs;
1599
+ return {
1600
+ state: ownerIsStillLive || removerIsStillLive ? 'live' : 'stale',
1601
+ identity: { dev: stats.dev, ino: stats.ino },
1602
+ owner,
1603
+ ownerMetadata,
1604
+ };
1605
+ }
1606
+
1607
+ private async reclaimOwnershipGuardDirectory(
1608
+ directoryArg: string,
1609
+ targetHashArg: string,
1610
+ ): Promise<'retry' | 'live'> {
1611
+ const inspected = await this.inspectOwnershipGuardDirectory(directoryArg, targetHashArg);
1612
+ if (inspected.state === 'absent') return 'retry';
1613
+ if (inspected.state === 'live') return 'live';
1614
+ const identity = inspected.identity!;
1615
+ if (inspected.state === 'empty') {
1616
+ const current = await lstatIfPresent(directoryArg);
1617
+ if (!current) return 'retry';
1618
+ if (!sameOwnedDirectoryIdentity(current, identity)) {
1619
+ throw new Error('Upgrade ownership mutation guard changed before empty recovery.');
1620
+ }
1621
+ if ((await plugins.fs.promises.readdir(directoryArg)).length !== 0) {
1622
+ throw new Error('Upgrade ownership mutation guard changed during empty recovery.');
1623
+ }
1624
+ await plugins.fs.promises.rmdir(directoryArg).catch((errorArg) => {
1625
+ if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1626
+ });
1627
+ await syncDirectory(this.ownershipGuardsDirectory);
1628
+ return 'retry';
1629
+ }
1630
+ await restoreOwnedDirectoryOwnerMetadata(directoryArg, inspected.ownerMetadata!);
1631
+ await removeExactOwnedDirectory(directoryArg, {
1632
+ identity,
1633
+ assertOwner: (valueArg) => {
1634
+ const removedOwner = parseOwnershipMutationGuardOwner(valueArg);
1635
+ if (!exactPrimitiveRecordMatches(removedOwner, inspected.owner!)) {
1636
+ throw new Error('Upgrade ownership mutation guard changed during stale recovery.');
1637
+ }
1638
+ },
1639
+ });
1640
+ return 'retry';
1641
+ }
1642
+
1643
+ private async cleanupOwnershipGuardDirectories(): Promise<void> {
1644
+ const entries = await plugins.fs.promises.readdir(this.ownershipGuardsDirectory, {
1645
+ withFileTypes: true,
1646
+ });
1647
+ for (const entry of entries) {
1648
+ const path = plugins.path.join(this.ownershipGuardsDirectory, entry.name);
1649
+ if (ownershipGuardTemporaryPattern.test(entry.name)) continue;
1650
+ if (!entry.isDirectory() || !ownershipGuardDirectoryPattern.test(entry.name)) {
1651
+ throw new Error(`Upgrade ownership guard directory contains an unexpected entry: ${path}`);
1652
+ }
1653
+ await this.reclaimOwnershipGuardDirectory(path, entry.name.slice(0, 64));
1654
+ }
1655
+ }
1656
+
1657
+ private async cleanupOwnershipGuardTemporaryDirectories(): Promise<void> {
1658
+ const entries = await plugins.fs.promises.readdir(this.ownershipGuardsDirectory, {
1659
+ withFileTypes: true,
1660
+ });
1661
+ for (const entry of entries) {
1662
+ if (!ownershipGuardTemporaryPattern.test(entry.name)) continue;
1663
+ const match = /^([a-f0-9]{64})\.guard\.tmp-([1-9][0-9]*)-([a-f0-9]{32})$/.exec(entry.name);
1664
+ if (!match) throw new Error(`Upgrade ownership guard temporary name is invalid: ${entry.name}`);
1665
+ const pid = Number(match[2]);
1666
+ const path = plugins.path.join(this.ownershipGuardsDirectory, entry.name);
1667
+ const stats = await lstatIfPresent(path);
1668
+ if (
1669
+ !stats
1670
+ || !stats.isDirectory()
1671
+ || stats.isSymbolicLink()
1672
+ || stats.uid !== this.uid
1673
+ || (stats.mode & 0o077) !== 0
1674
+ ) throw new Error(`Upgrade ownership guard temporary is unsafe: ${path}`);
1675
+ const ownerPath = plugins.path.join(path, ownerFileName);
1676
+ const ownerStats = await lstatIfPresent(ownerPath);
1677
+ let owner: IUpgradeOwnershipMutationGuardOwner | undefined;
1678
+ if (ownerStats) {
1679
+ if (
1680
+ !ownerStats.isFile()
1681
+ || ownerStats.isSymbolicLink()
1682
+ || ownerStats.uid !== this.uid
1683
+ || (ownerStats.mode & 0o077) !== 0
1684
+ || ownerStats.nlink !== 1
1685
+ ) throw new Error(`Upgrade ownership guard temporary owner is unsafe: ${ownerPath}`);
1686
+ try {
1687
+ owner = parseOwnershipMutationGuardOwner(await readPrivateJson(ownerPath));
1688
+ } catch (errorArg) {
1689
+ if (await readControllerProcessIdentity(pid)) {
1690
+ throw new Error(`Upgrade ownership guard temporary is owned by live PID ${pid}.`, {
1691
+ cause: errorArg,
1692
+ });
1693
+ }
1694
+ }
1695
+ }
1696
+ const live = await readControllerProcessIdentity(pid);
1697
+ if (live && (!owner || owner.pid !== pid || owner.fingerprint !== live.fingerprint)) {
1698
+ throw new Error(`Upgrade ownership guard temporary is owned by live PID ${pid}.`);
1699
+ }
1700
+ if (live) continue;
1701
+ if (owner && (owner.pid !== pid || owner.uid !== this.uid || owner.targetHash !== match[1])) {
1702
+ throw new Error(`Upgrade ownership guard temporary binding is invalid: ${path}`);
1703
+ }
1704
+ const stable = await lstatIfPresent(path);
1705
+ if (!stable || stable.dev !== stats.dev || stable.ino !== stats.ino) {
1706
+ throw new Error(`Upgrade ownership guard temporary changed before cleanup: ${path}`);
1707
+ }
1708
+ if (ownerStats) {
1709
+ const stableOwner = await lstatIfPresent(ownerPath);
1710
+ if (!stableOwner || stableOwner.dev !== ownerStats.dev || stableOwner.ino !== ownerStats.ino) {
1711
+ throw new Error(`Upgrade ownership guard temporary owner changed before cleanup: ${ownerPath}`);
1712
+ }
1713
+ await plugins.fs.promises.unlink(ownerPath);
1714
+ }
1715
+ if ((await plugins.fs.promises.readdir(path)).length !== 0) {
1716
+ throw new Error(`Upgrade ownership guard temporary contains unexpected entries: ${path}`);
1717
+ }
1718
+ await plugins.fs.promises.rmdir(path);
1719
+ await syncDirectory(this.ownershipGuardsDirectory);
1720
+ }
1721
+ }
1722
+
1296
1723
  private async acquireOwnershipMutationGuard(
1297
1724
  directoryArg: string,
1298
1725
  ): Promise<UpgradeOwnershipMutationGuard> {
@@ -1310,70 +1737,63 @@ export class UpgradeCoordinator {
1310
1737
  createdAt: Date.now(),
1311
1738
  };
1312
1739
  const deadline = Date.now() + ownershipGuardTimeoutMs;
1740
+ const temporaryDirectory = `${guardDirectory}.tmp-${owner.pid}-${owner.nonce}`;
1313
1741
  while (true) {
1742
+ let temporaryCreated = false;
1743
+ let publishedIdentity: { dev: number; ino: number } | undefined;
1314
1744
  try {
1315
- await plugins.fs.promises.mkdir(guardDirectory, { mode: 0o700 });
1316
- try {
1317
- await writePrivateJson(plugins.path.join(guardDirectory, ownerFileName), owner);
1318
- await syncDirectory(this.ownershipGuardsDirectory);
1319
- } catch (errorArg) {
1320
- // A partially initialized guard is deliberately fail-closed. It must
1321
- // never be reclaimed automatically, or the guard would repeat the
1322
- // fixed-path ownership race that it exists to prevent.
1323
- throw errorArg;
1745
+ await plugins.fs.promises.mkdir(temporaryDirectory, { mode: 0o700 });
1746
+ temporaryCreated = true;
1747
+ await writePrivateJson(plugins.path.join(temporaryDirectory, ownerFileName), owner);
1748
+ await syncDirectory(temporaryDirectory);
1749
+ const temporaryStats = await plugins.fs.promises.lstat(temporaryDirectory);
1750
+ const temporaryIdentity = { dev: temporaryStats.dev, ino: temporaryStats.ino };
1751
+ await plugins.fs.promises.rename(temporaryDirectory, guardDirectory);
1752
+ temporaryCreated = false;
1753
+ publishedIdentity = temporaryIdentity;
1754
+ const publishedStats = await plugins.fs.promises.lstat(guardDirectory);
1755
+ if (!sameOwnedDirectoryIdentity(publishedStats, publishedIdentity)) {
1756
+ throw new Error('Published upgrade ownership guard identity changed unexpectedly.');
1324
1757
  }
1325
- return new UpgradeOwnershipMutationGuard(guardDirectory, owner);
1758
+ await syncDirectory(this.ownershipGuardsDirectory);
1759
+ return new UpgradeOwnershipMutationGuard(guardDirectory, owner, publishedIdentity);
1326
1760
  } catch (errorArg) {
1327
- if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
1328
- }
1329
-
1330
- const stats = await lstatIfPresent(guardDirectory);
1331
- if (!stats) continue;
1332
- if (
1333
- !stats.isDirectory()
1334
- || stats.isSymbolicLink()
1335
- || stats.uid !== this.uid
1336
- || (stats.mode & 0o077) !== 0
1337
- ) throw new Error(`Upgrade ownership mutation guard is unsafe: ${guardDirectory}`);
1338
-
1339
- let existingOwner: IUpgradeOwnershipMutationGuardOwner;
1340
- try {
1341
- existingOwner = parseOwnershipMutationGuardOwner(await readPrivateJson(
1342
- plugins.path.join(guardDirectory, ownerFileName),
1343
- ));
1344
- } catch (errorArg) {
1345
- const refreshedStats = await lstatIfPresent(guardDirectory);
1346
- if (!refreshedStats || refreshedStats.dev !== stats.dev || refreshedStats.ino !== stats.ino) {
1347
- continue;
1761
+ const cleanupErrors: unknown[] = [];
1762
+ if (temporaryCreated) {
1763
+ try {
1764
+ await plugins.fs.promises.rm(temporaryDirectory, { recursive: true, force: true });
1765
+ } catch (cleanupErrorArg) {
1766
+ cleanupErrors.push(cleanupErrorArg);
1767
+ }
1348
1768
  }
1349
- if (
1350
- Date.now() - refreshedStats.mtimeMs < initializationGraceMs
1351
- && Date.now() < deadline
1352
- ) {
1353
- await new Promise((resolve) => setTimeout(resolve, 25));
1354
- continue;
1769
+ if (publishedIdentity) {
1770
+ try {
1771
+ await removeExactOwnedDirectory(guardDirectory, {
1772
+ identity: publishedIdentity,
1773
+ assertOwner: (valueArg) => {
1774
+ const publishedOwner = parseOwnershipMutationGuardOwner(valueArg);
1775
+ if (!exactPrimitiveRecordMatches(publishedOwner, owner)) {
1776
+ throw new Error('Published upgrade ownership guard changed during cleanup.');
1777
+ }
1778
+ },
1779
+ });
1780
+ } catch (cleanupErrorArg) {
1781
+ cleanupErrors.push(cleanupErrorArg);
1782
+ }
1355
1783
  }
1356
- throw new Error(
1357
- `Upgrade ownership mutation guard is incomplete and cannot be reclaimed automatically: ${guardDirectory}. Inspect it after proving its owner is gone, or reboot the host to clear /tmp.`,
1358
- { cause: errorArg },
1359
- );
1360
- }
1361
- if (existingOwner.uid !== this.uid || existingOwner.targetHash !== targetHash) {
1362
- throw new Error(`Upgrade ownership mutation guard binding is invalid: ${guardDirectory}`);
1363
- }
1364
- let existingIdentity: Awaited<ReturnType<typeof readControllerProcessIdentity>>;
1365
- try {
1366
- existingIdentity = await readControllerProcessIdentity(existingOwner.pid);
1367
- } catch (errorArg) {
1368
- throw new Error(
1369
- `The existing upgrade ownership mutation guard cannot be verified safely: ${guardDirectory}`,
1370
- { cause: errorArg },
1371
- );
1784
+ if (cleanupErrors.length > 0) {
1785
+ throw new AggregateError(
1786
+ [errorArg, ...cleanupErrors],
1787
+ 'Upgrade ownership guard publication failed and cleanup was incomplete.',
1788
+ { cause: errorArg },
1789
+ );
1790
+ }
1791
+ const code = (errorArg as NodeJS.ErrnoException).code;
1792
+ if (code !== 'EEXIST' && code !== 'ENOTEMPTY') throw errorArg;
1372
1793
  }
1373
- if (existingIdentity?.fingerprint !== existingOwner.fingerprint) {
1374
- throw new Error(
1375
- `Upgrade ownership mutation guard is stale and cannot be reclaimed automatically: ${guardDirectory}. Inspect it after proving its owner is gone, or reboot the host to clear /tmp.`,
1376
- );
1794
+
1795
+ if (await this.reclaimOwnershipGuardDirectory(guardDirectory, targetHash) === 'retry') {
1796
+ continue;
1377
1797
  }
1378
1798
  if (Date.now() >= deadline) {
1379
1799
  throw new Error('Timed out waiting for an upgrade ownership mutation guard.');
@@ -1419,6 +1839,9 @@ export class UpgradeCoordinator {
1419
1839
  throw new Error(`The existing ${currentCliName} ownership cannot be verified safely.`);
1420
1840
  }
1421
1841
  if (current.state !== 'stale') return false;
1842
+ if (current.ownerMetadata) {
1843
+ await restoreOwnedDirectoryOwnerMetadata(directoryArg, current.ownerMetadata);
1844
+ }
1422
1845
  await removeExactOwnedDirectory(directoryArg, {
1423
1846
  identity: current.identity!,
1424
1847
  ...(current.owner
@@ -1439,41 +1862,35 @@ export class UpgradeCoordinator {
1439
1862
  private async releaseOwnedDirectory(
1440
1863
  directoryArg: string,
1441
1864
  expectedOwnerArg: IUpgradeProcessOwner,
1865
+ expectedIdentityArg: { dev: number; ino: number },
1442
1866
  shouldReleaseArg?: () => boolean,
1443
1867
  ): Promise<void> {
1444
1868
  await this.withOwnershipMutationGuard(directoryArg, async () => {
1445
- const stats = await plugins.fs.promises.lstat(directoryArg);
1446
- let currentOwner: IUpgradeProcessOwner;
1447
- try {
1448
- currentOwner = parseOwner(await readPrivateJson(
1449
- plugins.path.join(directoryArg, ownerFileName),
1450
- ));
1451
- } catch (errorArg) {
1452
- if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
1453
- throw new Error('Upgrade coordination ownership changed unexpectedly.', { cause: errorArg });
1454
- }
1455
- throw errorArg;
1869
+ const stats = await lstatIfPresent(directoryArg);
1870
+ if (!stats) return;
1871
+ if (!sameOwnedDirectoryIdentity(stats, expectedIdentityArg)) {
1872
+ throw new Error('Upgrade coordination ownership changed unexpectedly.');
1456
1873
  }
1457
- if (!exactPrimitiveRecordMatches(currentOwner, expectedOwnerArg)) {
1874
+ const metadata = await inspectOwnedDirectoryOwnerMetadata(directoryArg, parseOwner);
1875
+ if (metadata && !exactPrimitiveRecordMatches(metadata.owner, expectedOwnerArg)) {
1458
1876
  throw new Error('Upgrade coordination ownership changed unexpectedly.');
1459
1877
  }
1878
+ if (shouldReleaseArg && !shouldReleaseArg()) {
1879
+ throw new Error('Upgrade coordination release is no longer authorized.');
1880
+ }
1881
+ if (metadata) await restoreOwnedDirectoryOwnerMetadata(directoryArg, metadata);
1460
1882
  await removeExactOwnedDirectory(directoryArg, {
1461
- identity: { dev: stats.dev, ino: stats.ino },
1462
- ...(shouldReleaseArg
1883
+ identity: expectedIdentityArg,
1884
+ ...(metadata
1463
1885
  ? {
1464
- assertRemovalAuthorized: () => {
1465
- if (!shouldReleaseArg()) {
1466
- throw new Error('Upgrade coordination release is no longer authorized.');
1886
+ assertOwner: (valueArg: unknown) => {
1887
+ const removedOwner = parseOwner(valueArg);
1888
+ if (!exactPrimitiveRecordMatches(removedOwner, expectedOwnerArg)) {
1889
+ throw new Error('Upgrade coordination ownership changed during release.');
1467
1890
  }
1468
1891
  },
1469
1892
  }
1470
1893
  : {}),
1471
- assertOwner: (valueArg) => {
1472
- const removedOwner = parseOwner(valueArg);
1473
- if (!exactPrimitiveRecordMatches(removedOwner, expectedOwnerArg)) {
1474
- throw new Error('Upgrade coordination ownership changed during release.');
1475
- }
1476
- },
1477
1894
  });
1478
1895
  });
1479
1896
  }
@@ -1481,10 +1898,13 @@ export class UpgradeCoordinator {
1481
1898
  private async inspectTransactionMutationDirectory(
1482
1899
  directoryArg: string,
1483
1900
  expectedTokenHashArg?: string,
1901
+ attemptArg = 0,
1484
1902
  ): Promise<{
1485
1903
  state: 'absent' | 'live' | 'recent-invalid' | 'stale' | 'indeterminate';
1486
1904
  identity?: { dev: number; ino: number };
1487
1905
  owner?: IUpgradeTransactionMutationOwner;
1906
+ ownerMetadata?: IOwnedDirectoryOwnerMetadata<IUpgradeTransactionMutationOwner>;
1907
+ inspectionError?: unknown;
1488
1908
  }> {
1489
1909
  const stats = await lstatIfPresent(directoryArg);
1490
1910
  if (!stats) return { state: 'absent' };
@@ -1494,15 +1914,54 @@ export class UpgradeCoordinator {
1494
1914
  || stats.uid !== this.uid
1495
1915
  || (stats.mode & 0o077) !== 0
1496
1916
  ) throw new Error('The upgrade transaction mutation lock is unsafe.');
1497
- let rawOwner: unknown;
1917
+ let ownerMetadata: IOwnedDirectoryOwnerMetadata<IUpgradeTransactionMutationOwner> | undefined;
1498
1918
  try {
1499
- rawOwner = await readPrivateJson(plugins.path.join(directoryArg, ownerFileName));
1919
+ ownerMetadata = await inspectOwnedDirectoryOwnerMetadata(
1920
+ directoryArg,
1921
+ parseTransactionMutationOwner,
1922
+ );
1500
1923
  } catch (errorArg) {
1924
+ const currentStats = await lstatIfPresent(directoryArg);
1925
+ if (!currentStats) return { state: 'absent' };
1926
+ if (!sameOwnedDirectoryIdentity(currentStats, { dev: stats.dev, ino: stats.ino })) {
1927
+ if (attemptArg < 2) {
1928
+ return await this.inspectTransactionMutationDirectory(
1929
+ directoryArg,
1930
+ expectedTokenHashArg,
1931
+ attemptArg + 1,
1932
+ );
1933
+ }
1934
+ return {
1935
+ state: 'indeterminate',
1936
+ identity: { dev: currentStats.dev, ino: currentStats.ino },
1937
+ inspectionError: errorArg,
1938
+ };
1939
+ }
1501
1940
  const code = (errorArg as NodeJS.ErrnoException).code;
1502
- if (code === 'HCON_INVALID_JSON') {
1503
- return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1941
+ if (code === 'ENOENT') {
1942
+ return {
1943
+ state: Date.now() - currentStats.mtimeMs < initializationGraceMs
1944
+ ? 'recent-invalid'
1945
+ : 'stale',
1946
+ identity: { dev: currentStats.dev, ino: currentStats.ino },
1947
+ };
1504
1948
  }
1505
- if (code !== 'ENOENT') throw errorArg;
1949
+ if (
1950
+ Date.now() - currentStats.mtimeMs < initializationGraceMs
1951
+ && (code === undefined || code === 'HCON_INVALID_JSON')
1952
+ ) {
1953
+ return {
1954
+ state: 'recent-invalid',
1955
+ identity: { dev: currentStats.dev, ino: currentStats.ino },
1956
+ };
1957
+ }
1958
+ return {
1959
+ state: 'indeterminate',
1960
+ identity: { dev: currentStats.dev, ino: currentStats.ino },
1961
+ inspectionError: errorArg,
1962
+ };
1963
+ }
1964
+ if (!ownerMetadata) {
1506
1965
  return {
1507
1966
  state: Date.now() - stats.mtimeMs < initializationGraceMs
1508
1967
  ? 'recent-invalid'
@@ -1510,12 +1969,7 @@ export class UpgradeCoordinator {
1510
1969
  identity: { dev: stats.dev, ino: stats.ino },
1511
1970
  };
1512
1971
  }
1513
- let owner: IUpgradeTransactionMutationOwner;
1514
- try {
1515
- owner = parseTransactionMutationOwner(rawOwner);
1516
- } catch {
1517
- return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1518
- }
1972
+ const owner = ownerMetadata.owner;
1519
1973
  if (
1520
1974
  owner.uid !== this.uid
1521
1975
  || (expectedTokenHashArg !== undefined
@@ -1527,9 +1981,14 @@ export class UpgradeCoordinator {
1527
1981
  state: ownerIdentity?.fingerprint === owner.fingerprint ? 'live' : 'stale',
1528
1982
  identity: { dev: stats.dev, ino: stats.ino },
1529
1983
  owner,
1984
+ ownerMetadata,
1985
+ };
1986
+ } catch (errorArg) {
1987
+ return {
1988
+ state: 'indeterminate',
1989
+ identity: { dev: stats.dev, ino: stats.ino },
1990
+ inspectionError: errorArg,
1530
1991
  };
1531
- } catch {
1532
- return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1533
1992
  }
1534
1993
  }
1535
1994
 
@@ -1544,9 +2003,14 @@ export class UpgradeCoordinator {
1544
2003
  );
1545
2004
  if (current.state === 'absent') return false;
1546
2005
  if (current.state === 'indeterminate') {
1547
- throw new Error('The upgrade transaction mutation owner cannot be verified safely.');
2006
+ throw new Error('The upgrade transaction mutation owner cannot be verified safely.', {
2007
+ cause: current.inspectionError,
2008
+ });
1548
2009
  }
1549
2010
  if (current.state !== 'stale') return false;
2011
+ if (current.ownerMetadata) {
2012
+ await restoreOwnedDirectoryOwnerMetadata(directoryArg, current.ownerMetadata);
2013
+ }
1550
2014
  await removeExactOwnedDirectory(directoryArg, {
1551
2015
  identity: current.identity!,
1552
2016
  ...(current.owner
@@ -1572,6 +2036,7 @@ export class UpgradeCoordinator {
1572
2036
  state: 'absent' | 'live' | 'recent-invalid' | 'stale' | 'indeterminate';
1573
2037
  identity?: { dev: number; ino: number };
1574
2038
  owner?: IUpgradeProcessOwner;
2039
+ ownerMetadata?: IOwnedDirectoryOwnerMetadata<IUpgradeProcessOwner>;
1575
2040
  }> {
1576
2041
  let stats: plugins.fs.Stats;
1577
2042
  try {
@@ -1586,15 +2051,13 @@ export class UpgradeCoordinator {
1586
2051
  if (stats.uid !== this.uid || (stats.mode & 0o077) !== 0) {
1587
2052
  throw new Error(`Upgrade coordination owner path is not private: ${directoryArg}`);
1588
2053
  }
1589
- let ownerValue: unknown;
2054
+ let ownerMetadata: IOwnedDirectoryOwnerMetadata<IUpgradeProcessOwner> | undefined;
1590
2055
  try {
1591
- ownerValue = await readPrivateJson(plugins.path.join(directoryArg, ownerFileName));
1592
- } catch (errorArg) {
1593
- const code = (errorArg as NodeJS.ErrnoException).code;
1594
- if (code === 'HCON_INVALID_JSON') {
1595
- return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1596
- }
1597
- if (code !== 'ENOENT') throw errorArg;
2056
+ ownerMetadata = await inspectOwnedDirectoryOwnerMetadata(directoryArg, parseOwner);
2057
+ } catch {
2058
+ return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
2059
+ }
2060
+ if (!ownerMetadata) {
1598
2061
  return {
1599
2062
  state: Date.now() - stats.mtimeMs < initializationGraceMs
1600
2063
  ? 'recent-invalid'
@@ -1602,17 +2065,13 @@ export class UpgradeCoordinator {
1602
2065
  identity: { dev: stats.dev, ino: stats.ino },
1603
2066
  };
1604
2067
  }
1605
- let owner: IUpgradeProcessOwner;
1606
- try {
1607
- owner = parseOwner(ownerValue);
1608
- } catch {
1609
- return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
1610
- }
2068
+ const owner = ownerMetadata.owner;
1611
2069
  try {
1612
2070
  return {
1613
2071
  state: await ownerIsLive(owner) ? 'live' : 'stale',
1614
2072
  identity: { dev: stats.dev, ino: stats.ino },
1615
2073
  owner,
2074
+ ownerMetadata,
1616
2075
  };
1617
2076
  } catch {
1618
2077
  return { state: 'indeterminate', identity: { dev: stats.dev, ino: stats.ino } };
@@ -1622,7 +2081,11 @@ export class UpgradeCoordinator {
1622
2081
  private async createOwnedDirectory(
1623
2082
  directoryArg: string,
1624
2083
  ownerArg: IUpgradeProcessOwner,
1625
- ): Promise<{ directory: string; owner: IUpgradeProcessOwner } | undefined> {
2084
+ ): Promise<{
2085
+ directory: string;
2086
+ owner: IUpgradeProcessOwner;
2087
+ identity: { dev: number; ino: number };
2088
+ } | undefined> {
1626
2089
  return await this.withOwnershipMutationGuard(directoryArg, async () => {
1627
2090
  try {
1628
2091
  await plugins.fs.promises.mkdir(directoryArg, { mode: 0o700 });
@@ -1630,13 +2093,26 @@ export class UpgradeCoordinator {
1630
2093
  if ((errorArg as NodeJS.ErrnoException).code === 'EEXIST') return undefined;
1631
2094
  throw errorArg;
1632
2095
  }
2096
+ const stats = await plugins.fs.promises.lstat(directoryArg);
2097
+ const identity = { dev: stats.dev, ino: stats.ino };
1633
2098
  try {
1634
2099
  await writePrivateJson(plugins.path.join(directoryArg, ownerFileName), ownerArg);
1635
2100
  await syncDirectory(plugins.path.dirname(directoryArg));
1636
- return { directory: directoryArg, owner: ownerArg };
2101
+ return {
2102
+ directory: directoryArg,
2103
+ owner: ownerArg,
2104
+ identity,
2105
+ };
1637
2106
  } catch (errorArg) {
1638
- // Preserve a failed initialization directory. Contenders treat it as
1639
- // recent-invalid, then reclaim it only after the bounded grace period.
2107
+ try {
2108
+ await removeExactFailedOwnerPublication(directoryArg, identity);
2109
+ } catch (cleanupErrorArg) {
2110
+ throw new AggregateError(
2111
+ [errorArg, cleanupErrorArg],
2112
+ 'Upgrade coordination owner publication failed and cleanup was incomplete.',
2113
+ { cause: errorArg },
2114
+ );
2115
+ }
1640
2116
  throw errorArg;
1641
2117
  }
1642
2118
  });
@@ -1662,20 +2138,13 @@ export class UpgradeCoordinator {
1662
2138
  (directoryArg, ownerArg, shouldReleaseArg) => this.releaseOwnedDirectory(
1663
2139
  directoryArg,
1664
2140
  ownerArg,
2141
+ created.identity,
1665
2142
  shouldReleaseArg,
1666
2143
  ),
1667
2144
  );
1668
2145
  const state = await this.inspectOwnedDirectory(this.upgradeLockDirectory);
1669
2146
  if (state.state === 'live') {
1670
- let currentOwner: IUpgradeProcessOwner;
1671
- try {
1672
- currentOwner = parseOwner(await readPrivateJson(
1673
- plugins.path.join(this.upgradeLockDirectory, ownerFileName),
1674
- ));
1675
- } catch (errorArg) {
1676
- if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') continue;
1677
- throw errorArg;
1678
- }
2147
+ const currentOwner = state.owner!;
1679
2148
  if (
1680
2149
  currentOwner.pid === owner.pid
1681
2150
  && currentOwner.fingerprint === owner.fingerprint
@@ -1686,6 +2155,7 @@ export class UpgradeCoordinator {
1686
2155
  (directoryArg, ownerArg, shouldReleaseArg) => this.releaseOwnedDirectory(
1687
2156
  directoryArg,
1688
2157
  ownerArg,
2158
+ state.identity!,
1689
2159
  shouldReleaseArg,
1690
2160
  ),
1691
2161
  );
@@ -1763,13 +2233,7 @@ export class UpgradeCoordinator {
1763
2233
  await this.reclaimStaleOwnedDirectory(this.upgradeLockDirectory);
1764
2234
  continue;
1765
2235
  }
1766
- try {
1767
- return parseOwner(await readPrivateJson(
1768
- plugins.path.join(this.upgradeLockDirectory, ownerFileName),
1769
- ));
1770
- } catch (errorArg) {
1771
- if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
1772
- }
2236
+ return state.owner!;
1773
2237
  }
1774
2238
  throw new Error(`The ${currentCliName} upgrade owner changed repeatedly during admission.`);
1775
2239
  }
@@ -1806,6 +2270,7 @@ export class UpgradeCoordinator {
1806
2270
  (directoryArg, ownerArg, shouldReleaseArg) => this.releaseOwnedDirectory(
1807
2271
  directoryArg,
1808
2272
  ownerArg,
2273
+ created.identity,
1809
2274
  shouldReleaseArg,
1810
2275
  ),
1811
2276
  );
@@ -1893,37 +2358,54 @@ export class UpgradeCoordinator {
1893
2358
  createdAt: Date.now(),
1894
2359
  };
1895
2360
  while (true) {
1896
- try {
1897
- await plugins.fs.promises.mkdir(lockDirectory, { mode: 0o700 });
2361
+ const acquisition = await this.withOwnershipMutationGuard(lockDirectory, async () => {
2362
+ try {
2363
+ await plugins.fs.promises.mkdir(lockDirectory, { mode: 0o700 });
2364
+ } catch (errorArg) {
2365
+ if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
2366
+ const current = await this.inspectTransactionMutationDirectory(
2367
+ lockDirectory,
2368
+ owner.tokenHash,
2369
+ );
2370
+ return { state: 'contended' as const, current };
2371
+ }
2372
+ const lockStats = await plugins.fs.promises.lstat(lockDirectory);
2373
+ const lockIdentity = { dev: lockStats.dev, ino: lockStats.ino };
1898
2374
  try {
1899
2375
  await writePrivateJson(plugins.path.join(lockDirectory, ownerFileName), owner);
2376
+ return { state: 'acquired' as const };
1900
2377
  } catch (errorArg) {
1901
- // Leave partial initialization for the next contender's bounded stale check.
2378
+ try {
2379
+ await removeExactFailedOwnerPublication(lockDirectory, lockIdentity);
2380
+ } catch (cleanupErrorArg) {
2381
+ throw new AggregateError(
2382
+ [errorArg, cleanupErrorArg],
2383
+ 'Upgrade transaction mutation owner publication failed and cleanup was incomplete.',
2384
+ { cause: errorArg },
2385
+ );
2386
+ }
1902
2387
  throw errorArg;
1903
2388
  }
1904
- break;
1905
- } catch (errorArg) {
1906
- if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') throw errorArg;
1907
- const current = await this.inspectTransactionMutationDirectory(
2389
+ });
2390
+ if (acquisition.state === 'acquired') break;
2391
+ const current = acquisition.current;
2392
+ if (current.state === 'absent') continue;
2393
+ if (current.state === 'stale') {
2394
+ await this.reclaimStaleTransactionMutationDirectory(
1908
2395
  lockDirectory,
1909
2396
  owner.tokenHash,
1910
2397
  );
1911
- if (current.state === 'absent') continue;
1912
- if (current.state === 'stale') {
1913
- await this.reclaimStaleTransactionMutationDirectory(
1914
- lockDirectory,
1915
- owner.tokenHash,
1916
- );
1917
- continue;
1918
- }
1919
- if (current.state === 'indeterminate') {
1920
- throw new Error('The upgrade transaction mutation owner cannot be verified safely.');
1921
- }
1922
- if (Date.now() >= deadline) {
1923
- throw new Error('Timed out waiting to update the upgrade transaction.');
1924
- }
1925
- await new Promise((resolve) => setTimeout(resolve, 25));
2398
+ continue;
1926
2399
  }
2400
+ if (current.state === 'indeterminate') {
2401
+ throw new Error('The upgrade transaction mutation owner cannot be verified safely.', {
2402
+ cause: current.inspectionError,
2403
+ });
2404
+ }
2405
+ if (Date.now() >= deadline) {
2406
+ throw new Error('Timed out waiting to update the upgrade transaction.');
2407
+ }
2408
+ await new Promise((resolve) => setTimeout(resolve, 25));
1927
2409
  }
1928
2410
  let result: T | undefined;
1929
2411
  let operationError: unknown;
@@ -1936,23 +2418,18 @@ export class UpgradeCoordinator {
1936
2418
  try {
1937
2419
  await this.withOwnershipMutationGuard(lockDirectory, async () => {
1938
2420
  const stats = await plugins.fs.promises.lstat(lockDirectory);
1939
- let currentOwner: IUpgradeTransactionMutationOwner;
1940
- try {
1941
- currentOwner = parseTransactionMutationOwner(await readPrivateJson(
1942
- plugins.path.join(lockDirectory, ownerFileName),
1943
- ));
1944
- } catch (errorArg) {
1945
- if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') {
1946
- throw new Error(
1947
- 'The upgrade transaction mutation lock ownership changed unexpectedly.',
1948
- { cause: errorArg },
1949
- );
1950
- }
1951
- throw errorArg;
2421
+ const metadata = await inspectOwnedDirectoryOwnerMetadata(
2422
+ lockDirectory,
2423
+ parseTransactionMutationOwner,
2424
+ );
2425
+ if (!metadata) {
2426
+ throw new Error('The upgrade transaction mutation lock ownership changed unexpectedly.');
1952
2427
  }
2428
+ const currentOwner = metadata.owner;
1953
2429
  if (!exactPrimitiveRecordMatches(currentOwner, owner)) {
1954
2430
  throw new Error('The upgrade transaction mutation lock ownership changed unexpectedly.');
1955
2431
  }
2432
+ await restoreOwnedDirectoryOwnerMetadata(lockDirectory, metadata);
1956
2433
  await removeExactOwnedDirectory(lockDirectory, {
1957
2434
  identity: { dev: stats.dev, ino: stats.ino },
1958
2435
  assertOwner: (valueArg) => {