agl 22.0.0 → 22.0.2
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/changelog.md +20 -0
- package/dist_serve/bundle.js +520 -509
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.authstore.d.ts +1 -2
- package/dist_ts/classes.authstore.js +1 -6
- package/dist_ts/classes.cli.d.ts +4 -2
- package/dist_ts/classes.cli.js +161 -68
- package/dist_ts/classes.controller.js +68 -30
- package/dist_ts/classes.upgradecoordinator.d.ts +39 -1
- package/dist_ts/classes.upgradecoordinator.js +740 -74
- package/dist_ts/classes.upgradetransaction.d.ts +68 -1
- package/dist_ts/classes.upgradetransaction.js +383 -223
- package/dist_ts_migration/classes.documentmigrationrunner.js +3 -1
- package/dist_ts_migration/index.d.ts +2 -0
- package/dist_ts_migration/index.js +3 -1
- package/dist_ts_migration/v24_legacyterminallayout.d.ts +18 -0
- package/dist_ts_migration/v24_legacyterminallayout.js +79 -0
- package/dist_ts_migration/v24_legacyterminallayoutmigration.d.ts +6 -0
- package/dist_ts_migration/v24_legacyterminallayoutmigration.js +67 -0
- package/package.json +1 -1
- package/readme.md +255 -378
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.authstore.ts +0 -15
- package/ts/classes.cli.ts +184 -83
- package/ts/classes.controller.ts +79 -33
- package/ts/classes.upgradecoordinator.ts +876 -74
- package/ts/classes.upgradetransaction.ts +574 -250
- package/ts_migration/classes.documentmigrationrunner.ts +2 -0
- package/ts_migration/index.ts +2 -0
- package/ts_migration/v24_legacyterminallayout.ts +131 -0
- package/ts_migration/v24_legacyterminallayoutmigration.ts +88 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements.harnesscontrollerapp.ts +32 -2
|
@@ -41,11 +41,17 @@ const initializationGraceMs = 30_000;
|
|
|
41
41
|
const startLeaseDrainTimeoutMs = 60_000;
|
|
42
42
|
const upgradeTokenPattern = /^[A-Za-z0-9_-]{43}$/;
|
|
43
43
|
const upgradeTokenHashPattern = /^[a-f0-9]{64}$/;
|
|
44
|
+
const upgradeSemverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
44
45
|
const leaseDirectoryPattern = /^[a-f0-9]{20}$/;
|
|
45
46
|
const tokenMetadataPattern = /^(?:grant|ack|action-(?:prepare|finalize))-[a-f0-9]{20}\.json(?:\.consuming-[1-9][0-9]*-[a-f0-9]{8})?$/;
|
|
47
|
+
const acknowledgementTemporaryPattern = /^ack-[a-f0-9]{20}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{8}$/;
|
|
48
|
+
const ownerTransferTemporaryPattern = /^owner-transfer-[1-9][0-9]*-[a-f0-9]{8}\.json\.tmp$/;
|
|
46
49
|
const transactionMetadataPattern = /^transaction-[a-f0-9]{20}\.json$/;
|
|
47
50
|
const transactionTemporaryMetadataPattern = /^transaction-[a-f0-9]{20}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{8}$/;
|
|
48
51
|
const transactionMutationDirectoryPattern = /^transaction-[a-f0-9]{20}\.json\.lock$/;
|
|
52
|
+
const transactionAdoptionPattern = /^transaction-([a-f0-9]{20})\.json\.adopting-([a-f0-9]{20})$/;
|
|
53
|
+
const transactionAdoptionAuditPattern = /^adoption-([a-f0-9]{20})-([a-f0-9]{20})\.json$/;
|
|
54
|
+
const transactionAdoptionAuditTemporaryPattern = /^adoption-[a-f0-9]{20}-[a-f0-9]{20}\.json\.tmp-[1-9][0-9]*-[a-f0-9]{8}$/;
|
|
49
55
|
const ownershipGuardDirectoryPattern = /^[a-f0-9]{64}\.guard$/;
|
|
50
56
|
const ownershipGuardTemporaryPattern = /^[a-f0-9]{64}\.guard\.tmp-[1-9][0-9]*-[a-f0-9]{32}$/;
|
|
51
57
|
const ownerRemovalPattern = /^\.owner-removing-([1-9][0-9]*)-([a-f0-9]{64})-([a-f0-9]{16})$/;
|
|
@@ -60,6 +66,13 @@ const workerTerminationGraceMs = 5_000;
|
|
|
60
66
|
|
|
61
67
|
type TUpgradeOwnerKind = 'upgrade' | 'start';
|
|
62
68
|
|
|
69
|
+
interface IUpgradeSemver {
|
|
70
|
+
major: string;
|
|
71
|
+
minor: string;
|
|
72
|
+
patch: string;
|
|
73
|
+
prerelease: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
63
76
|
interface IUpgradeProcessOwner {
|
|
64
77
|
version: typeof coordinationVersion;
|
|
65
78
|
kind: TUpgradeOwnerKind;
|
|
@@ -90,6 +103,14 @@ interface IUpgradeLaunchGrant {
|
|
|
90
103
|
controller: IUpgradeExpectedController;
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
interface IUpgradeWorkerAcknowledgement {
|
|
107
|
+
version: typeof coordinationVersion;
|
|
108
|
+
tokenHash: string;
|
|
109
|
+
pid: number;
|
|
110
|
+
processGroupId: number;
|
|
111
|
+
fingerprint: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
export interface IUpgradeWorkerPayload {
|
|
94
115
|
version: typeof coordinationVersion;
|
|
95
116
|
token: string;
|
|
@@ -158,6 +179,7 @@ export interface IUpgradeTransactionV2 extends IUpgradeTransactionBase {
|
|
|
158
179
|
version: typeof coordinationVersion;
|
|
159
180
|
sourceVersion: string;
|
|
160
181
|
targetVersion?: string;
|
|
182
|
+
registryUrl?: string;
|
|
161
183
|
targetStartupInvoked?: true;
|
|
162
184
|
}
|
|
163
185
|
|
|
@@ -173,6 +195,7 @@ export interface IUpgradeTransactionV3 extends IUpgradeTransactionBase {
|
|
|
173
195
|
targetManagementVersion: typeof upgradePackageTransitionTarget.managementVersion;
|
|
174
196
|
targetCliName: typeof upgradePackageTransitionTarget.cliName;
|
|
175
197
|
targetCliRelativePath: typeof upgradePackageTransitionTarget.cliRelativePath;
|
|
198
|
+
recoveryTargetVersion?: string;
|
|
176
199
|
packageTransitionStarted?: true;
|
|
177
200
|
groupedTargetVerified?: true;
|
|
178
201
|
targetPackageCommitStarted?: true;
|
|
@@ -182,6 +205,35 @@ export interface IUpgradeTransactionV3 extends IUpgradeTransactionBase {
|
|
|
182
205
|
|
|
183
206
|
export type TUpgradeTransaction = IUpgradeTransactionV2 | IUpgradeTransactionV3;
|
|
184
207
|
|
|
208
|
+
export interface IUpgradeTransactionInventoryEntry {
|
|
209
|
+
fileName: string;
|
|
210
|
+
state: 'active' | 'adopting';
|
|
211
|
+
transaction: TUpgradeTransaction;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface IUpgradeTransactionInventory {
|
|
215
|
+
transactions: IUpgradeTransactionInventoryEntry[];
|
|
216
|
+
tokenMetadataFileNames: string[];
|
|
217
|
+
mutationDirectoryNames: string[];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface IAdoptOrphanedUpgradeTransactionOptions {
|
|
221
|
+
lock: UpgradeInstallationLock;
|
|
222
|
+
token: string;
|
|
223
|
+
expectedTokenHash: string;
|
|
224
|
+
expectedRevision: number;
|
|
225
|
+
port: number;
|
|
226
|
+
installedVersion: string;
|
|
227
|
+
registryUrl?: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
interface IUpgradeTransactionAdoptionAudit {
|
|
231
|
+
version: 1;
|
|
232
|
+
adoptedAt: number;
|
|
233
|
+
adoptedTargetVersion: string;
|
|
234
|
+
previousTransaction: TUpgradeTransaction;
|
|
235
|
+
}
|
|
236
|
+
|
|
185
237
|
export type TUpgradeControllerAction = 'prepare' | 'finalize';
|
|
186
238
|
|
|
187
239
|
interface IUpgradeControllerActionGrant {
|
|
@@ -443,6 +495,82 @@ const assertBoundedText = (valueArg: unknown, nameArg: string, maximumBytesArg:
|
|
|
443
495
|
return valueArg;
|
|
444
496
|
};
|
|
445
497
|
|
|
498
|
+
const parseUpgradeSemver = (valueArg: string): IUpgradeSemver => {
|
|
499
|
+
const match = upgradeSemverPattern.exec(valueArg);
|
|
500
|
+
if (!match) throw new Error(`Invalid semantic version: ${valueArg}`);
|
|
501
|
+
const prerelease = match[4]?.split('.') ?? [];
|
|
502
|
+
if (prerelease.some((identifierArg) => /^\d+$/.test(identifierArg)
|
|
503
|
+
&& identifierArg.length > 1
|
|
504
|
+
&& identifierArg.startsWith('0'))) {
|
|
505
|
+
throw new Error(`Invalid semantic version: ${valueArg}`);
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
major: match[1],
|
|
509
|
+
minor: match[2],
|
|
510
|
+
patch: match[3],
|
|
511
|
+
prerelease,
|
|
512
|
+
};
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const compareUpgradeNumericStrings = (leftArg: string, rightArg: string): number => {
|
|
516
|
+
if (leftArg.length !== rightArg.length) return leftArg.length < rightArg.length ? -1 : 1;
|
|
517
|
+
return leftArg === rightArg ? 0 : leftArg < rightArg ? -1 : 1;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
export const compareUpgradeSemver = (leftArg: string, rightArg: string): number => {
|
|
521
|
+
const left = parseUpgradeSemver(leftArg);
|
|
522
|
+
const right = parseUpgradeSemver(rightArg);
|
|
523
|
+
for (const key of ['major', 'minor', 'patch'] as const) {
|
|
524
|
+
const comparison = compareUpgradeNumericStrings(left[key], right[key]);
|
|
525
|
+
if (comparison !== 0) return comparison;
|
|
526
|
+
}
|
|
527
|
+
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
|
528
|
+
if (left.prerelease.length === right.prerelease.length) return 0;
|
|
529
|
+
return left.prerelease.length === 0 ? 1 : -1;
|
|
530
|
+
}
|
|
531
|
+
const identifierCount = Math.max(left.prerelease.length, right.prerelease.length);
|
|
532
|
+
for (let index = 0; index < identifierCount; index++) {
|
|
533
|
+
const leftIdentifier = left.prerelease[index];
|
|
534
|
+
const rightIdentifier = right.prerelease[index];
|
|
535
|
+
if (leftIdentifier === undefined || rightIdentifier === undefined) {
|
|
536
|
+
return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === undefined ? -1 : 1;
|
|
537
|
+
}
|
|
538
|
+
if (leftIdentifier === rightIdentifier) continue;
|
|
539
|
+
const leftNumeric = /^\d+$/.test(leftIdentifier);
|
|
540
|
+
const rightNumeric = /^\d+$/.test(rightIdentifier);
|
|
541
|
+
if (leftNumeric && rightNumeric) {
|
|
542
|
+
return compareUpgradeNumericStrings(leftIdentifier, rightIdentifier);
|
|
543
|
+
}
|
|
544
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
545
|
+
return leftIdentifier < rightIdentifier ? -1 : 1;
|
|
546
|
+
}
|
|
547
|
+
return 0;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
export const normalizeUpgradeRegistryUrl = (valueArg: unknown): string => {
|
|
551
|
+
const value = assertBoundedText(valueArg, 'The upgrade registry URL', 2_048);
|
|
552
|
+
let parsed: URL;
|
|
553
|
+
try {
|
|
554
|
+
parsed = new URL(value);
|
|
555
|
+
} catch (errorArg) {
|
|
556
|
+
throw new Error('The upgrade registry URL is invalid.', { cause: errorArg });
|
|
557
|
+
}
|
|
558
|
+
const loopbackHttp = parsed.protocol === 'http:' && new Set([
|
|
559
|
+
'localhost',
|
|
560
|
+
'127.0.0.1',
|
|
561
|
+
'[::1]',
|
|
562
|
+
]).has(parsed.hostname);
|
|
563
|
+
if (
|
|
564
|
+
(parsed.protocol !== 'https:' && !loopbackHttp)
|
|
565
|
+
|| parsed.hostname.length === 0
|
|
566
|
+
|| parsed.username.length > 0
|
|
567
|
+
|| parsed.password.length > 0
|
|
568
|
+
|| parsed.search.length > 0
|
|
569
|
+
|| parsed.hash.length > 0
|
|
570
|
+
) throw new Error('The upgrade registry URL is unsafe.');
|
|
571
|
+
return parsed.toString();
|
|
572
|
+
};
|
|
573
|
+
|
|
446
574
|
const assertRuntimeId = (valueArg: unknown): IControllerRuntimeId => {
|
|
447
575
|
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
448
576
|
throw new Error('The upgrade session identity is malformed.');
|
|
@@ -550,6 +678,7 @@ const parseUpgradeTransactionV2 = (valueArg: unknown): IUpgradeTransactionV2 =>
|
|
|
550
678
|
'continueSessions', 'gracePeriodMs', 'phase', 'message', 'createdAt', 'updatedAt',
|
|
551
679
|
'phaseStartedAt', 'sessions',
|
|
552
680
|
...(value.targetVersion === undefined ? [] : ['targetVersion']),
|
|
681
|
+
...(value.registryUrl === undefined ? [] : ['registryUrl']),
|
|
553
682
|
...(value.targetStartupInvoked === undefined ? [] : ['targetStartupInvoked']),
|
|
554
683
|
...(value.preparationAcceptedAt === undefined ? [] : ['preparationAcceptedAt']),
|
|
555
684
|
...(value.preparationDeadlineAt === undefined ? [] : ['preparationDeadlineAt']),
|
|
@@ -696,6 +825,9 @@ const parseUpgradeTransactionV2 = (valueArg: unknown): IUpgradeTransactionV2 =>
|
|
|
696
825
|
...(value.targetVersion === undefined
|
|
697
826
|
? {}
|
|
698
827
|
: { targetVersion: assertBoundedText(value.targetVersion, 'The upgrade target version', 128) }),
|
|
828
|
+
...(value.registryUrl === undefined
|
|
829
|
+
? {}
|
|
830
|
+
: { registryUrl: normalizeUpgradeRegistryUrl(value.registryUrl) }),
|
|
699
831
|
...(value.targetStartupInvoked === true ? { targetStartupInvoked: true } : {}),
|
|
700
832
|
controllerWasRunning: value.controllerWasRunning,
|
|
701
833
|
continueSessions: value.continueSessions,
|
|
@@ -737,6 +869,7 @@ const parseUpgradeTransactionV3 = (valueArg: unknown): IUpgradeTransactionV3 =>
|
|
|
737
869
|
'targetCliName', 'targetCliRelativePath',
|
|
738
870
|
'controllerWasRunning', 'continueSessions', 'gracePeriodMs', 'phase', 'message',
|
|
739
871
|
'createdAt', 'updatedAt', 'phaseStartedAt', 'sessions',
|
|
872
|
+
...(value.recoveryTargetVersion === undefined ? [] : ['recoveryTargetVersion']),
|
|
740
873
|
...(value.packageTransitionStarted === undefined ? [] : ['packageTransitionStarted']),
|
|
741
874
|
...(value.groupedTargetVerified === undefined ? [] : ['groupedTargetVerified']),
|
|
742
875
|
...(value.targetPackageCommitStarted === undefined ? [] : ['targetPackageCommitStarted']),
|
|
@@ -762,6 +895,14 @@ const parseUpgradeTransactionV3 = (valueArg: unknown): IUpgradeTransactionV3 =>
|
|
|
762
895
|
|| value.targetManagementVersion !== upgradePackageTransitionTarget.managementVersion
|
|
763
896
|
|| value.targetCliName !== upgradePackageTransitionTarget.cliName
|
|
764
897
|
|| value.targetCliRelativePath !== upgradePackageTransitionTarget.cliRelativePath
|
|
898
|
+
|| (value.recoveryTargetVersion !== undefined && (
|
|
899
|
+
typeof value.recoveryTargetVersion !== 'string'
|
|
900
|
+
|| compareUpgradeSemver(
|
|
901
|
+
value.recoveryTargetVersion,
|
|
902
|
+
upgradePackageTransitionTarget.version,
|
|
903
|
+
) <= 0
|
|
904
|
+
|| value.targetPackageCommitStarted !== true
|
|
905
|
+
))
|
|
765
906
|
|| (value.packageTransitionStarted !== undefined && value.packageTransitionStarted !== true)
|
|
766
907
|
|| (value.groupedTargetVerified !== undefined && value.groupedTargetVerified !== true)
|
|
767
908
|
|| (value.targetPackageCommitStarted !== undefined
|
|
@@ -829,6 +970,15 @@ const parseUpgradeTransactionV3 = (valueArg: unknown): IUpgradeTransactionV3 =>
|
|
|
829
970
|
targetManagementVersion: upgradePackageTransitionTarget.managementVersion,
|
|
830
971
|
targetCliName: upgradePackageTransitionTarget.cliName,
|
|
831
972
|
targetCliRelativePath: upgradePackageTransitionTarget.cliRelativePath,
|
|
973
|
+
...(value.recoveryTargetVersion === undefined
|
|
974
|
+
? {}
|
|
975
|
+
: {
|
|
976
|
+
recoveryTargetVersion: assertBoundedText(
|
|
977
|
+
value.recoveryTargetVersion,
|
|
978
|
+
'The package-transition recovery target version',
|
|
979
|
+
128,
|
|
980
|
+
),
|
|
981
|
+
}),
|
|
832
982
|
...(value.packageTransitionStarted === true ? { packageTransitionStarted: true } : {}),
|
|
833
983
|
...(value.groupedTargetVerified === true ? { groupedTargetVerified: true } : {}),
|
|
834
984
|
...(value.targetPackageCommitStarted === true ? { targetPackageCommitStarted: true } : {}),
|
|
@@ -846,6 +996,34 @@ const parseUpgradeTransaction = (valueArg: unknown): TUpgradeTransaction => {
|
|
|
846
996
|
: parseUpgradeTransactionV3(valueArg);
|
|
847
997
|
};
|
|
848
998
|
|
|
999
|
+
const parseUpgradeTransactionAdoptionAudit = (
|
|
1000
|
+
valueArg: unknown,
|
|
1001
|
+
): IUpgradeTransactionAdoptionAudit => {
|
|
1002
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
1003
|
+
throw new Error('Upgrade transaction adoption metadata is malformed.');
|
|
1004
|
+
}
|
|
1005
|
+
const value = valueArg as Record<string, unknown>;
|
|
1006
|
+
if (!exactKeys(value, [
|
|
1007
|
+
'version', 'adoptedAt', 'adoptedTargetVersion', 'previousTransaction',
|
|
1008
|
+
])) throw new Error('Upgrade transaction adoption metadata has unexpected fields.');
|
|
1009
|
+
const previousTransaction = parseUpgradeTransaction(value.previousTransaction);
|
|
1010
|
+
if (
|
|
1011
|
+
value.version !== 1
|
|
1012
|
+
|| !Number.isSafeInteger(value.adoptedAt)
|
|
1013
|
+
|| (value.adoptedAt as number) < previousTransaction.createdAt
|
|
1014
|
+
) throw new Error('Upgrade transaction adoption metadata is invalid.');
|
|
1015
|
+
return {
|
|
1016
|
+
version: 1,
|
|
1017
|
+
adoptedAt: value.adoptedAt as number,
|
|
1018
|
+
adoptedTargetVersion: assertBoundedText(
|
|
1019
|
+
value.adoptedTargetVersion,
|
|
1020
|
+
'The adopted upgrade target version',
|
|
1021
|
+
128,
|
|
1022
|
+
),
|
|
1023
|
+
previousTransaction,
|
|
1024
|
+
};
|
|
1025
|
+
};
|
|
1026
|
+
|
|
849
1027
|
const parseTransactionMutationOwner = (valueArg: unknown): IUpgradeTransactionMutationOwner => {
|
|
850
1028
|
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
851
1029
|
throw new Error('The upgrade transaction mutation owner is malformed.');
|
|
@@ -945,6 +1123,18 @@ export const upgradeTransactionIsStalled = (
|
|
|
945
1123
|
return nowArg - transactionArg.phaseStartedAt > maximumPhaseDurationMs[transactionArg.phase];
|
|
946
1124
|
};
|
|
947
1125
|
|
|
1126
|
+
export const upgradeTransactionRequiresForwardRecovery = (
|
|
1127
|
+
transactionArg: TUpgradeTransaction,
|
|
1128
|
+
): boolean => transactionArg.version === coordinationVersion
|
|
1129
|
+
? transactionArg.targetStartupInvoked === true
|
|
1130
|
+
: transactionArg.targetPackageCommitStarted === true;
|
|
1131
|
+
|
|
1132
|
+
export const upgradeTransactionTargetVersion = (
|
|
1133
|
+
transactionArg: TUpgradeTransaction,
|
|
1134
|
+
): string | undefined => transactionArg.version === upgradePackageTransitionTransactionVersion
|
|
1135
|
+
? transactionArg.recoveryTargetVersion ?? transactionArg.targetVersion
|
|
1136
|
+
: transactionArg.targetVersion;
|
|
1137
|
+
|
|
948
1138
|
const ownerIsLive = async (ownerArg: IUpgradeProcessOwner): Promise<boolean> => {
|
|
949
1139
|
if (ownerArg.uid !== assertUid()) return false;
|
|
950
1140
|
const identity = await readControllerProcessIdentity(ownerArg.pid);
|
|
@@ -1357,6 +1547,104 @@ class UpgradeOwnedDirectory {
|
|
|
1357
1547
|
export class UpgradeInstallationLock extends UpgradeOwnedDirectory {}
|
|
1358
1548
|
export class UpgradeStartLease extends UpgradeOwnedDirectory {}
|
|
1359
1549
|
|
|
1550
|
+
export interface IVerifiedUpgradeWorkerProcessGroup {
|
|
1551
|
+
pid: number;
|
|
1552
|
+
processGroupId: number;
|
|
1553
|
+
fingerprint: string;
|
|
1554
|
+
cliPath: string;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
export const terminateVerifiedUpgradeWorkerProcessGroup = async (
|
|
1558
|
+
worker: IVerifiedUpgradeWorkerProcessGroup,
|
|
1559
|
+
): Promise<void> => {
|
|
1560
|
+
const identity = await readControllerProcessIdentity(worker.pid);
|
|
1561
|
+
const existingMembers = worker.processGroupId === worker.pid
|
|
1562
|
+
? await readProcessGroupMemberPids(worker.processGroupId)
|
|
1563
|
+
: [];
|
|
1564
|
+
if (!identity || identity.fingerprint !== worker.fingerprint) {
|
|
1565
|
+
if (existingMembers.length === 0) return;
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
'The stalled upgrade worker leader is unavailable; its remaining group cannot be signaled safely.',
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
if (
|
|
1571
|
+
identity.processGroupId !== worker.processGroupId
|
|
1572
|
+
|| !identity.processGroupLeader
|
|
1573
|
+
|| !await processIdentityHasCliCommand(
|
|
1574
|
+
identity,
|
|
1575
|
+
worker.cliPath,
|
|
1576
|
+
'__upgrade-worker',
|
|
1577
|
+
{ allowMissingAbsolutePath: true },
|
|
1578
|
+
)
|
|
1579
|
+
) throw new Error('The stalled upgrade worker identity changed unexpectedly.');
|
|
1580
|
+
const readStableMemberSnapshot = async (): Promise<NonNullable<Awaited<
|
|
1581
|
+
ReturnType<typeof readControllerProcessIdentity>
|
|
1582
|
+
>>[]> => {
|
|
1583
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
1584
|
+
const processIdsBefore = await readProcessGroupMemberPids(worker.processGroupId);
|
|
1585
|
+
const identities = await Promise.all(
|
|
1586
|
+
processIdsBefore.map(async (processIdArg) => await readControllerProcessIdentity(processIdArg)),
|
|
1587
|
+
);
|
|
1588
|
+
const processIdsAfter = await readProcessGroupMemberPids(worker.processGroupId);
|
|
1589
|
+
if (
|
|
1590
|
+
processIdsBefore.length === processIdsAfter.length
|
|
1591
|
+
&& processIdsBefore.every((processIdArg, indexArg) => processIdArg === processIdsAfter[indexArg])
|
|
1592
|
+
&& identities.every((candidateArg) => (
|
|
1593
|
+
candidateArg !== null && candidateArg.processGroupId === worker.processGroupId
|
|
1594
|
+
))
|
|
1595
|
+
&& identities.some((candidateArg) => (
|
|
1596
|
+
candidateArg!.pid === worker.pid && candidateArg!.fingerprint === worker.fingerprint
|
|
1597
|
+
))
|
|
1598
|
+
) return identities as NonNullable<typeof identities[number]>[];
|
|
1599
|
+
}
|
|
1600
|
+
throw new Error('The stalled upgrade worker group could not be snapshotted safely.');
|
|
1601
|
+
};
|
|
1602
|
+
const members = await readStableMemberSnapshot();
|
|
1603
|
+
const signalOwnedMembers = async (signalArg: NodeJS.Signals): Promise<void> => {
|
|
1604
|
+
const orderedMembers = [...members].sort((leftArg, rightArg) => (
|
|
1605
|
+
leftArg.pid === worker.pid ? 1 : rightArg.pid === worker.pid ? -1 : 0
|
|
1606
|
+
));
|
|
1607
|
+
for (const member of orderedMembers) {
|
|
1608
|
+
const current = await readControllerProcessIdentity(member.pid);
|
|
1609
|
+
if (!current || current.fingerprint !== member.fingerprint) continue;
|
|
1610
|
+
if (current.processGroupId !== worker.processGroupId) {
|
|
1611
|
+
throw new Error('A captured upgrade worker process escaped its owned process group.');
|
|
1612
|
+
}
|
|
1613
|
+
try {
|
|
1614
|
+
process.kill(member.pid, signalArg);
|
|
1615
|
+
} catch (errorArg) {
|
|
1616
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
};
|
|
1620
|
+
const waitForDrain = async (): Promise<boolean> => {
|
|
1621
|
+
const deadline = Date.now() + workerTerminationGraceMs;
|
|
1622
|
+
while (Date.now() < deadline) {
|
|
1623
|
+
const [captured, group] = await Promise.all([
|
|
1624
|
+
Promise.all(members.map(async (memberArg) => {
|
|
1625
|
+
const current = await readControllerProcessIdentity(memberArg.pid);
|
|
1626
|
+
return current?.fingerprint === memberArg.fingerprint;
|
|
1627
|
+
})),
|
|
1628
|
+
readProcessGroupMemberPids(worker.processGroupId),
|
|
1629
|
+
]);
|
|
1630
|
+
if (!captured.some(Boolean) && group.length === 0) return true;
|
|
1631
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1632
|
+
}
|
|
1633
|
+
const [captured, group] = await Promise.all([
|
|
1634
|
+
Promise.all(members.map(async (memberArg) => {
|
|
1635
|
+
const current = await readControllerProcessIdentity(memberArg.pid);
|
|
1636
|
+
return current?.fingerprint === memberArg.fingerprint;
|
|
1637
|
+
})),
|
|
1638
|
+
readProcessGroupMemberPids(worker.processGroupId),
|
|
1639
|
+
]);
|
|
1640
|
+
return !captured.some(Boolean) && group.length === 0;
|
|
1641
|
+
};
|
|
1642
|
+
await signalOwnedMembers('SIGTERM');
|
|
1643
|
+
if (await waitForDrain()) return;
|
|
1644
|
+
await signalOwnedMembers('SIGKILL');
|
|
1645
|
+
if (!await waitForDrain()) throw new Error('The stalled upgrade worker group did not terminate.');
|
|
1646
|
+
};
|
|
1647
|
+
|
|
1360
1648
|
export class UpgradeCoordinator {
|
|
1361
1649
|
public readonly uid: number;
|
|
1362
1650
|
public readonly baseDirectory: string;
|
|
@@ -1424,6 +1712,14 @@ export class UpgradeCoordinator {
|
|
|
1424
1712
|
await this.initializationTask;
|
|
1425
1713
|
}
|
|
1426
1714
|
|
|
1715
|
+
public async initializeExistingCanonicalState(): Promise<void> {
|
|
1716
|
+
if (this.usesLegacyLocation || this.usesInheritedLocation) {
|
|
1717
|
+
throw new Error('Canonical upgrade initialization cannot use an inherited root.');
|
|
1718
|
+
}
|
|
1719
|
+
if (!await lstatIfPresent(this.baseDirectory)) return;
|
|
1720
|
+
await this.init();
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1427
1723
|
private async performInitialization(): Promise<void> {
|
|
1428
1724
|
if (!this.usesLegacyLocation && !this.usesInheritedLocation) {
|
|
1429
1725
|
const home = resolveAGLHomePaths();
|
|
@@ -1438,6 +1734,86 @@ export class UpgradeCoordinator {
|
|
|
1438
1734
|
await this.cleanupOwnershipGuardDirectories();
|
|
1439
1735
|
const entries = await plugins.fs.promises.readdir(this.baseDirectory, { withFileTypes: true });
|
|
1440
1736
|
for (const entry of entries) {
|
|
1737
|
+
if (entry.isFile() && transactionAdoptionAuditTemporaryPattern.test(entry.name)) {
|
|
1738
|
+
const filePath = plugins.path.join(this.baseDirectory, entry.name);
|
|
1739
|
+
const stats = await lstatIfPresent(filePath);
|
|
1740
|
+
if (!stats) continue;
|
|
1741
|
+
if (
|
|
1742
|
+
!stats.isFile()
|
|
1743
|
+
|| stats.isSymbolicLink()
|
|
1744
|
+
|| stats.uid !== this.uid
|
|
1745
|
+
|| (stats.mode & 0o077) !== 0
|
|
1746
|
+
) throw new Error(`Upgrade transaction adoption temporary is unsafe: ${filePath}`);
|
|
1747
|
+
if (Date.now() - stats.mtimeMs > initializationGraceMs) {
|
|
1748
|
+
await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
|
|
1749
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
continue;
|
|
1753
|
+
}
|
|
1754
|
+
if (entry.isFile() && transactionAdoptionAuditPattern.test(entry.name)) {
|
|
1755
|
+
const filePath = plugins.path.join(this.baseDirectory, entry.name);
|
|
1756
|
+
const stats = await lstatIfPresent(filePath);
|
|
1757
|
+
if (!stats) continue;
|
|
1758
|
+
if (
|
|
1759
|
+
!stats.isFile()
|
|
1760
|
+
|| stats.isSymbolicLink()
|
|
1761
|
+
|| stats.uid !== this.uid
|
|
1762
|
+
|| (stats.mode & 0o077) !== 0
|
|
1763
|
+
) throw new Error(`Upgrade transaction adoption metadata is unsafe: ${filePath}`);
|
|
1764
|
+
parseUpgradeTransactionAdoptionAudit(await readPrivateJson(filePath));
|
|
1765
|
+
if (Date.now() - stats.mtimeMs > transactionRetentionMs) {
|
|
1766
|
+
const lockState = await this.inspectOwnedDirectory(this.upgradeLockDirectory);
|
|
1767
|
+
if (lockState.state === 'absent' || lockState.state === 'stale') {
|
|
1768
|
+
await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
|
|
1769
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
if (entry.isFile() && acknowledgementTemporaryPattern.test(entry.name)) {
|
|
1776
|
+
const filePath = plugins.path.join(this.baseDirectory, entry.name);
|
|
1777
|
+
const stats = await lstatIfPresent(filePath);
|
|
1778
|
+
if (!stats) continue;
|
|
1779
|
+
if (
|
|
1780
|
+
!stats.isFile()
|
|
1781
|
+
|| stats.isSymbolicLink()
|
|
1782
|
+
|| stats.uid !== this.uid
|
|
1783
|
+
|| (stats.mode & 0o077) !== 0
|
|
1784
|
+
) throw new Error(`Upgrade acknowledgement temporary is unsafe: ${filePath}`);
|
|
1785
|
+
if (Date.now() - stats.mtimeMs > initializationGraceMs) {
|
|
1786
|
+
await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
|
|
1787
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
if (entry.isFile() && ownerTransferTemporaryPattern.test(entry.name)) {
|
|
1793
|
+
const filePath = plugins.path.join(this.baseDirectory, entry.name);
|
|
1794
|
+
const stats = await lstatIfPresent(filePath);
|
|
1795
|
+
if (!stats) continue;
|
|
1796
|
+
if (
|
|
1797
|
+
!stats.isFile()
|
|
1798
|
+
|| stats.isSymbolicLink()
|
|
1799
|
+
|| stats.uid !== this.uid
|
|
1800
|
+
|| (stats.mode & 0o077) !== 0
|
|
1801
|
+
|| stats.nlink !== 1
|
|
1802
|
+
) throw new Error(`Upgrade owner-transfer temporary is unsafe: ${filePath}`);
|
|
1803
|
+
if (Date.now() - stats.mtimeMs > initializationGraceMs) {
|
|
1804
|
+
await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
|
|
1805
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
1806
|
+
});
|
|
1807
|
+
await syncDirectory(this.baseDirectory);
|
|
1808
|
+
}
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (entry.name.startsWith('owner-transfer-')) {
|
|
1812
|
+
throw new Error(`Upgrade owner-transfer temporary has an invalid name: ${entry.name}`);
|
|
1813
|
+
}
|
|
1814
|
+
if (entry.name.startsWith('adoption-')) {
|
|
1815
|
+
throw new Error(`Upgrade transaction adoption metadata has an invalid name: ${entry.name}`);
|
|
1816
|
+
}
|
|
1441
1817
|
if (!entry.isFile() || !tokenMetadataPattern.test(entry.name)) continue;
|
|
1442
1818
|
const filePath = plugins.path.join(this.baseDirectory, entry.name);
|
|
1443
1819
|
const stats = await lstatIfPresent(filePath);
|
|
@@ -1460,6 +1836,18 @@ export class UpgradeCoordinator {
|
|
|
1460
1836
|
withFileTypes: true,
|
|
1461
1837
|
})) {
|
|
1462
1838
|
const filePath = plugins.path.join(this.transactionsDirectory, entry.name);
|
|
1839
|
+
if (entry.isFile() && transactionAdoptionPattern.test(entry.name)) {
|
|
1840
|
+
const stats = await lstatIfPresent(filePath);
|
|
1841
|
+
if (!stats) continue;
|
|
1842
|
+
if (
|
|
1843
|
+
!stats.isFile()
|
|
1844
|
+
|| stats.isSymbolicLink()
|
|
1845
|
+
|| stats.uid !== this.uid
|
|
1846
|
+
|| (stats.mode & 0o077) !== 0
|
|
1847
|
+
) throw new Error(`Upgrade transaction adoption state is unsafe: ${filePath}`);
|
|
1848
|
+
parseUpgradeTransaction(await readPrivateJson(filePath));
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1463
1851
|
if (entry.isFile() && transactionTemporaryMetadataPattern.test(entry.name)) {
|
|
1464
1852
|
const stats = await lstatIfPresent(filePath);
|
|
1465
1853
|
if (!stats) continue;
|
|
@@ -1490,28 +1878,145 @@ export class UpgradeCoordinator {
|
|
|
1490
1878
|
}
|
|
1491
1879
|
continue;
|
|
1492
1880
|
}
|
|
1493
|
-
if (
|
|
1494
|
-
|
|
1495
|
-
|
|
1881
|
+
if (entry.isFile() && transactionMetadataPattern.test(entry.name)) {
|
|
1882
|
+
const stats = await lstatIfPresent(filePath);
|
|
1883
|
+
if (!stats) continue;
|
|
1884
|
+
if (
|
|
1885
|
+
!stats.isFile()
|
|
1886
|
+
|| stats.isSymbolicLink()
|
|
1887
|
+
|| stats.uid !== this.uid
|
|
1888
|
+
|| (stats.mode & 0o077) !== 0
|
|
1889
|
+
) throw new Error(`Upgrade transaction metadata is unsafe: ${filePath}`);
|
|
1890
|
+
if (Date.now() - stats.mtimeMs > transactionRetentionMs) {
|
|
1891
|
+
const transaction = parseUpgradeTransaction(await readPrivateJson(filePath));
|
|
1892
|
+
const workerIsLive = await this.transactionWorkerIsLive(transaction).catch(() => true);
|
|
1893
|
+
const controllerIsLive = transaction.controller
|
|
1894
|
+
? await this.transactionControllerIsLive(transaction).catch(() => true)
|
|
1895
|
+
: false;
|
|
1896
|
+
if (
|
|
1897
|
+
transaction.terminal
|
|
1898
|
+
|| (
|
|
1899
|
+
!upgradeTransactionRequiresForwardRecovery(transaction)
|
|
1900
|
+
&& !workerIsLive
|
|
1901
|
+
&& !controllerIsLive
|
|
1902
|
+
)
|
|
1903
|
+
) {
|
|
1904
|
+
await plugins.fs.promises.unlink(filePath).catch((errorArg) => {
|
|
1905
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
if (entry.name.startsWith('transaction-')) {
|
|
1912
|
+
throw new Error(`Upgrade transaction metadata has an invalid name: ${entry.name}`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
public async inspectCanonicalTransactionInventory(): Promise<IUpgradeTransactionInventory> {
|
|
1918
|
+
if (this.usesLegacyLocation || this.usesInheritedLocation) {
|
|
1919
|
+
throw new Error('Canonical upgrade transaction inventory cannot use an inherited root.');
|
|
1920
|
+
}
|
|
1921
|
+
const empty = (): IUpgradeTransactionInventory => ({
|
|
1922
|
+
transactions: [],
|
|
1923
|
+
tokenMetadataFileNames: [],
|
|
1924
|
+
mutationDirectoryNames: [],
|
|
1925
|
+
});
|
|
1926
|
+
const baseStats = await lstatIfPresent(this.baseDirectory);
|
|
1927
|
+
if (!baseStats) return empty();
|
|
1928
|
+
await assertPrivateDirectory(this.baseDirectory);
|
|
1929
|
+
|
|
1930
|
+
const tokenMetadataFileNames: string[] = [];
|
|
1931
|
+
for (const entry of await plugins.fs.promises.readdir(this.baseDirectory, {
|
|
1932
|
+
withFileTypes: true,
|
|
1933
|
+
})) {
|
|
1934
|
+
if (entry.isFile() && tokenMetadataPattern.test(entry.name)) {
|
|
1935
|
+
const filePath = plugins.path.join(this.baseDirectory, entry.name);
|
|
1936
|
+
await readPrivateJson(filePath);
|
|
1937
|
+
tokenMetadataFileNames.push(entry.name);
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
if (entry.isFile() && acknowledgementTemporaryPattern.test(entry.name)) {
|
|
1941
|
+
await readPrivateJson(plugins.path.join(this.baseDirectory, entry.name));
|
|
1942
|
+
tokenMetadataFileNames.push(entry.name);
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (entry.isFile() && transactionAdoptionAuditPattern.test(entry.name)) {
|
|
1946
|
+
parseUpgradeTransactionAdoptionAudit(
|
|
1947
|
+
await readPrivateJson(plugins.path.join(this.baseDirectory, entry.name)),
|
|
1948
|
+
);
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1496
1951
|
if (
|
|
1497
|
-
|
|
1498
|
-
||
|
|
1499
|
-
||
|
|
1500
|
-
|| (
|
|
1501
|
-
) throw new Error(`Upgrade
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1952
|
+
entry.name.startsWith('grant-')
|
|
1953
|
+
|| entry.name.startsWith('ack-')
|
|
1954
|
+
|| entry.name.startsWith('action-')
|
|
1955
|
+
|| entry.name.startsWith('adoption-')
|
|
1956
|
+
) throw new Error(`Upgrade coordination metadata has an invalid name: ${entry.name}`);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const transactionsStats = await lstatIfPresent(this.transactionsDirectory);
|
|
1960
|
+
if (!transactionsStats) {
|
|
1961
|
+
return { transactions: [], tokenMetadataFileNames, mutationDirectoryNames: [] };
|
|
1962
|
+
}
|
|
1963
|
+
await assertPrivateDirectory(this.transactionsDirectory);
|
|
1964
|
+
const transactions: IUpgradeTransactionInventoryEntry[] = [];
|
|
1965
|
+
const mutationDirectoryNames: string[] = [];
|
|
1966
|
+
for (const entry of await plugins.fs.promises.readdir(this.transactionsDirectory, {
|
|
1967
|
+
withFileTypes: true,
|
|
1968
|
+
})) {
|
|
1969
|
+
if (entry.isDirectory() && transactionMutationDirectoryPattern.test(entry.name)) {
|
|
1970
|
+
mutationDirectoryNames.push(entry.name);
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
if (entry.isFile() && transactionTemporaryMetadataPattern.test(entry.name)) continue;
|
|
1974
|
+
const active = entry.isFile() && transactionMetadataPattern.test(entry.name);
|
|
1975
|
+
const adoptionMatch = entry.isFile() ? transactionAdoptionPattern.exec(entry.name) : null;
|
|
1976
|
+
if (!active && !adoptionMatch) {
|
|
1977
|
+
if (entry.name.startsWith('transaction-')) {
|
|
1978
|
+
throw new Error(`Upgrade transaction metadata has an invalid name: ${entry.name}`);
|
|
1512
1979
|
}
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
const filePath = plugins.path.join(this.transactionsDirectory, entry.name);
|
|
1983
|
+
const before = await lstatIfPresent(filePath);
|
|
1984
|
+
if (!before) continue;
|
|
1985
|
+
if (
|
|
1986
|
+
!before.isFile()
|
|
1987
|
+
|| before.isSymbolicLink()
|
|
1988
|
+
|| before.uid !== this.uid
|
|
1989
|
+
|| (before.mode & 0o077) !== 0
|
|
1990
|
+
) throw new Error(`Upgrade transaction metadata is unsafe: ${filePath}`);
|
|
1991
|
+
const transaction = parseUpgradeTransaction(await readPrivateJson(filePath));
|
|
1992
|
+
const after = await lstatIfPresent(filePath);
|
|
1993
|
+
if (
|
|
1994
|
+
!after
|
|
1995
|
+
|| !sameOwnedDirectoryIdentity(after, { dev: before.dev, ino: before.ino })
|
|
1996
|
+
|| after.size !== before.size
|
|
1997
|
+
|| after.mtimeMs !== before.mtimeMs
|
|
1998
|
+
) throw new Error('Upgrade transaction metadata changed during inventory.');
|
|
1999
|
+
const tokenPrefix = transaction.tokenHash.slice(0, 20);
|
|
2000
|
+
if (active && entry.name !== `transaction-${tokenPrefix}.json`) {
|
|
2001
|
+
throw new Error('Upgrade transaction filename and token binding do not match.');
|
|
2002
|
+
}
|
|
2003
|
+
if (adoptionMatch && tokenPrefix !== adoptionMatch[1] && tokenPrefix !== adoptionMatch[2]) {
|
|
2004
|
+
throw new Error('Upgrade transaction adoption state has an invalid token binding.');
|
|
1513
2005
|
}
|
|
2006
|
+
transactions.push({
|
|
2007
|
+
fileName: entry.name,
|
|
2008
|
+
state: active ? 'active' : 'adopting',
|
|
2009
|
+
transaction,
|
|
2010
|
+
});
|
|
1514
2011
|
}
|
|
2012
|
+
return {
|
|
2013
|
+
transactions: transactions.sort((leftArg, rightArg) => (
|
|
2014
|
+
leftArg.transaction.createdAt - rightArg.transaction.createdAt
|
|
2015
|
+
|| leftArg.fileName.localeCompare(rightArg.fileName)
|
|
2016
|
+
)),
|
|
2017
|
+
tokenMetadataFileNames: tokenMetadataFileNames.sort(),
|
|
2018
|
+
mutationDirectoryNames: mutationDirectoryNames.sort(),
|
|
2019
|
+
};
|
|
1515
2020
|
}
|
|
1516
2021
|
|
|
1517
2022
|
private ownershipGuardDirectory(directoryArg: string): string {
|
|
@@ -2207,14 +2712,24 @@ export class UpgradeCoordinator {
|
|
|
2207
2712
|
await this.withOwnershipMutationGuard(this.upgradeLockDirectory, async () => {
|
|
2208
2713
|
await optionsArg.lock.assertOwned();
|
|
2209
2714
|
const ownerPath = plugins.path.join(this.upgradeLockDirectory, ownerFileName);
|
|
2210
|
-
const temporaryPath =
|
|
2715
|
+
const temporaryPath = plugins.path.join(
|
|
2716
|
+
this.baseDirectory,
|
|
2717
|
+
`owner-transfer-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}.json.tmp`,
|
|
2718
|
+
);
|
|
2211
2719
|
try {
|
|
2212
2720
|
await writePrivateJson(temporaryPath, owner);
|
|
2213
2721
|
await plugins.fs.promises.rename(temporaryPath, ownerPath);
|
|
2214
2722
|
await syncDirectory(this.upgradeLockDirectory);
|
|
2723
|
+
await syncDirectory(this.baseDirectory);
|
|
2215
2724
|
optionsArg.lock.relinquishAfterTransfer();
|
|
2216
2725
|
} finally {
|
|
2217
|
-
|
|
2726
|
+
let removed = false;
|
|
2727
|
+
await plugins.fs.promises.unlink(temporaryPath).then(() => {
|
|
2728
|
+
removed = true;
|
|
2729
|
+
}).catch((errorArg) => {
|
|
2730
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
2731
|
+
});
|
|
2732
|
+
if (removed) await syncDirectory(this.baseDirectory);
|
|
2218
2733
|
}
|
|
2219
2734
|
});
|
|
2220
2735
|
}
|
|
@@ -2456,10 +2971,263 @@ export class UpgradeCoordinator {
|
|
|
2456
2971
|
return result as T;
|
|
2457
2972
|
}
|
|
2458
2973
|
|
|
2974
|
+
private async cleanupUncommittedAdoptionAuditsUnderLock(
|
|
2975
|
+
lockArg: UpgradeInstallationLock,
|
|
2976
|
+
): Promise<void> {
|
|
2977
|
+
await lockArg.assertOwned();
|
|
2978
|
+
for (const entry of await plugins.fs.promises.readdir(this.baseDirectory, {
|
|
2979
|
+
withFileTypes: true,
|
|
2980
|
+
})) {
|
|
2981
|
+
const match = entry.isFile() ? transactionAdoptionAuditPattern.exec(entry.name) : null;
|
|
2982
|
+
if (!match) continue;
|
|
2983
|
+
const auditPath = plugins.path.join(this.baseDirectory, entry.name);
|
|
2984
|
+
const audit = parseUpgradeTransactionAdoptionAudit(await readPrivateJson(auditPath));
|
|
2985
|
+
const sourcePath = plugins.path.join(
|
|
2986
|
+
this.transactionsDirectory,
|
|
2987
|
+
`transaction-${match[1]}.json`,
|
|
2988
|
+
);
|
|
2989
|
+
const intermediatePath = `${sourcePath}.adopting-${match[2]}`;
|
|
2990
|
+
const targetPath = plugins.path.join(
|
|
2991
|
+
this.transactionsDirectory,
|
|
2992
|
+
`transaction-${match[2]}.json`,
|
|
2993
|
+
);
|
|
2994
|
+
await this.withOwnershipMutationGuard(sourcePath, async () => {
|
|
2995
|
+
await lockArg.assertOwned();
|
|
2996
|
+
const [sourceStats, intermediateStats, targetStats] = await Promise.all([
|
|
2997
|
+
lstatIfPresent(sourcePath),
|
|
2998
|
+
lstatIfPresent(intermediatePath),
|
|
2999
|
+
lstatIfPresent(targetPath),
|
|
3000
|
+
]);
|
|
3001
|
+
if (!sourceStats || intermediateStats || targetStats) return;
|
|
3002
|
+
const source = parseUpgradeTransaction(await readPrivateJson(sourcePath));
|
|
3003
|
+
if (!plugins.util.isDeepStrictEqual(source, audit.previousTransaction)) return;
|
|
3004
|
+
await plugins.fs.promises.unlink(auditPath);
|
|
3005
|
+
await syncDirectory(this.baseDirectory);
|
|
3006
|
+
});
|
|
3007
|
+
}
|
|
3008
|
+
await lockArg.assertOwned();
|
|
3009
|
+
}
|
|
3010
|
+
|
|
3011
|
+
public async adoptOrphanedTransaction(
|
|
3012
|
+
optionsArg: IAdoptOrphanedUpgradeTransactionOptions,
|
|
3013
|
+
): Promise<TUpgradeTransaction> {
|
|
3014
|
+
await optionsArg.lock.assertOwned();
|
|
3015
|
+
await this.waitForStartLeasesToDrain(optionsArg.lock);
|
|
3016
|
+
await this.cleanupUncommittedAdoptionAuditsUnderLock(optionsArg.lock);
|
|
3017
|
+
const inventory = await this.inspectCanonicalTransactionInventory();
|
|
3018
|
+
const nonterminal = inventory.transactions.filter((entryArg) => !entryArg.transaction.terminal);
|
|
3019
|
+
if (nonterminal.length !== 1) {
|
|
3020
|
+
throw new Error('Exactly one orphaned upgrade transaction is required for adoption.');
|
|
3021
|
+
}
|
|
3022
|
+
const entry = nonterminal[0];
|
|
3023
|
+
const transaction = entry.transaction;
|
|
3024
|
+
const eligibleV2 = transaction.version === coordinationVersion
|
|
3025
|
+
&& transaction.controllerWasRunning === true
|
|
3026
|
+
&& transaction.targetStartupInvoked === true
|
|
3027
|
+
&& transaction.targetVersion !== undefined
|
|
3028
|
+
&& transaction.preparationCompletedAt !== undefined
|
|
3029
|
+
&& (transaction.phase === 'restarting' || transaction.phase === 'continuing');
|
|
3030
|
+
const eligibleV3 = transaction.version === upgradePackageTransitionTransactionVersion
|
|
3031
|
+
&& transaction.targetPackageCommitStarted === true
|
|
3032
|
+
&& (!transaction.controllerWasRunning || transaction.preparationCompletedAt !== undefined)
|
|
3033
|
+
&& (
|
|
3034
|
+
transaction.phase === 'installing'
|
|
3035
|
+
|| transaction.phase === 'restarting'
|
|
3036
|
+
|| transaction.phase === 'continuing'
|
|
3037
|
+
);
|
|
3038
|
+
if (
|
|
3039
|
+
transaction.port !== optionsArg.port
|
|
3040
|
+
|| transaction.tokenHash !== optionsArg.expectedTokenHash
|
|
3041
|
+
|| transaction.revision !== optionsArg.expectedRevision
|
|
3042
|
+
|| (!eligibleV2 && !eligibleV3)
|
|
3043
|
+
|| !upgradeTransactionIsStalled(transaction)
|
|
3044
|
+
) throw new Error('The retained upgrade transaction is not eligible for adoption.');
|
|
3045
|
+
if (transaction.version === upgradePackageTransitionTransactionVersion && optionsArg.registryUrl) {
|
|
3046
|
+
throw new Error(
|
|
3047
|
+
'Legacy package-transition recovery uses pnpm registry configuration and cannot retain --registry.',
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
3050
|
+
if (await this.transactionWorkerIsLive(transaction)) {
|
|
3051
|
+
throw new Error('The retained upgrade worker is still running.');
|
|
3052
|
+
}
|
|
3053
|
+
if (transaction.controller && await this.transactionControllerIsLive(transaction)) {
|
|
3054
|
+
throw new Error('The retained upgrade controller is still running.');
|
|
3055
|
+
}
|
|
3056
|
+
|
|
3057
|
+
const adoptionMatch = transactionAdoptionPattern.exec(entry.fileName);
|
|
3058
|
+
const tokenPrefixes = new Set([
|
|
3059
|
+
transaction.tokenHash.slice(0, 20),
|
|
3060
|
+
...(adoptionMatch ? [adoptionMatch[1], adoptionMatch[2]] : []),
|
|
3061
|
+
]);
|
|
3062
|
+
for (const fileName of inventory.tokenMetadataFileNames) {
|
|
3063
|
+
if ([...tokenPrefixes].some((prefixArg) => fileName.includes(`-${prefixArg}.json`))) {
|
|
3064
|
+
throw new Error('The orphaned upgrade retains token-bound coordination metadata.');
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
for (const directoryName of inventory.mutationDirectoryNames) {
|
|
3068
|
+
const prefix = [...tokenPrefixes].find((candidateArg) => (
|
|
3069
|
+
directoryName === `transaction-${candidateArg}.json.lock`
|
|
3070
|
+
));
|
|
3071
|
+
if (!prefix) continue;
|
|
3072
|
+
const directory = plugins.path.join(this.transactionsDirectory, directoryName);
|
|
3073
|
+
const state = await this.inspectTransactionMutationDirectory(
|
|
3074
|
+
directory,
|
|
3075
|
+
transaction.tokenHash,
|
|
3076
|
+
);
|
|
3077
|
+
if (state.state === 'stale') {
|
|
3078
|
+
await this.reclaimStaleTransactionMutationDirectory(directory, transaction.tokenHash);
|
|
3079
|
+
continue;
|
|
3080
|
+
}
|
|
3081
|
+
if (state.state !== 'absent') {
|
|
3082
|
+
throw new Error('The orphaned upgrade transaction still has mutation ownership.');
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
const installedVersion = assertBoundedText(
|
|
3087
|
+
optionsArg.installedVersion,
|
|
3088
|
+
'The installed upgrade version',
|
|
3089
|
+
128,
|
|
3090
|
+
);
|
|
3091
|
+
compareUpgradeSemver(installedVersion, installedVersion);
|
|
3092
|
+
if (
|
|
3093
|
+
transaction.version === upgradePackageTransitionTransactionVersion
|
|
3094
|
+
&& compareUpgradeSemver(
|
|
3095
|
+
installedVersion,
|
|
3096
|
+
upgradeTransactionTargetVersion(transaction)!,
|
|
3097
|
+
) < 0
|
|
3098
|
+
) throw new Error('The installed package-transition recovery target would downgrade AGL.');
|
|
3099
|
+
const registryUrl = transaction.version === coordinationVersion
|
|
3100
|
+
? transaction.registryUrl ?? optionsArg.registryUrl
|
|
3101
|
+
: undefined;
|
|
3102
|
+
if (
|
|
3103
|
+
transaction.version === coordinationVersion
|
|
3104
|
+
&& transaction.registryUrl !== undefined
|
|
3105
|
+
&& optionsArg.registryUrl !== undefined
|
|
3106
|
+
&& transaction.registryUrl !== optionsArg.registryUrl
|
|
3107
|
+
) throw new Error('The orphaned upgrade registry cannot change during adoption.');
|
|
3108
|
+
const newTokenHash = hashToken(optionsArg.token);
|
|
3109
|
+
const sourcePath = plugins.path.join(this.transactionsDirectory, entry.fileName);
|
|
3110
|
+
const sourcePrefix = transaction.tokenHash.slice(0, 20);
|
|
3111
|
+
const targetPrefix = newTokenHash.slice(0, 20);
|
|
3112
|
+
if (sourcePrefix === targetPrefix) {
|
|
3113
|
+
throw new Error('The adopted upgrade token collided with the retained token.');
|
|
3114
|
+
}
|
|
3115
|
+
const intermediatePath = plugins.path.join(
|
|
3116
|
+
this.transactionsDirectory,
|
|
3117
|
+
`transaction-${sourcePrefix}.json.adopting-${targetPrefix}`,
|
|
3118
|
+
);
|
|
3119
|
+
const targetPath = plugins.path.join(
|
|
3120
|
+
this.transactionsDirectory,
|
|
3121
|
+
`transaction-${targetPrefix}.json`,
|
|
3122
|
+
);
|
|
3123
|
+
const auditPath = plugins.path.join(
|
|
3124
|
+
this.baseDirectory,
|
|
3125
|
+
`adoption-${sourcePrefix}-${targetPrefix}.json`,
|
|
3126
|
+
);
|
|
3127
|
+
const auditTemporaryPath = `${auditPath}.tmp-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
|
|
3128
|
+
for (const path of [intermediatePath, targetPath, auditPath, auditTemporaryPath]) {
|
|
3129
|
+
if (await lstatIfPresent(path)) {
|
|
3130
|
+
throw new Error(`Upgrade transaction adoption destination already exists: ${path}`);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
const now = Date.now();
|
|
3134
|
+
const {
|
|
3135
|
+
worker: _worker,
|
|
3136
|
+
controller: _controller,
|
|
3137
|
+
retainedUntil: _retainedUntil,
|
|
3138
|
+
...retained
|
|
3139
|
+
} = transaction;
|
|
3140
|
+
const adopted = transaction.version === coordinationVersion
|
|
3141
|
+
? parseUpgradeTransactionV2({
|
|
3142
|
+
...retained,
|
|
3143
|
+
tokenHash: newTokenHash,
|
|
3144
|
+
revision: transaction.revision + 1,
|
|
3145
|
+
targetVersion: installedVersion,
|
|
3146
|
+
...(registryUrl === undefined ? {} : { registryUrl }),
|
|
3147
|
+
phaseStartedAt: now,
|
|
3148
|
+
updatedAt: now,
|
|
3149
|
+
message: `Recovering orphaned upgrade ${transaction.sourceVersion} -> ${transaction.targetVersion} with installed ${installedVersion}.`,
|
|
3150
|
+
})
|
|
3151
|
+
: parseUpgradeTransactionV3({
|
|
3152
|
+
...retained,
|
|
3153
|
+
tokenHash: newTokenHash,
|
|
3154
|
+
revision: transaction.revision + 1,
|
|
3155
|
+
...(installedVersion === transaction.targetVersion
|
|
3156
|
+
? {}
|
|
3157
|
+
: { recoveryTargetVersion: installedVersion }),
|
|
3158
|
+
phaseStartedAt: now,
|
|
3159
|
+
updatedAt: now,
|
|
3160
|
+
message: `Recovering orphaned package transition to installed ${transaction.targetPackageName}@${installedVersion}.`,
|
|
3161
|
+
});
|
|
3162
|
+
const audit = parseUpgradeTransactionAdoptionAudit({
|
|
3163
|
+
version: 1,
|
|
3164
|
+
adoptedAt: now,
|
|
3165
|
+
adoptedTargetVersion: installedVersion,
|
|
3166
|
+
previousTransaction: transaction,
|
|
3167
|
+
});
|
|
3168
|
+
const temporaryPath = `${targetPath}.tmp-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
|
|
3169
|
+
|
|
3170
|
+
await optionsArg.lock.assertOwned();
|
|
3171
|
+
try {
|
|
3172
|
+
await this.withOwnershipMutationGuard(sourcePath, async () => {
|
|
3173
|
+
await this.withOwnershipMutationGuard(intermediatePath, async () => {
|
|
3174
|
+
await this.withOwnershipMutationGuard(targetPath, async () => {
|
|
3175
|
+
const sourceStats = await lstatIfPresent(sourcePath);
|
|
3176
|
+
if (!sourceStats) throw new Error('The orphaned upgrade transaction disappeared.');
|
|
3177
|
+
const current = parseUpgradeTransaction(await readPrivateJson(sourcePath));
|
|
3178
|
+
if (
|
|
3179
|
+
current.tokenHash !== transaction.tokenHash
|
|
3180
|
+
|| current.revision !== transaction.revision
|
|
3181
|
+
) throw new Error('The orphaned upgrade transaction changed before adoption.');
|
|
3182
|
+
try {
|
|
3183
|
+
await writePrivateJson(auditTemporaryPath, audit);
|
|
3184
|
+
await plugins.fs.promises.rename(auditTemporaryPath, auditPath);
|
|
3185
|
+
await syncDirectory(this.baseDirectory);
|
|
3186
|
+
await plugins.fs.promises.rename(sourcePath, intermediatePath);
|
|
3187
|
+
await syncDirectory(this.transactionsDirectory);
|
|
3188
|
+
await writePrivateJson(temporaryPath, adopted);
|
|
3189
|
+
const intermediateStats = await lstatIfPresent(intermediatePath);
|
|
3190
|
+
if (
|
|
3191
|
+
!intermediateStats
|
|
3192
|
+
|| !sameOwnedDirectoryIdentity(intermediateStats, {
|
|
3193
|
+
dev: sourceStats.dev,
|
|
3194
|
+
ino: sourceStats.ino,
|
|
3195
|
+
})
|
|
3196
|
+
) throw new Error('The upgrade adoption source identity changed unexpectedly.');
|
|
3197
|
+
await plugins.fs.promises.rename(temporaryPath, intermediatePath);
|
|
3198
|
+
await syncDirectory(this.transactionsDirectory);
|
|
3199
|
+
await plugins.fs.promises.rename(intermediatePath, targetPath);
|
|
3200
|
+
await syncDirectory(this.transactionsDirectory);
|
|
3201
|
+
} finally {
|
|
3202
|
+
await plugins.fs.promises.unlink(auditTemporaryPath).catch(() => undefined);
|
|
3203
|
+
await plugins.fs.promises.unlink(temporaryPath).catch(() => undefined);
|
|
3204
|
+
}
|
|
3205
|
+
});
|
|
3206
|
+
});
|
|
3207
|
+
});
|
|
3208
|
+
} catch (errorArg) {
|
|
3209
|
+
const [sourceStats, intermediateStats, targetStats] = await Promise.all([
|
|
3210
|
+
lstatIfPresent(sourcePath),
|
|
3211
|
+
lstatIfPresent(intermediatePath),
|
|
3212
|
+
lstatIfPresent(targetPath),
|
|
3213
|
+
]);
|
|
3214
|
+
if (sourceStats && !intermediateStats && !targetStats) {
|
|
3215
|
+
await plugins.fs.promises.unlink(auditPath).catch((cleanupErrorArg) => {
|
|
3216
|
+
if ((cleanupErrorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw cleanupErrorArg;
|
|
3217
|
+
});
|
|
3218
|
+
await syncDirectory(this.baseDirectory);
|
|
3219
|
+
}
|
|
3220
|
+
throw errorArg;
|
|
3221
|
+
}
|
|
3222
|
+
await optionsArg.lock.assertOwned();
|
|
3223
|
+
return parseUpgradeTransaction(await readPrivateJson(targetPath));
|
|
3224
|
+
}
|
|
3225
|
+
|
|
2459
3226
|
public async createTransaction(optionsArg: {
|
|
2460
3227
|
token: string;
|
|
2461
3228
|
port: number;
|
|
2462
3229
|
sourceVersion: string;
|
|
3230
|
+
registryUrl?: string;
|
|
2463
3231
|
controllerWasRunning: boolean;
|
|
2464
3232
|
continueSessions: boolean;
|
|
2465
3233
|
gracePeriodMs: number;
|
|
@@ -2472,6 +3240,9 @@ export class UpgradeCoordinator {
|
|
|
2472
3240
|
revision: 0,
|
|
2473
3241
|
port: assertPort(optionsArg.port),
|
|
2474
3242
|
sourceVersion: assertBoundedText(optionsArg.sourceVersion, 'The upgrade source version', 128),
|
|
3243
|
+
...(optionsArg.registryUrl === undefined
|
|
3244
|
+
? {}
|
|
3245
|
+
: { registryUrl: normalizeUpgradeRegistryUrl(optionsArg.registryUrl) }),
|
|
2475
3246
|
controllerWasRunning: optionsArg.controllerWasRunning,
|
|
2476
3247
|
continueSessions: optionsArg.continueSessions,
|
|
2477
3248
|
gracePeriodMs: optionsArg.gracePeriodMs,
|
|
@@ -2602,7 +3373,15 @@ export class UpgradeCoordinator {
|
|
|
2602
3373
|
throw new Error(`The upgrade package-transition checkpoint ${checkpoint} cannot regress.`);
|
|
2603
3374
|
}
|
|
2604
3375
|
}
|
|
3376
|
+
if (currentArg.recoveryTargetVersion !== next.recoveryTargetVersion) {
|
|
3377
|
+
throw new Error('The package-transition recovery target cannot change after adoption.');
|
|
3378
|
+
}
|
|
2605
3379
|
}
|
|
3380
|
+
if (
|
|
3381
|
+
currentArg.version === coordinationVersion
|
|
3382
|
+
&& next.version === coordinationVersion
|
|
3383
|
+
&& currentArg.registryUrl !== next.registryUrl
|
|
3384
|
+
) throw new Error('The upgrade registry cannot change after the transaction is created.');
|
|
2606
3385
|
if (
|
|
2607
3386
|
(currentArg.preparationAcceptedAt !== undefined
|
|
2608
3387
|
&& next.preparationAcceptedAt !== currentArg.preparationAcceptedAt)
|
|
@@ -2639,7 +3418,7 @@ export class UpgradeCoordinator {
|
|
|
2639
3418
|
...(current.worker?.logFilePath ? { logFilePath: current.worker.logFilePath } : {}),
|
|
2640
3419
|
},
|
|
2641
3420
|
phase: 'checking',
|
|
2642
|
-
message: 'Checking the
|
|
3421
|
+
message: 'Checking the registry latest version.',
|
|
2643
3422
|
}));
|
|
2644
3423
|
}
|
|
2645
3424
|
|
|
@@ -2744,44 +3523,7 @@ export class UpgradeCoordinator {
|
|
|
2744
3523
|
public async terminateUpgradeWorkerCandidate(
|
|
2745
3524
|
worker: NonNullable<TUpgradeTransaction['worker']>,
|
|
2746
3525
|
): Promise<void> {
|
|
2747
|
-
|
|
2748
|
-
if (identity) {
|
|
2749
|
-
if (
|
|
2750
|
-
identity.fingerprint !== worker.fingerprint
|
|
2751
|
-
|| identity.processGroupId !== worker.processGroupId
|
|
2752
|
-
|| !identity.processGroupLeader
|
|
2753
|
-
|| !await processIdentityHasCliCommand(
|
|
2754
|
-
identity,
|
|
2755
|
-
worker.cliPath,
|
|
2756
|
-
'__upgrade-worker',
|
|
2757
|
-
{ allowMissingAbsolutePath: true },
|
|
2758
|
-
)
|
|
2759
|
-
) throw new Error('The stalled upgrade worker identity changed unexpectedly.');
|
|
2760
|
-
} else if (
|
|
2761
|
-
worker.processGroupId !== worker.pid
|
|
2762
|
-
|| (await readProcessGroupMemberPids(worker.processGroupId)).length === 0
|
|
2763
|
-
) {
|
|
2764
|
-
return;
|
|
2765
|
-
}
|
|
2766
|
-
const signalOwnedWorker = (signalArg: NodeJS.Signals): void => {
|
|
2767
|
-
try {
|
|
2768
|
-
process.kill(-worker.processGroupId, signalArg);
|
|
2769
|
-
} catch (errorArg) {
|
|
2770
|
-
if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
|
|
2771
|
-
}
|
|
2772
|
-
};
|
|
2773
|
-
const waitForDrain = async (): Promise<boolean> => {
|
|
2774
|
-
const deadline = Date.now() + workerTerminationGraceMs;
|
|
2775
|
-
while (Date.now() < deadline) {
|
|
2776
|
-
if ((await readProcessGroupMemberPids(worker.processGroupId)).length === 0) return true;
|
|
2777
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
2778
|
-
}
|
|
2779
|
-
return (await readProcessGroupMemberPids(worker.processGroupId)).length === 0;
|
|
2780
|
-
};
|
|
2781
|
-
signalOwnedWorker('SIGTERM');
|
|
2782
|
-
if (await waitForDrain()) return;
|
|
2783
|
-
signalOwnedWorker('SIGKILL');
|
|
2784
|
-
if (!await waitForDrain()) throw new Error('The stalled upgrade worker group did not terminate.');
|
|
3526
|
+
await terminateVerifiedUpgradeWorkerProcessGroup(worker);
|
|
2785
3527
|
}
|
|
2786
3528
|
|
|
2787
3529
|
public async readSanitizedUpgradeStatus(
|
|
@@ -2797,7 +3539,7 @@ export class UpgradeCoordinator {
|
|
|
2797
3539
|
: transaction.phase;
|
|
2798
3540
|
return {
|
|
2799
3541
|
fromVersion: transaction.sourceVersion,
|
|
2800
|
-
toVersion: transaction
|
|
3542
|
+
toVersion: upgradeTransactionTargetVersion(transaction)!,
|
|
2801
3543
|
phase,
|
|
2802
3544
|
};
|
|
2803
3545
|
}
|
|
@@ -3022,26 +3764,81 @@ export class UpgradeCoordinator {
|
|
|
3022
3764
|
}
|
|
3023
3765
|
}
|
|
3024
3766
|
|
|
3025
|
-
public async
|
|
3767
|
+
public async removeWorkerAcknowledgement(tokenArg: string): Promise<void> {
|
|
3768
|
+
await this.init();
|
|
3769
|
+
let removed = false;
|
|
3770
|
+
await plugins.fs.promises.unlink(this.tokenFilePath('ack', tokenArg)).then(() => {
|
|
3771
|
+
removed = true;
|
|
3772
|
+
}).catch((errorArg) => {
|
|
3773
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT') throw errorArg;
|
|
3774
|
+
});
|
|
3775
|
+
if (removed) await syncDirectory(this.baseDirectory);
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
public async acknowledgeWorker(
|
|
3779
|
+
tokenArg: string,
|
|
3780
|
+
workerArg: Pick<IVerifiedUpgradeWorkerProcessGroup, 'pid' | 'processGroupId' | 'fingerprint'>,
|
|
3781
|
+
): Promise<void> {
|
|
3026
3782
|
await this.init();
|
|
3027
|
-
const
|
|
3783
|
+
const identity = await readControllerProcessIdentity(workerArg.pid);
|
|
3784
|
+
if (
|
|
3785
|
+
!identity
|
|
3786
|
+
|| identity.fingerprint !== workerArg.fingerprint
|
|
3787
|
+
|| identity.processGroupId !== workerArg.processGroupId
|
|
3788
|
+
) throw new Error('The upgrade acknowledgement worker identity is no longer current.');
|
|
3789
|
+
const acknowledgement: IUpgradeWorkerAcknowledgement = {
|
|
3028
3790
|
version: coordinationVersion,
|
|
3029
3791
|
tokenHash: hashToken(tokenArg),
|
|
3792
|
+
pid: identity.pid,
|
|
3793
|
+
processGroupId: identity.processGroupId,
|
|
3794
|
+
fingerprint: identity.fingerprint,
|
|
3030
3795
|
};
|
|
3031
3796
|
const acknowledgementPath = this.tokenFilePath('ack', tokenArg);
|
|
3797
|
+
const temporaryPath = `${acknowledgementPath}.tmp-${process.pid}-${plugins.crypto.randomBytes(4).toString('hex')}`;
|
|
3798
|
+
let published = false;
|
|
3032
3799
|
try {
|
|
3033
|
-
await writePrivateJson(
|
|
3800
|
+
await writePrivateJson(temporaryPath, acknowledgement);
|
|
3801
|
+
try {
|
|
3802
|
+
await plugins.fs.promises.link(temporaryPath, acknowledgementPath);
|
|
3803
|
+
published = true;
|
|
3804
|
+
await syncDirectory(this.baseDirectory);
|
|
3805
|
+
} catch (errorArg) {
|
|
3806
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
3807
|
+
if (published) return;
|
|
3808
|
+
throw errorArg;
|
|
3809
|
+
}
|
|
3810
|
+
let existing: unknown;
|
|
3811
|
+
try {
|
|
3812
|
+
existing = await readPrivateJson(acknowledgementPath);
|
|
3813
|
+
} catch (readErrorArg) {
|
|
3814
|
+
if ((readErrorArg as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
3815
|
+
published = true;
|
|
3816
|
+
return;
|
|
3817
|
+
}
|
|
3818
|
+
throw readErrorArg;
|
|
3819
|
+
}
|
|
3820
|
+
if (
|
|
3821
|
+
!existing
|
|
3822
|
+
|| typeof existing !== 'object'
|
|
3823
|
+
|| Array.isArray(existing)
|
|
3824
|
+
|| !exactKeys(existing as Record<string, unknown>, [
|
|
3825
|
+
'version', 'tokenHash', 'pid', 'processGroupId', 'fingerprint',
|
|
3826
|
+
])
|
|
3827
|
+
|| (existing as Record<string, unknown>).version !== coordinationVersion
|
|
3828
|
+
|| (existing as Record<string, unknown>).tokenHash !== acknowledgement.tokenHash
|
|
3829
|
+
|| (existing as Record<string, unknown>).pid !== acknowledgement.pid
|
|
3830
|
+
|| (existing as Record<string, unknown>).processGroupId !== acknowledgement.processGroupId
|
|
3831
|
+
|| (existing as Record<string, unknown>).fingerprint !== acknowledgement.fingerprint
|
|
3832
|
+
) throw new Error('The existing upgrade acknowledgement is invalid.');
|
|
3833
|
+
published = true;
|
|
3834
|
+
}
|
|
3034
3835
|
} catch (errorArg) {
|
|
3035
|
-
if (
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|| !exactKeys(existing as Record<string, unknown>, ['version', 'tokenHash'])
|
|
3042
|
-
|| (existing as Record<string, unknown>).version !== coordinationVersion
|
|
3043
|
-
|| (existing as Record<string, unknown>).tokenHash !== acknowledgement.tokenHash
|
|
3044
|
-
) throw new Error('The existing upgrade acknowledgement is invalid.');
|
|
3836
|
+
if (published) return;
|
|
3837
|
+
throw errorArg;
|
|
3838
|
+
} finally {
|
|
3839
|
+
await plugins.fs.promises.unlink(temporaryPath).catch((errorArg) => {
|
|
3840
|
+
if ((errorArg as NodeJS.ErrnoException).code !== 'ENOENT' && !published) throw errorArg;
|
|
3841
|
+
});
|
|
3045
3842
|
}
|
|
3046
3843
|
}
|
|
3047
3844
|
|
|
@@ -3050,6 +3847,8 @@ export class UpgradeCoordinator {
|
|
|
3050
3847
|
timeoutMsArg = 10_000,
|
|
3051
3848
|
): Promise<void> {
|
|
3052
3849
|
const token = assertToken(tokenArg);
|
|
3850
|
+
const identity = await readControllerProcessIdentity(process.pid);
|
|
3851
|
+
if (!identity) throw new Error('The upgrade worker acknowledgement identity is unavailable.');
|
|
3053
3852
|
const ackPath = this.tokenFilePath('ack', token);
|
|
3054
3853
|
const deadline = Date.now() + timeoutMsArg;
|
|
3055
3854
|
while (Date.now() < deadline) {
|
|
@@ -3060,11 +3859,14 @@ export class UpgradeCoordinator {
|
|
|
3060
3859
|
}
|
|
3061
3860
|
const ack = value as Record<string, unknown>;
|
|
3062
3861
|
if (
|
|
3063
|
-
!exactKeys(ack, ['version', 'tokenHash'])
|
|
3862
|
+
!exactKeys(ack, ['version', 'tokenHash', 'pid', 'processGroupId', 'fingerprint'])
|
|
3064
3863
|
|| ack.version !== coordinationVersion
|
|
3065
3864
|
|| typeof ack.tokenHash !== 'string'
|
|
3066
3865
|
|| !upgradeTokenHashPattern.test(ack.tokenHash)
|
|
3067
3866
|
|| !tokensEqual(ack.tokenHash, hashToken(token))
|
|
3867
|
+
|| ack.pid !== identity.pid
|
|
3868
|
+
|| ack.processGroupId !== identity.processGroupId
|
|
3869
|
+
|| ack.fingerprint !== identity.fingerprint
|
|
3068
3870
|
) {
|
|
3069
3871
|
throw new Error(`The ${currentCliName} upgrade acknowledgement binding is invalid.`);
|
|
3070
3872
|
}
|