agl 22.0.1 → 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.
@@ -17,36 +17,43 @@ import {
17
17
  import {
18
18
  inspectControllerProcess,
19
19
  listControllerDataWriterProcessesForCli,
20
+ listControllerDataWriterProcessesForCliPaths,
20
21
  listControllerProcessesForCli,
21
22
  processIdentityHasCliCommand,
22
23
  readControllerProcessIdentity,
23
24
  readProcessGroupMemberPids,
25
+ type IControllerProcessIdentity,
24
26
  } from './classes.processinspection.js';
25
27
  import {
26
28
  createUpgradeToken,
29
+ compareUpgradeSemver,
27
30
  type TUpgradeTransaction,
31
+ type IUpgradeTransactionInventoryEntry,
28
32
  type IUpgradeTransactionV3,
29
33
  type IUpgradeExpectedController,
30
34
  type IUpgradeWorkerPayload,
35
+ normalizeUpgradeRegistryUrl,
31
36
  UpgradeCoordinator,
37
+ type UpgradeInstallationLock,
32
38
  upgradeCoordinationVersion,
33
39
  upgradePackageTransitionTransactionVersion,
34
40
  upgradePackageTransitionSource,
35
41
  upgradePackageTransitionTarget,
42
+ upgradeTransactionIsStalled,
43
+ upgradeTransactionTargetVersion,
36
44
  serializeUpgradeWorkerPayload,
45
+ terminateVerifiedUpgradeWorkerProcessGroup,
37
46
  } from './classes.upgradecoordinator.js';
38
47
  import {
39
48
  upgradeCoordinationRootEnvironmentVariable,
40
49
  upgradeTokenEnvironmentVariable,
41
50
  } from './constants.upgradeenvironment.js';
42
51
 
43
- const npmRegistryUrl = 'https://registry.npmjs.org/';
44
52
  const commandOutputByteLimit = 1024 * 1024;
45
53
  const loggedCommandOutputByteLimit = 16 * 1024 * 1024;
46
54
  const packageManagerTimeoutMs = 10 * 60 * 1000;
47
55
  const controllerRestartTimeoutMs = 90_000;
48
56
  const commandGroupDrainTimeoutMs = 5_000;
49
- const candidateCleanupRetryIntervalMs = 250;
50
57
  const retainedUpgradeLogCount = 10;
51
58
  const minimumPrunableUpgradeLogAgeMs = 30 * 60 * 1000;
52
59
  const upgradePreparationPollMs = 250;
@@ -59,7 +66,6 @@ const scrubUpgradeCoordinationEnvironment = (): void => {
59
66
  delete process.env[upgradeCoordinationRootEnvironmentVariable];
60
67
  };
61
68
  const upgradeLogPattern = /^upgrade-[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{6}\.[0-9]{3}Z-[A-Za-z0-9_-]{10}\.log$/;
62
- const semverPattern = /^(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-]+)*)?$/;
63
69
  let upgradeWorkerCliPath: string | undefined;
64
70
  const currentPackageIdentity = String(controllerPackageName)
65
71
  === upgradePackageTransitionSource.packageName
@@ -67,6 +73,10 @@ const currentPackageIdentity = String(controllerPackageName)
67
73
  : upgradePackageTransitionTarget;
68
74
  const currentCliName = currentPackageIdentity.cliName;
69
75
 
76
+ const registryArguments = (registryUrlArg?: string): string[] => registryUrlArg
77
+ ? [`--registry=${registryUrlArg}`]
78
+ : [];
79
+
70
80
  const packageManagerEnvironmentAllowlist = new Set([
71
81
  'CI',
72
82
  'COREPACK_HOME',
@@ -81,6 +91,7 @@ const packageManagerEnvironmentAllowlist = new Set([
81
91
  'NPM_CONFIG_REGISTRY',
82
92
  'NPM_CONFIG_USERCONFIG',
83
93
  'PATH',
94
+ 'PNPM_CONFIG_REGISTRY',
84
95
  'PNPM_HOME',
85
96
  'SHELL',
86
97
  'SSL_CERT_DIR',
@@ -98,13 +109,6 @@ const packageManagerEnvironmentAllowlist = new Set([
98
109
  'XDG_STATE_HOME',
99
110
  ]);
100
111
 
101
- interface IParsedSemver {
102
- major: string;
103
- minor: string;
104
- patch: string;
105
- prerelease: string[];
106
- }
107
-
108
112
  class UpgradeCommandCleanupError extends Error {}
109
113
 
110
114
  interface IGlobalListDependency {
@@ -134,6 +138,74 @@ export interface IDetachedUpgradeWorker {
134
138
  logFilePath: string;
135
139
  }
136
140
 
141
+ export interface IUpgradeWorkerLaunchCandidate {
142
+ pid: number;
143
+ cliPath: string;
144
+ logFilePath: string;
145
+ processGroupId?: number;
146
+ fingerprint?: string;
147
+ }
148
+
149
+ export class UpgradeWorkerHandoffError extends Error {
150
+ constructor(
151
+ messageArg: string,
152
+ causeArg: unknown,
153
+ public readonly recoveryLock: UpgradeInstallationLock,
154
+ ) {
155
+ super(messageArg, { cause: causeArg });
156
+ this.name = 'UpgradeWorkerHandoffError';
157
+ }
158
+ }
159
+
160
+ export class UpgradeWorkerCandidateDrainageError extends Error {
161
+ constructor(messageArg: string, causeArg: unknown) {
162
+ super(messageArg, { cause: causeArg });
163
+ this.name = 'UpgradeWorkerCandidateDrainageError';
164
+ }
165
+ }
166
+
167
+ export interface ITerminalizeUpgradeWorkerHandoffFailureOptions {
168
+ coordinator: UpgradeCoordinator;
169
+ token: string;
170
+ error: UpgradeWorkerHandoffError;
171
+ }
172
+
173
+ export const terminalizeUpgradeWorkerHandoffFailure = async (
174
+ optionsArg: ITerminalizeUpgradeWorkerHandoffFailureOptions,
175
+ ): Promise<never> => {
176
+ const handoffFailure = optionsArg.error.cause ?? optionsArg.error;
177
+ let terminalizationError: unknown;
178
+ let terminalized = false;
179
+ try {
180
+ await optionsArg.error.recoveryLock.assertOwned();
181
+ await optionsArg.coordinator.finishTransaction(
182
+ optionsArg.token,
183
+ false,
184
+ 'The upgrade worker handoff failed before package work began.',
185
+ handoffFailure instanceof Error ? handoffFailure.message : String(handoffFailure),
186
+ );
187
+ terminalized = true;
188
+ } catch (errorArg) {
189
+ terminalizationError = errorArg;
190
+ }
191
+ let releaseError: unknown;
192
+ if (terminalized) {
193
+ try {
194
+ await optionsArg.error.recoveryLock.release();
195
+ } catch (errorArg) {
196
+ releaseError = errorArg;
197
+ }
198
+ }
199
+ const failures = [optionsArg.error, terminalizationError, releaseError]
200
+ .filter((failureArg) => failureArg !== undefined);
201
+ throw failures.length > 1
202
+ ? new AggregateError(
203
+ failures,
204
+ 'The upgrade worker handoff failed and its transaction cleanup was incomplete.',
205
+ )
206
+ : optionsArg.error;
207
+ };
208
+
137
209
  const proxyEnvironmentValueIsSafe = (valueArg: string): boolean => {
138
210
  try {
139
211
  const parsed = new URL(valueArg);
@@ -158,6 +230,13 @@ export const createPackageManagerEnvironment = (
158
230
  (normalizedName === 'HTTP_PROXY' || normalizedName === 'HTTPS_PROXY')
159
231
  && !proxyEnvironmentValueIsSafe(value)
160
232
  ) continue;
233
+ if (normalizedName === 'NPM_CONFIG_REGISTRY' || normalizedName === 'PNPM_CONFIG_REGISTRY') {
234
+ try {
235
+ normalizeUpgradeRegistryUrl(value);
236
+ } catch {
237
+ continue;
238
+ }
239
+ }
161
240
  environment[name] = value;
162
241
  }
163
242
  return environment;
@@ -787,7 +866,12 @@ const resolveAuthoritativeRecoveryInstallation = async (
787
866
  : upgradePackageTransitionSource;
788
867
  return {
789
868
  packageName: identity.packageName,
790
- versions: [identity.version],
869
+ versions: transactionArg.targetPackageCommitStarted === true
870
+ ? [...new Set([
871
+ identity.version,
872
+ transactionArg.recoveryTargetVersion ?? capturedInstallationArg.packageVersion,
873
+ ])]
874
+ : [identity.version],
791
875
  managementVersion: identity.managementVersion,
792
876
  cliName: identity.cliName,
793
877
  cliRelativePath: identity.cliRelativePath,
@@ -865,6 +949,7 @@ const resolveAuthoritativeRecoveryInstallation = async (
865
949
  const verifyPackageTransitionTopology = async (
866
950
  contextArg: IPackageTransitionContext,
867
951
  expectedArg: TPackageTransitionTopology,
952
+ targetVersionArg: string = upgradePackageTransitionTarget.version,
868
953
  ): Promise<IPackageTransitionTopology> => {
869
954
  const output = await runCapturedCommand(
870
955
  contextArg.packageManagerPath,
@@ -894,7 +979,10 @@ const verifyPackageTransitionTopology = async (
894
979
  ? readExactGlobalPackage(contextArg, sourceDependency, upgradePackageTransitionSource)
895
980
  : undefined,
896
981
  targetDependency
897
- ? readExactGlobalPackage(contextArg, targetDependency, upgradePackageTransitionTarget)
982
+ ? readExactGlobalPackage(contextArg, targetDependency, {
983
+ ...upgradePackageTransitionTarget,
984
+ version: targetVersionArg,
985
+ })
898
986
  : undefined,
899
987
  ]);
900
988
  if (expectedArg === 'grouped' && source?.groupDirectory !== target?.groupDirectory) {
@@ -944,6 +1032,7 @@ export const resolveInheritedUpgradeTargetInstallation = async (
944
1032
  const topology = await verifyPackageTransitionTopology(
945
1033
  await resolvePackageManagerContext(),
946
1034
  'target-only',
1035
+ upgradeTransactionTargetVersion(transactionArg)!,
947
1036
  );
948
1037
  if (!topology.target) throw new Error('The committed target package is unavailable.');
949
1038
  return topology.target;
@@ -983,66 +1072,143 @@ export const tryResolveCurrentPnpmGlobalInstallation = async (
983
1072
  return await resolveCurrentPnpmGlobalInstallation();
984
1073
  };
985
1074
 
986
- const parseSemver = (valueArg: string): IParsedSemver => {
987
- const match = semverPattern.exec(valueArg);
988
- if (!match) throw new Error(`Invalid semantic version: ${valueArg}`);
989
- const prerelease = match[4]?.split('.') ?? [];
990
- if (prerelease.some((identifierArg) => /^\d+$/.test(identifierArg)
991
- && identifierArg.length > 1
992
- && identifierArg.startsWith('0'))) {
993
- throw new Error(`Invalid semantic version: ${valueArg}`);
1075
+ export const compareSemver = compareUpgradeSemver;
1076
+
1077
+ export interface IInspectOrphanedUpgradeForAdoptionOptions {
1078
+ coordinator: UpgradeCoordinator;
1079
+ installation: IPnpmGlobalInstallation;
1080
+ port: number;
1081
+ registryUrl?: string;
1082
+ }
1083
+
1084
+ export const inspectOrphanedUpgradeForAdoption = async (
1085
+ optionsArg: IInspectOrphanedUpgradeForAdoptionOptions,
1086
+ ): Promise<IUpgradeTransactionInventoryEntry | undefined> => {
1087
+ const inventory = await optionsArg.coordinator.inspectCanonicalTransactionInventory();
1088
+ const nonterminal = inventory.transactions.filter((entryArg) => !entryArg.transaction.terminal);
1089
+ if (nonterminal.length === 0) return undefined;
1090
+ if (nonterminal.length !== 1) {
1091
+ throw new Error('Multiple nonterminal upgrade transactions block a fresh upgrade.');
1092
+ }
1093
+ const entry = nonterminal[0];
1094
+ const transaction = entry.transaction;
1095
+ const eligibleV2 = transaction.version === upgradeCoordinationVersion
1096
+ && transaction.controllerWasRunning === true
1097
+ && transaction.targetStartupInvoked === true
1098
+ && transaction.targetVersion !== undefined
1099
+ && transaction.preparationCompletedAt !== undefined
1100
+ && (transaction.phase === 'restarting' || transaction.phase === 'continuing')
1101
+ && compareSemver(transaction.sourceVersion, upgradePackageTransitionTarget.version) >= 0
1102
+ && compareSemver(transaction.sourceVersion, transaction.targetVersion) < 0
1103
+ && compareSemver(transaction.targetVersion, optionsArg.installation.packageVersion) <= 0;
1104
+ const eligibleV3 = transaction.version === upgradePackageTransitionTransactionVersion
1105
+ && currentPackageIdentity.packageName === upgradePackageTransitionTarget.packageName
1106
+ && transaction.targetPackageCommitStarted === true
1107
+ && (!transaction.controllerWasRunning || transaction.preparationCompletedAt !== undefined)
1108
+ && (
1109
+ transaction.phase === 'installing'
1110
+ || transaction.phase === 'restarting'
1111
+ || transaction.phase === 'continuing'
1112
+ )
1113
+ && compareSemver(
1114
+ upgradeTransactionTargetVersion(transaction)!,
1115
+ optionsArg.installation.packageVersion,
1116
+ ) <= 0;
1117
+ if (
1118
+ transaction.port !== optionsArg.port
1119
+ || (!eligibleV2 && !eligibleV3)
1120
+ || !upgradeTransactionIsStalled(transaction)
1121
+ ) throw new Error('A nonterminal upgrade transaction blocks a fresh upgrade.');
1122
+ if (transaction.version === upgradePackageTransitionTransactionVersion && optionsArg.registryUrl) {
1123
+ throw new Error(
1124
+ 'Legacy package-transition recovery uses pnpm registry configuration and cannot retain --registry.',
1125
+ );
994
1126
  }
995
- return {
996
- major: match[1],
997
- minor: match[2],
998
- patch: match[3],
999
- prerelease,
1000
- };
1127
+ if (
1128
+ transaction.version === upgradeCoordinationVersion
1129
+ && transaction.registryUrl !== undefined
1130
+ && optionsArg.registryUrl !== undefined
1131
+ && transaction.registryUrl !== optionsArg.registryUrl
1132
+ ) throw new Error('The orphaned upgrade registry conflicts with the requested registry.');
1133
+ if (await optionsArg.coordinator.transactionWorkerIsLive(transaction)) {
1134
+ throw new Error('A nonterminal upgrade worker is still running.');
1135
+ }
1136
+ if (
1137
+ transaction.controller
1138
+ && await optionsArg.coordinator.transactionControllerIsLive(transaction)
1139
+ ) throw new Error('A nonterminal upgrade controller is still running.');
1140
+ const adoptionMatch = /^transaction-([a-f0-9]{20})\.json\.adopting-([a-f0-9]{20})$/
1141
+ .exec(entry.fileName);
1142
+ const prefixes = new Set([
1143
+ transaction.tokenHash.slice(0, 20),
1144
+ ...(adoptionMatch ? [adoptionMatch[1], adoptionMatch[2]] : []),
1145
+ ]);
1146
+ if (inventory.tokenMetadataFileNames.some((fileNameArg) => (
1147
+ [...prefixes].some((prefixArg) => fileNameArg.includes(`-${prefixArg}.json`))
1148
+ ))) throw new Error('The orphaned upgrade retains token-bound coordination metadata.');
1149
+ return entry;
1001
1150
  };
1002
1151
 
1003
- const compareNumericStrings = (leftArg: string, rightArg: string): number => {
1004
- if (leftArg.length !== rightArg.length) return leftArg.length < rightArg.length ? -1 : 1;
1005
- return leftArg === rightArg ? 0 : leftArg < rightArg ? -1 : 1;
1006
- };
1152
+ export interface IAdoptOrphanedUpgradeUnderLockOptions
1153
+ extends IInspectOrphanedUpgradeForAdoptionOptions {
1154
+ lock: UpgradeInstallationLock;
1155
+ token: string;
1156
+ expected: IUpgradeTransactionInventoryEntry;
1157
+ }
1007
1158
 
1008
- export const compareSemver = (leftArg: string, rightArg: string): number => {
1009
- const left = parseSemver(leftArg);
1010
- const right = parseSemver(rightArg);
1011
- for (const key of ['major', 'minor', 'patch'] as const) {
1012
- const comparison = compareNumericStrings(left[key], right[key]);
1013
- if (comparison !== 0) return comparison;
1014
- }
1015
- if (left.prerelease.length === 0 || right.prerelease.length === 0) {
1016
- if (left.prerelease.length === right.prerelease.length) return 0;
1017
- return left.prerelease.length === 0 ? 1 : -1;
1018
- }
1019
- const identifierCount = Math.max(left.prerelease.length, right.prerelease.length);
1020
- for (let index = 0; index < identifierCount; index++) {
1021
- const leftIdentifier = left.prerelease[index];
1022
- const rightIdentifier = right.prerelease[index];
1023
- if (leftIdentifier === undefined || rightIdentifier === undefined) {
1024
- return leftIdentifier === rightIdentifier ? 0 : leftIdentifier === undefined ? -1 : 1;
1025
- }
1026
- if (leftIdentifier === rightIdentifier) continue;
1027
- const leftNumeric = /^\d+$/.test(leftIdentifier);
1028
- const rightNumeric = /^\d+$/.test(rightIdentifier);
1029
- if (leftNumeric && rightNumeric) {
1030
- return compareNumericStrings(leftIdentifier, rightIdentifier);
1031
- }
1032
- if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
1033
- return leftIdentifier < rightIdentifier ? -1 : 1;
1034
- }
1035
- return 0;
1159
+ export const adoptOrphanedUpgradeUnderLock = async (
1160
+ optionsArg: IAdoptOrphanedUpgradeUnderLockOptions,
1161
+ ): Promise<TUpgradeTransaction> => {
1162
+ await optionsArg.lock.assertOwned();
1163
+ const candidate = await inspectOrphanedUpgradeForAdoption(optionsArg);
1164
+ if (
1165
+ !candidate
1166
+ || candidate.transaction.tokenHash !== optionsArg.expected.transaction.tokenHash
1167
+ || candidate.transaction.revision !== optionsArg.expected.transaction.revision
1168
+ ) throw new Error('The orphaned upgrade transaction changed before adoption.');
1169
+ const cliPaths = [
1170
+ optionsArg.installation.cliPath,
1171
+ ...(candidate.transaction.worker ? [candidate.transaction.worker.cliPath] : []),
1172
+ ...(candidate.transaction.controller ? [candidate.transaction.controller.cliPath] : []),
1173
+ ];
1174
+ const assertNoRuntimeWriters = async (): Promise<void> => {
1175
+ if (await isLoopbackPortListening(optionsArg.port)) {
1176
+ throw new Error('The orphaned upgrade port became occupied before recovery.');
1177
+ }
1178
+ const writers = await listControllerDataWriterProcessesForCliPaths(cliPaths, {
1179
+ includeInstalledPackagePaths: true,
1180
+ });
1181
+ if (writers.length > 0) {
1182
+ throw new Error(`A ${writers[0].kind} data writer prevents orphaned upgrade recovery.`);
1183
+ }
1184
+ };
1185
+ await assertNoRuntimeWriters();
1186
+ await optionsArg.lock.assertOwned();
1187
+ const adopted = await optionsArg.coordinator.adoptOrphanedTransaction({
1188
+ lock: optionsArg.lock,
1189
+ token: optionsArg.token,
1190
+ expectedTokenHash: candidate.transaction.tokenHash,
1191
+ expectedRevision: candidate.transaction.revision,
1192
+ port: optionsArg.port,
1193
+ installedVersion: optionsArg.installation.packageVersion,
1194
+ ...(optionsArg.registryUrl === undefined ? {} : { registryUrl: optionsArg.registryUrl }),
1195
+ });
1196
+ await assertNoRuntimeWriters();
1197
+ await optionsArg.lock.assertOwned();
1198
+ return adopted;
1036
1199
  };
1037
1200
 
1038
- const readLatestVersion = async (installationArg: IPnpmGlobalInstallation): Promise<string> => {
1201
+ const readLatestVersion = async (
1202
+ installationArg: IPnpmGlobalInstallation,
1203
+ registryUrlArg?: string,
1204
+ ): Promise<string> => {
1039
1205
  const output = await runCapturedCommand(
1040
1206
  installationArg.packageManagerPath,
1041
1207
  [
1042
1208
  'view',
1043
1209
  controllerPackageName,
1044
1210
  'dist-tags.latest',
1045
- `--registry=${npmRegistryUrl}`,
1211
+ ...registryArguments(registryUrlArg),
1046
1212
  '--json',
1047
1213
  ],
1048
1214
  createPackageManagerEnvironment(),
@@ -1051,40 +1217,25 @@ const readLatestVersion = async (installationArg: IPnpmGlobalInstallation): Prom
1051
1217
  try {
1052
1218
  version = JSON.parse(output) as unknown;
1053
1219
  } catch {
1054
- throw new Error('pnpm view returned malformed JSON for the npm latest tag.');
1220
+ throw new Error('pnpm view returned malformed JSON for the registry latest tag.');
1055
1221
  }
1056
1222
  if (typeof version !== 'string') {
1057
- throw new Error('The npm latest tag did not resolve to one package version.');
1223
+ throw new Error('The registry latest tag did not resolve to one package version.');
1058
1224
  }
1059
- parseSemver(version);
1225
+ compareUpgradeSemver(version, version);
1060
1226
  return version;
1061
1227
  };
1062
1228
 
1063
- const assertInternalRegistryUrl = (valueArg: string): string => {
1064
- let parsed: URL;
1065
- try {
1066
- parsed = new URL(valueArg);
1067
- } catch (errorArg) {
1068
- throw new Error('The internal upgrade registry URL is invalid.', { cause: errorArg });
1069
- }
1070
- if (
1071
- (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
1072
- || parsed.username.length > 0
1073
- || parsed.password.length > 0
1074
- ) throw new Error('The internal upgrade registry URL is unsafe.');
1075
- return parsed.toString();
1076
- };
1077
-
1078
1229
  const resolveExactPackageTransitionTarget = async (
1079
1230
  contextArg: IPackageTransitionContext,
1080
- registryUrlArg: string,
1231
+ registryUrlArg?: string,
1081
1232
  ): Promise<void> => {
1082
1233
  const output = await runCapturedCommand(
1083
1234
  contextArg.packageManagerPath,
1084
1235
  [
1085
1236
  'view',
1086
1237
  `${upgradePackageTransitionTarget.packageName}@${upgradePackageTransitionTarget.version}`,
1087
- `--registry=${registryUrlArg}`,
1238
+ ...registryArguments(registryUrlArg),
1088
1239
  '--json',
1089
1240
  ],
1090
1241
  createPackageManagerEnvironment(),
@@ -1226,63 +1377,42 @@ const parseUpgradeRecoveryWorkerContext = (
1226
1377
  return context as unknown as IUpgradeRecoveryWorkerContext;
1227
1378
  };
1228
1379
 
1229
- const waitForProcessGroupDrain = async (
1230
- processGroupIdArg: number,
1231
- timeoutMsArg: number,
1232
- ): Promise<boolean> => {
1233
- const deadline = Date.now() + timeoutMsArg;
1234
- while (Date.now() < deadline) {
1235
- if ((await readProcessGroupMemberPids(processGroupIdArg)).length === 0) return true;
1236
- await new Promise((resolve) => setTimeout(resolve, 50));
1237
- }
1238
- return (await readProcessGroupMemberPids(processGroupIdArg)).length === 0;
1239
- };
1240
-
1241
- const terminateSpawnedDetachedWorker = async (processIdArg: number): Promise<void> => {
1242
- const signal = (signalArg: NodeJS.Signals): void => {
1243
- try {
1244
- process.kill(-processIdArg, signalArg);
1245
- } catch (errorArg) {
1246
- if ((errorArg as NodeJS.ErrnoException).code !== 'ESRCH') throw errorArg;
1247
- }
1248
- };
1249
- signal('SIGTERM');
1250
- if (await waitForProcessGroupDrain(processIdArg, commandGroupDrainTimeoutMs)) return;
1251
- signal('SIGKILL');
1252
- if (!await waitForProcessGroupDrain(processIdArg, commandGroupDrainTimeoutMs)) {
1253
- throw new Error('The detached upgrade worker candidate group did not terminate.');
1254
- }
1255
- };
1380
+ const terminateSpawnedDetachedWorker = async (
1381
+ identityArg: IControllerProcessIdentity,
1382
+ cliPathArg: string,
1383
+ ): Promise<void> => await terminateVerifiedUpgradeWorkerProcessGroup({
1384
+ pid: identityArg.pid,
1385
+ processGroupId: identityArg.processGroupId,
1386
+ fingerprint: identityArg.fingerprint,
1387
+ cliPath: cliPathArg,
1388
+ });
1256
1389
 
1257
- interface ICandidateCleanupRetryDependencies {
1390
+ export interface ICandidateCleanupDependencies {
1258
1391
  beforeAttempt?: (processIdArg?: number) => Promise<void>;
1259
- delay?: (millisecondsArg: number) => Promise<void>;
1260
1392
  }
1261
1393
 
1262
- const retryCandidateCleanupUntilDrained = async (
1394
+ const cleanupCandidateUntilDrained = async (
1263
1395
  cleanupArg: () => Promise<void>,
1264
- dependenciesArg: ICandidateCleanupRetryDependencies = {},
1396
+ dependenciesArg: ICandidateCleanupDependencies = {},
1265
1397
  processIdArg?: number,
1266
1398
  ): Promise<void> => {
1267
- const delay = dependenciesArg.delay
1268
- ?? (async (millisecondsArg: number) => new Promise((resolve) => (
1269
- setTimeout(resolve, millisecondsArg)
1270
- )));
1271
- while (true) {
1272
- try {
1273
- await dependenciesArg.beforeAttempt?.(processIdArg);
1274
- await cleanupArg();
1275
- return;
1276
- } catch {
1277
- await delay(candidateCleanupRetryIntervalMs);
1278
- }
1279
- }
1399
+ await dependenciesArg.beforeAttempt?.(processIdArg);
1400
+ await cleanupArg();
1280
1401
  };
1281
1402
 
1403
+ export const terminateUpgradeWorkerCandidateUntilDrained = async (
1404
+ coordinatorArg: UpgradeCoordinator,
1405
+ candidateArg: IDetachedUpgradeWorker,
1406
+ ): Promise<void> => await cleanupCandidateUntilDrained(
1407
+ async () => await coordinatorArg.terminateUpgradeWorkerCandidate(candidateArg),
1408
+ {},
1409
+ candidateArg.pid,
1410
+ );
1411
+
1282
1412
  interface IDetachedUpgradeWorkerProcessDependencies {
1283
1413
  readProcessIdentity?: typeof readControllerProcessIdentity;
1284
1414
  processHasCliCommand?: typeof processIdentityHasCliCommand;
1285
- candidateCleanup?: ICandidateCleanupRetryDependencies;
1415
+ candidateCleanup?: ICandidateCleanupDependencies;
1286
1416
  }
1287
1417
 
1288
1418
  const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
@@ -1296,6 +1426,7 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1296
1426
  }
1297
1427
  const log = await openUpgradeLog(optionsArg.payload.token);
1298
1428
  let child: plugins.childProcess.ChildProcess | undefined;
1429
+ let spawnedIdentity: IControllerProcessIdentity | undefined;
1299
1430
  let logClosed = false;
1300
1431
  try {
1301
1432
  const workerEnvironment = { ...(optionsArg.environment ?? process.env) };
@@ -1333,13 +1464,15 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1333
1464
  launchedChild.once('spawn', resolve);
1334
1465
  launchedChild.once('error', reject);
1335
1466
  });
1336
- await log.handle.close();
1337
- logClosed = true;
1338
1467
  if (!launchedChild.pid) {
1339
1468
  throw new Error(`The detached ${currentCliName} upgrade worker has no process ID.`);
1340
1469
  }
1470
+ spawnedIdentity = await readControllerProcessIdentity(launchedChild.pid) ?? undefined;
1471
+ await log.handle.close();
1472
+ logClosed = true;
1341
1473
  const identity = await (dependenciesArg.readProcessIdentity
1342
- ?? readControllerProcessIdentity)(launchedChild.pid);
1474
+ ? dependenciesArg.readProcessIdentity(launchedChild.pid)
1475
+ : spawnedIdentity);
1343
1476
  if (
1344
1477
  !identity
1345
1478
  || !identity.processGroupLeader
@@ -1360,6 +1493,7 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1360
1493
  };
1361
1494
  } catch (errorArg) {
1362
1495
  const errors: unknown[] = [errorArg];
1496
+ let candidateCleanupError: unknown;
1363
1497
  if (!logClosed) {
1364
1498
  try {
1365
1499
  await log.handle.close();
@@ -1369,16 +1503,33 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
1369
1503
  }
1370
1504
  }
1371
1505
  if (child?.pid) {
1372
- await retryCandidateCleanupUntilDrained(
1373
- async () => terminateSpawnedDetachedWorker(child!.pid!),
1374
- dependenciesArg.candidateCleanup,
1375
- child.pid,
1506
+ try {
1507
+ if (!spawnedIdentity) {
1508
+ throw new Error('The detached upgrade worker cleanup identity was not established.');
1509
+ }
1510
+ await cleanupCandidateUntilDrained(
1511
+ async () => terminateSpawnedDetachedWorker(
1512
+ spawnedIdentity!,
1513
+ optionsArg.installation.cliPath,
1514
+ ),
1515
+ dependenciesArg.candidateCleanup,
1516
+ child.pid,
1517
+ );
1518
+ } catch (cleanupErrorArg) {
1519
+ candidateCleanupError = cleanupErrorArg;
1520
+ errors.push(cleanupErrorArg);
1521
+ }
1522
+ }
1523
+ if (candidateCleanupError !== undefined) {
1524
+ throw new UpgradeWorkerCandidateDrainageError(
1525
+ 'The detached upgrade worker launch failed and candidate drainage is unproven.',
1526
+ new AggregateError(errors, 'Detached upgrade worker launch cleanup was incomplete.'),
1376
1527
  );
1377
1528
  }
1378
1529
  if (errors.length > 1) {
1379
1530
  throw new AggregateError(
1380
1531
  errors,
1381
- 'The detached upgrade worker launch and log cleanup both failed.',
1532
+ 'The detached upgrade worker launch failed and cleanup was incomplete.',
1382
1533
  );
1383
1534
  }
1384
1535
  throw errorArg;
@@ -1433,6 +1584,7 @@ const runLoggedCommand = async (optionsArg: {
1433
1584
  const installVersion = async (
1434
1585
  installationArg: IPnpmGlobalInstallation,
1435
1586
  versionArg: string,
1587
+ registryUrlArg?: string,
1436
1588
  ): Promise<void> => {
1437
1589
  await runLoggedCommand({
1438
1590
  executable: installationArg.packageManagerPath,
@@ -1444,7 +1596,7 @@ const installVersion = async (
1444
1596
  '--reporter=append-only',
1445
1597
  '--allow-build=node-pty',
1446
1598
  '--allow-build=fs-ext-extra-prebuilt',
1447
- `--registry=${npmRegistryUrl}`,
1599
+ ...registryArguments(registryUrlArg),
1448
1600
  `${controllerPackageName}@${versionArg}`,
1449
1601
  ],
1450
1602
  environment: createPackageManagerEnvironment(),
@@ -1452,7 +1604,7 @@ const installVersion = async (
1452
1604
  });
1453
1605
  };
1454
1606
 
1455
- const packageAddArguments = (packageSpecArg: string, registryUrlArg: string): string[] => [
1607
+ const packageAddArguments = (packageSpecArg: string, registryUrlArg?: string): string[] => [
1456
1608
  'add',
1457
1609
  '--global',
1458
1610
  '--save-exact',
@@ -1460,14 +1612,14 @@ const packageAddArguments = (packageSpecArg: string, registryUrlArg: string): st
1460
1612
  '--reporter=append-only',
1461
1613
  '--allow-build=node-pty',
1462
1614
  '--allow-build=fs-ext-extra-prebuilt',
1463
- `--registry=${registryUrlArg}`,
1615
+ ...registryArguments(registryUrlArg),
1464
1616
  packageSpecArg,
1465
1617
  ];
1466
1618
 
1467
1619
  const preparePackageAdd = async (
1468
1620
  contextArg: IPackageTransitionContext,
1469
1621
  packageSpecArg: string,
1470
- registryUrlArg: string,
1622
+ registryUrlArg?: string,
1471
1623
  ): Promise<IPreparedManagedCommand> => await prepareManagedCommand({
1472
1624
  executable: contextArg.packageManagerPath,
1473
1625
  arguments: packageAddArguments(packageSpecArg, registryUrlArg),
@@ -1480,7 +1632,7 @@ const preparePackageAdd = async (
1480
1632
  const addExactPackage = async (
1481
1633
  contextArg: IPackageTransitionContext,
1482
1634
  packageSpecArg: string,
1483
- registryUrlArg: string,
1635
+ registryUrlArg?: string,
1484
1636
  ): Promise<void> => {
1485
1637
  await runPreparedManagedCommand(
1486
1638
  await preparePackageAdd(contextArg, packageSpecArg, registryUrlArg),
@@ -1505,12 +1657,14 @@ const prepareControllerStart = async (
1505
1657
  installationArg: IPnpmGlobalInstallation,
1506
1658
  portArg: number,
1507
1659
  tokenArg: string,
1660
+ coordinatorArg: UpgradeCoordinator,
1508
1661
  ): Promise<IPreparedManagedCommand> => await prepareManagedCommand({
1509
1662
  executable: process.execPath,
1510
1663
  arguments: [installationArg.cliPath, 'start', '--port', String(portArg)],
1511
1664
  environment: {
1512
1665
  ...process.env,
1513
1666
  [upgradeTokenEnvironmentVariable]: tokenArg,
1667
+ [upgradeCoordinationRootEnvironmentVariable]: coordinatorArg.baseDirectory,
1514
1668
  },
1515
1669
  timeoutMs: controllerRestartTimeoutMs,
1516
1670
  captureOutput: false,
@@ -1521,8 +1675,9 @@ const startController = async (
1521
1675
  installationArg: IPnpmGlobalInstallation,
1522
1676
  portArg: number,
1523
1677
  tokenArg: string,
1678
+ coordinatorArg: UpgradeCoordinator,
1524
1679
  ): Promise<void> => await runPreparedManagedCommand(
1525
- await prepareControllerStart(installationArg, portArg, tokenArg),
1680
+ await prepareControllerStart(installationArg, portArg, tokenArg, coordinatorArg),
1526
1681
  ).then(() => undefined);
1527
1682
 
1528
1683
  const retainedTargetTransactionMatches = (
@@ -1539,7 +1694,7 @@ const retainedTargetTransactionMatches = (
1539
1694
  && (transactionArg.version === upgradeCoordinationVersion
1540
1695
  || transactionArg.targetPackageCommitted === true)
1541
1696
  && (transactionArg.phase === 'restarting' || transactionArg.phase === 'continuing')
1542
- && transactionArg.targetVersion === installationArg.packageVersion
1697
+ && upgradeTransactionTargetVersion(transactionArg) === installationArg.packageVersion
1543
1698
  && transactionArg.controller?.command === '__serve'
1544
1699
  && transactionArg.controller.cliPath === installationArg.cliPath
1545
1700
  && transactionArg.controller.pid === transactionArg.controller.processGroupId
@@ -1816,6 +1971,7 @@ export const prepareAndStopRunningController = async (
1816
1971
 
1817
1972
  const restorePreviousInstallation = async (
1818
1973
  installationArg: IPnpmGlobalInstallation,
1974
+ registryUrlArg?: string,
1819
1975
  ): Promise<IPnpmGlobalInstallation> => {
1820
1976
  try {
1821
1977
  const current = await resolvePnpmGlobalInstallation();
@@ -1824,7 +1980,7 @@ const restorePreviousInstallation = async (
1824
1980
  if (errorContainsCommandCleanupFailure(errorArg)) throw errorArg;
1825
1981
  // Reinstalling the exact prior version is the authoritative rollback.
1826
1982
  }
1827
- await installVersion(installationArg, installationArg.packageVersion);
1983
+ await installVersion(installationArg, installationArg.packageVersion, registryUrlArg);
1828
1984
  const restored = await resolvePnpmGlobalInstallation();
1829
1985
  if (restored.packageVersion !== installationArg.packageVersion) {
1830
1986
  throw new Error('The previous pnpm-global package version was not restored.');
@@ -1882,7 +2038,7 @@ const targetControllerIdentity = (transactionArg: TUpgradeTransaction): {
1882
2038
  }
1883
2039
  : {
1884
2040
  packageName: transactionArg.targetPackageName,
1885
- packageVersion: transactionArg.targetVersion,
2041
+ packageVersion: upgradeTransactionTargetVersion(transactionArg)!,
1886
2042
  managementVersion: transactionArg.targetManagementVersion,
1887
2043
  };
1888
2044
 
@@ -1899,7 +2055,7 @@ export const assertInheritedUpgradeTransaction = (
1899
2055
  if (
1900
2056
  !transactionArg.controllerWasRunning
1901
2057
  || transactionArg.targetStartupInvoked !== true
1902
- || transactionArg.targetVersion !== currentIdentityArg.packageVersion
2058
+ || upgradeTransactionTargetVersion(transactionArg) !== currentIdentityArg.packageVersion
1903
2059
  ) throw new Error('The inherited upgrade transaction did not authorize target startup.');
1904
2060
  if (transactionArg.version === upgradeCoordinationVersion) {
1905
2061
  if (
@@ -1922,7 +2078,7 @@ const runPackageTransitionUpgradeWorker = async (optionsArg: {
1922
2078
  payload: IUpgradeWorkerPayload;
1923
2079
  transaction: IUpgradeTransactionV3;
1924
2080
  currentInstallation: IPnpmGlobalInstallation;
1925
- registryUrl: string;
2081
+ registryUrl?: string;
1926
2082
  }): Promise<void> => {
1927
2083
  const {
1928
2084
  coordinator,
@@ -2109,7 +2265,7 @@ const runPackageTransitionUpgradeWorker = async (optionsArg: {
2109
2265
  const sourceTopology = await verifyPackageTransitionTopology(context, 'source-only');
2110
2266
  if (!sourceTopology.source) throw new Error('The exact bridge package was not restored.');
2111
2267
  if (runningStatus) {
2112
- await startController(sourceTopology.source, payload.port, payload.token);
2268
+ await startController(sourceTopology.source, payload.port, payload.token, coordinator);
2113
2269
  const restoredStatus = await queryUpgradeManagedController(
2114
2270
  payload.port,
2115
2271
  sourceControllerIdentity(initialTransaction),
@@ -2147,6 +2303,7 @@ const runPackageTransitionUpgradeWorker = async (optionsArg: {
2147
2303
  targetInstallation,
2148
2304
  payload.port,
2149
2305
  payload.token,
2306
+ coordinator,
2150
2307
  );
2151
2308
  await coordinator.mutateTransaction(payload.token, (current) => {
2152
2309
  if (current.version !== upgradePackageTransitionTransactionVersion) {
@@ -2318,11 +2475,13 @@ export async function runUpgradeWorker(
2318
2475
  payload: payloadArg,
2319
2476
  transaction,
2320
2477
  currentInstallation,
2321
- registryUrl: assertInternalRegistryUrl(internalOptionsArg.registryUrl ?? npmRegistryUrl),
2478
+ registryUrl: internalOptionsArg.registryUrl === undefined
2479
+ ? undefined
2480
+ : normalizeUpgradeRegistryUrl(internalOptionsArg.registryUrl),
2322
2481
  });
2323
2482
  return;
2324
2483
  }
2325
- const latestVersion = await readLatestVersion(currentInstallation);
2484
+ const latestVersion = await readLatestVersion(currentInstallation, transaction.registryUrl);
2326
2485
  await coordinator.mutateTransaction(payloadArg.token, (current) => {
2327
2486
  if (current.version !== upgradeCoordinationVersion) {
2328
2487
  throw new Error('The same-package upgrade transaction changed format.');
@@ -2331,25 +2490,25 @@ export async function runUpgradeWorker(
2331
2490
  ...current,
2332
2491
  targetVersion: latestVersion,
2333
2492
  phase: 'checking',
2334
- message: `Installed ${currentInstallation.packageVersion}; npm latest is ${latestVersion}.`,
2493
+ message: `Installed ${currentInstallation.packageVersion}; registry latest is ${latestVersion}.`,
2335
2494
  };
2336
2495
  });
2337
2496
  const versionComparison = compareSemver(latestVersion, currentInstallation.packageVersion);
2338
2497
  process.stdout.write(
2339
- `${currentCliName} upgrade: installed ${currentInstallation.packageVersion}, npm latest ${latestVersion}.\n`,
2498
+ `${currentCliName} upgrade: installed ${currentInstallation.packageVersion}, registry latest ${latestVersion}.\n`,
2340
2499
  );
2341
2500
  if (versionComparison <= 0) {
2342
2501
  process.stdout.write(
2343
2502
  versionComparison === 0
2344
2503
  ? `${currentCliName} upgrade: already up to date.\n`
2345
- : `${currentCliName} upgrade: installed version is newer than npm latest; refusing to downgrade.\n`,
2504
+ : `${currentCliName} upgrade: installed version is newer than registry latest; refusing to downgrade.\n`,
2346
2505
  );
2347
2506
  await coordinator.finishTransaction(
2348
2507
  payloadArg.token,
2349
2508
  true,
2350
2509
  versionComparison === 0
2351
2510
  ? 'Already up to date.'
2352
- : 'Installed version is newer than npm latest; no downgrade was performed.',
2511
+ : 'Installed version is newer than registry latest; no downgrade was performed.',
2353
2512
  );
2354
2513
  return;
2355
2514
  }
@@ -2412,18 +2571,21 @@ export async function runUpgradeWorker(
2412
2571
  `Package replacement is blocked by ${writer.kind} source writer PID ${writer.identity.pid}.`,
2413
2572
  );
2414
2573
  }
2415
- await installVersion(currentInstallation, latestVersion);
2574
+ await installVersion(currentInstallation, latestVersion, transaction.registryUrl);
2416
2575
  upgradedInstallation = await resolvePnpmGlobalInstallation();
2417
2576
  if (upgradedInstallation.packageVersion !== latestVersion) {
2418
- throw new Error('pnpm completed without installing the requested npm latest version.');
2577
+ throw new Error('pnpm completed without installing the requested registry version.');
2419
2578
  }
2420
2579
  } catch (upgradeErrorArg) {
2421
2580
  if (errorContainsCommandCleanupFailure(upgradeErrorArg)) throw upgradeErrorArg;
2422
2581
  const recoveryErrors: unknown[] = [upgradeErrorArg];
2423
2582
  try {
2424
- const restored = await restorePreviousInstallation(currentInstallation);
2583
+ const restored = await restorePreviousInstallation(
2584
+ currentInstallation,
2585
+ transaction.registryUrl,
2586
+ );
2425
2587
  if (runningStatus) {
2426
- await startController(restored, payloadArg.port, payloadArg.token);
2588
+ await startController(restored, payloadArg.port, payloadArg.token, coordinator);
2427
2589
  const restoredStatus = await queryUpgradeManagedController(
2428
2590
  payloadArg.port,
2429
2591
  {
@@ -2465,7 +2627,7 @@ export async function runUpgradeWorker(
2465
2627
  targetStartupInvoked: true,
2466
2628
  message: `Invoking ${controllerPackageName} ${latestVersion}.`,
2467
2629
  }));
2468
- await startController(upgradedInstallation, payloadArg.port, payloadArg.token);
2630
+ await startController(upgradedInstallation, payloadArg.port, payloadArg.token, coordinator);
2469
2631
  const upgradedStatus = await queryUpgradeManagedController(payloadArg.port, {
2470
2632
  packageName: controllerPackageName,
2471
2633
  packageVersion: latestVersion,
@@ -2532,18 +2694,27 @@ const recoverPackageTransition = async (optionsArg: {
2532
2694
  globalRoot: optionsArg.installation.globalRoot,
2533
2695
  globalBinDirectory: optionsArg.installation.globalBinDirectory,
2534
2696
  };
2535
- const registryUrl = assertInternalRegistryUrl(npmRegistryUrl);
2697
+ const registryUrl = undefined;
2536
2698
  const transaction = optionsArg.transaction;
2537
2699
  if (transaction.targetPackageCommitStarted === true) {
2538
- const targetSpec = `${transaction.targetPackageName}@${transaction.targetVersion}`;
2700
+ const targetVersion = upgradeTransactionTargetVersion(transaction)!;
2701
+ const targetSpec = `${transaction.targetPackageName}@${targetVersion}`;
2539
2702
  let targetTopology: IPackageTransitionTopology;
2540
2703
  if (transaction.targetPackageCommitted === true) {
2541
2704
  try {
2542
- targetTopology = await verifyPackageTransitionTopology(context, 'target-only');
2705
+ targetTopology = await verifyPackageTransitionTopology(
2706
+ context,
2707
+ 'target-only',
2708
+ targetVersion,
2709
+ );
2543
2710
  } catch (verificationErrorArg) {
2544
2711
  try {
2545
2712
  await addExactPackage(context, targetSpec, registryUrl);
2546
- targetTopology = await verifyPackageTransitionTopology(context, 'target-only');
2713
+ targetTopology = await verifyPackageTransitionTopology(
2714
+ context,
2715
+ 'target-only',
2716
+ targetVersion,
2717
+ );
2547
2718
  } catch (normalizationErrorArg) {
2548
2719
  throw new AggregateError(
2549
2720
  [verificationErrorArg, normalizationErrorArg],
@@ -2553,7 +2724,11 @@ const recoverPackageTransition = async (optionsArg: {
2553
2724
  }
2554
2725
  } else {
2555
2726
  await addExactPackage(context, targetSpec, registryUrl);
2556
- targetTopology = await verifyPackageTransitionTopology(context, 'target-only');
2727
+ targetTopology = await verifyPackageTransitionTopology(
2728
+ context,
2729
+ 'target-only',
2730
+ targetVersion,
2731
+ );
2557
2732
  await optionsArg.coordinator.mutateTransaction(optionsArg.token, (current) => {
2558
2733
  if (current.version !== upgradePackageTransitionTransactionVersion) {
2559
2734
  throw new Error('The package-transition transaction changed format.');
@@ -2583,6 +2758,7 @@ const recoverPackageTransition = async (optionsArg: {
2583
2758
  targetInstallation,
2584
2759
  current.port,
2585
2760
  optionsArg.token,
2761
+ optionsArg.coordinator,
2586
2762
  );
2587
2763
  current = requirePackageTransitionTransaction(await optionsArg.coordinator.mutateTransaction(optionsArg.token, (candidate) => {
2588
2764
  if (candidate.version !== upgradePackageTransitionTransactionVersion) {
@@ -2599,7 +2775,7 @@ const recoverPackageTransition = async (optionsArg: {
2599
2775
  }
2600
2776
  let status = await queryControllerStatus(current.port, {
2601
2777
  packageName: current.targetPackageName,
2602
- packageVersion: current.targetVersion,
2778
+ packageVersion: upgradeTransactionTargetVersion(current)!,
2603
2779
  upgradeManagementVersion: current.targetManagementVersion,
2604
2780
  }).catch(() => undefined);
2605
2781
  if (!status) {
@@ -2614,7 +2790,12 @@ const recoverPackageTransition = async (optionsArg: {
2614
2790
  'A target controller is already starting or listening but cannot be adopted safely; refusing to launch a replacement.',
2615
2791
  );
2616
2792
  }
2617
- await startController(targetInstallation, current.port, optionsArg.token);
2793
+ await startController(
2794
+ targetInstallation,
2795
+ current.port,
2796
+ optionsArg.token,
2797
+ optionsArg.coordinator,
2798
+ );
2618
2799
  status = await queryUpgradeManagedController(
2619
2800
  current.port,
2620
2801
  targetControllerIdentity(current),
@@ -2630,7 +2811,7 @@ const recoverPackageTransition = async (optionsArg: {
2630
2811
  await optionsArg.coordinator.finishTransaction(
2631
2812
  optionsArg.token,
2632
2813
  true,
2633
- `Upgrade to ${current.targetPackageName}@${current.targetVersion} completed during bounded recovery.`,
2814
+ `Upgrade to ${current.targetPackageName}@${upgradeTransactionTargetVersion(current)} completed during bounded recovery.`,
2634
2815
  );
2635
2816
  return true;
2636
2817
  }
@@ -2652,7 +2833,12 @@ const recoverPackageTransition = async (optionsArg: {
2652
2833
  upgradeManagementVersion: transaction.sourceManagementVersion,
2653
2834
  }).catch(() => undefined);
2654
2835
  if (!status) {
2655
- await startController(sourceInstallation, transaction.port, optionsArg.token);
2836
+ await startController(
2837
+ sourceInstallation,
2838
+ transaction.port,
2839
+ optionsArg.token,
2840
+ optionsArg.coordinator,
2841
+ );
2656
2842
  status = await queryUpgradeManagedController(
2657
2843
  transaction.port,
2658
2844
  sourceControllerIdentity(transaction),
@@ -2725,7 +2911,12 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
2725
2911
  'A target controller is already starting or listening but cannot be adopted safely; refusing to launch a replacement.',
2726
2912
  );
2727
2913
  }
2728
- await startController(targetInstallation, transaction.port, optionsArg.token);
2914
+ await startController(
2915
+ targetInstallation,
2916
+ transaction.port,
2917
+ optionsArg.token,
2918
+ optionsArg.coordinator,
2919
+ );
2729
2920
  status = await queryUpgradeManagedController(
2730
2921
  transaction.port,
2731
2922
  {
@@ -2753,7 +2944,7 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
2753
2944
  const sourceInstallation = await restorePreviousInstallation({
2754
2945
  ...optionsArg.installation,
2755
2946
  packageVersion: transaction.sourceVersion,
2756
- });
2947
+ }, transaction.version === upgradeCoordinationVersion ? transaction.registryUrl : undefined);
2757
2948
  if (transaction.controllerWasRunning) {
2758
2949
  let status = await queryControllerStatus(transaction.port, {
2759
2950
  packageName: controllerPackageName,
@@ -2761,7 +2952,12 @@ const recoverStalledUpgradeUnderLock = async (optionsArg: {
2761
2952
  upgradeManagementVersion: controllerUpgradeManagementVersion,
2762
2953
  }).catch(() => undefined);
2763
2954
  if (!status) {
2764
- await startController(sourceInstallation, transaction.port, optionsArg.token);
2955
+ await startController(
2956
+ sourceInstallation,
2957
+ transaction.port,
2958
+ optionsArg.token,
2959
+ optionsArg.coordinator,
2960
+ );
2765
2961
  status = await queryUpgradeManagedController(
2766
2962
  transaction.port,
2767
2963
  {
@@ -2795,45 +2991,188 @@ interface IRecoverStalledUpgradeOptions {
2795
2991
  command: '__serve' | 'foreground' | 'upgrade';
2796
2992
  }
2797
2993
 
2798
- interface IRecoverStalledUpgradeDependencies {
2799
- launchRecoveryWorker?: typeof launchDetachedRecoveryWorker;
2994
+ export interface IUpgradeWorkerHandoffDependencies {
2800
2995
  terminateWorkerCandidate?: (
2801
2996
  candidateArg: IDetachedUpgradeWorker,
2802
2997
  ) => Promise<void>;
2803
- candidateCleanup?: ICandidateCleanupRetryDependencies;
2998
+ candidateCleanup?: ICandidateCleanupDependencies;
2804
2999
  }
2805
3000
 
2806
- export function recoverStalledUpgrade(
2807
- optionsArg: IRecoverStalledUpgradeOptions,
2808
- ): Promise<boolean>;
2809
- export async function recoverStalledUpgrade(
2810
- optionsArg: IRecoverStalledUpgradeOptions,
2811
- dependenciesArg: IRecoverStalledUpgradeDependencies = {},
2812
- ): Promise<boolean> {
2813
- await optionsArg.coordinator.terminateTransactionWorker(optionsArg.transaction);
2814
- let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>>;
3001
+ interface IRecoverStalledUpgradeDependencies extends IUpgradeWorkerHandoffDependencies {
3002
+ launchRecoveryWorker?: typeof launchDetachedRecoveryWorker;
3003
+ }
3004
+
3005
+ export interface IHandoffLaunchedUpgradeWorkerUnderLockOptions {
3006
+ coordinator: UpgradeCoordinator;
3007
+ token: string;
3008
+ installation: IPnpmGlobalInstallation;
3009
+ command: '__serve' | 'foreground' | 'upgrade';
3010
+ lock: UpgradeInstallationLock;
3011
+ candidate: IUpgradeWorkerLaunchCandidate;
3012
+ retainLockOnFailure?: boolean;
3013
+ }
3014
+
3015
+ export const handoffLaunchedUpgradeWorkerUnderLock = async (
3016
+ optionsArg: IHandoffLaunchedUpgradeWorkerUnderLockOptions,
3017
+ dependenciesArg: IUpgradeWorkerHandoffDependencies = {},
3018
+ ): Promise<IDetachedUpgradeWorker> => {
3019
+ let worker: IDetachedUpgradeWorker | undefined;
2815
3020
  try {
2816
- lock = await optionsArg.coordinator.acquireUpgradeLock({
3021
+ await optionsArg.coordinator.removeWorkerAcknowledgement(optionsArg.token);
3022
+ const launchedTransaction = await optionsArg.coordinator.recordWorkerLaunch(
3023
+ optionsArg.token,
3024
+ optionsArg.candidate.pid,
3025
+ optionsArg.candidate.cliPath,
3026
+ optionsArg.candidate.logFilePath,
3027
+ );
3028
+ const retainedWorker = launchedTransaction.worker;
3029
+ if (
3030
+ !retainedWorker
3031
+ || retainedWorker.pid !== optionsArg.candidate.pid
3032
+ || retainedWorker.cliPath !== optionsArg.candidate.cliPath
3033
+ || retainedWorker.logFilePath !== optionsArg.candidate.logFilePath
3034
+ || (optionsArg.candidate.processGroupId !== undefined
3035
+ && retainedWorker.processGroupId !== optionsArg.candidate.processGroupId)
3036
+ || (optionsArg.candidate.fingerprint !== undefined
3037
+ && retainedWorker.fingerprint !== optionsArg.candidate.fingerprint)
3038
+ ) throw new Error('The upgrade worker launch identity was not retained exactly.');
3039
+ worker = {
3040
+ pid: retainedWorker.pid,
3041
+ processGroupId: retainedWorker.processGroupId,
3042
+ fingerprint: retainedWorker.fingerprint,
3043
+ cliPath: retainedWorker.cliPath,
3044
+ logFilePath: optionsArg.candidate.logFilePath,
3045
+ };
3046
+ await optionsArg.coordinator.transferUpgradeLockToWorker({
3047
+ lock: optionsArg.lock,
2817
3048
  token: optionsArg.token,
2818
- cliPath: optionsArg.installation.cliPath,
2819
- command: optionsArg.command,
3049
+ worker,
2820
3050
  });
3051
+ await optionsArg.coordinator.acknowledgeWorker(optionsArg.token, worker);
3052
+ return {
3053
+ pid: worker.pid,
3054
+ processGroupId: worker.processGroupId,
3055
+ fingerprint: worker.fingerprint,
3056
+ cliPath: worker.cliPath,
3057
+ logFilePath: worker.logFilePath!,
3058
+ };
2821
3059
  } catch (errorArg) {
2822
- if (
2823
- errorArg instanceof Error
2824
- && (errorArg.message.includes('already running')
2825
- || errorArg.message.includes('initializing'))
2826
- ) return false;
3060
+ const errors: unknown[] = [errorArg];
3061
+ if (!worker) {
3062
+ const current = await optionsArg.coordinator.readTransaction(optionsArg.token).catch(() => undefined);
3063
+ if (
3064
+ current?.worker
3065
+ && current.worker.pid === optionsArg.candidate.pid
3066
+ && current.worker.cliPath === optionsArg.candidate.cliPath
3067
+ && current.worker.logFilePath === optionsArg.candidate.logFilePath
3068
+ ) worker = {
3069
+ pid: current.worker.pid,
3070
+ processGroupId: current.worker.processGroupId,
3071
+ fingerprint: current.worker.fingerprint,
3072
+ cliPath: current.worker.cliPath,
3073
+ logFilePath: optionsArg.candidate.logFilePath,
3074
+ };
3075
+ }
3076
+ if (!worker && (
3077
+ optionsArg.candidate.processGroupId !== undefined
3078
+ && optionsArg.candidate.fingerprint !== undefined
3079
+ )) {
3080
+ worker = {
3081
+ pid: optionsArg.candidate.pid,
3082
+ processGroupId: optionsArg.candidate.processGroupId,
3083
+ fingerprint: optionsArg.candidate.fingerprint,
3084
+ cliPath: optionsArg.candidate.cliPath,
3085
+ logFilePath: optionsArg.candidate.logFilePath,
3086
+ };
3087
+ }
3088
+ if (!worker) {
3089
+ throw new UpgradeWorkerCandidateDrainageError(
3090
+ 'The upgrade worker handoff failed before its cleanup identity could be retained.',
3091
+ errorArg,
3092
+ );
3093
+ }
3094
+ const cleanupWorker = worker;
3095
+ try {
3096
+ await cleanupCandidateUntilDrained(
3097
+ async () => await (dependenciesArg.terminateWorkerCandidate
3098
+ ?? ((candidateArg) => optionsArg.coordinator.terminateUpgradeWorkerCandidate(candidateArg)))(
3099
+ cleanupWorker,
3100
+ ),
3101
+ dependenciesArg.candidateCleanup,
3102
+ cleanupWorker.pid,
3103
+ );
3104
+ } catch (cleanupErrorArg) {
3105
+ errors.push(cleanupErrorArg);
3106
+ }
3107
+ if (errors.length > 1) {
3108
+ throw new AggregateError(
3109
+ errors,
3110
+ 'Upgrade worker handoff failed and candidate drainage is unproven; ownership was retained.',
3111
+ );
3112
+ }
3113
+ const acquireCallerLock = async (): Promise<UpgradeInstallationLock> => {
3114
+ try {
3115
+ await optionsArg.lock.assertOwned();
3116
+ return optionsArg.lock;
3117
+ } catch {
3118
+ return await optionsArg.coordinator.acquireUpgradeLock({
3119
+ token: optionsArg.token,
3120
+ cliPath: optionsArg.installation.cliPath,
3121
+ command: optionsArg.command,
3122
+ });
3123
+ }
3124
+ };
3125
+ if (optionsArg.retainLockOnFailure) {
3126
+ try {
3127
+ const recoveryLock = await acquireCallerLock();
3128
+ await recoveryLock.assertOwned();
3129
+ throw new UpgradeWorkerHandoffError(
3130
+ 'The upgrade worker handoff failed; exclusive ownership was retained for recovery.',
3131
+ errors.length === 1
3132
+ ? errorArg
3133
+ : new AggregateError(errors, 'Upgrade worker handoff and candidate cleanup failed.'),
3134
+ recoveryLock,
3135
+ );
3136
+ } catch (lockRetentionErrorArg) {
3137
+ if (lockRetentionErrorArg instanceof UpgradeWorkerHandoffError) {
3138
+ throw lockRetentionErrorArg;
3139
+ }
3140
+ errors.push(lockRetentionErrorArg);
3141
+ }
3142
+ }
3143
+ try {
3144
+ await (await acquireCallerLock()).release();
3145
+ } catch (lockCleanupErrorArg) {
3146
+ errors.push(lockCleanupErrorArg);
3147
+ }
3148
+ if (errors.length > 1) {
3149
+ throw new AggregateError(
3150
+ errors,
3151
+ 'Upgrade worker handoff failed and cleanup was incomplete.',
3152
+ );
3153
+ }
2827
3154
  throw errorArg;
2828
3155
  }
3156
+ };
3157
+
3158
+ export interface IHandoffStalledUpgradeRecoveryUnderLockOptions {
3159
+ coordinator: UpgradeCoordinator;
3160
+ token: string;
3161
+ installation: IPnpmGlobalInstallation;
3162
+ command: '__serve' | 'foreground' | 'upgrade';
3163
+ lock: UpgradeInstallationLock;
3164
+ }
2829
3165
 
2830
- let candidate: IDetachedUpgradeWorker | undefined;
2831
- let lockTransferred = false;
3166
+ export const handoffStalledUpgradeRecoveryUnderLock = async (
3167
+ optionsArg: IHandoffStalledUpgradeRecoveryUnderLockOptions,
3168
+ dependenciesArg: IRecoverStalledUpgradeDependencies = {},
3169
+ ): Promise<IDetachedUpgradeWorker | undefined> => {
3170
+ let candidate: IDetachedUpgradeWorker;
2832
3171
  try {
2833
3172
  const transaction = await optionsArg.coordinator.readTransaction(optionsArg.token);
2834
3173
  if (transaction.terminal) {
2835
- await lock.release();
2836
- return true;
3174
+ await optionsArg.lock.release();
3175
+ return undefined;
2837
3176
  }
2838
3177
  const recoveryInstallation = await resolveAuthoritativeRecoveryInstallation(
2839
3178
  transaction,
@@ -2846,76 +3185,61 @@ export async function recoverStalledUpgrade(
2846
3185
  gracePeriodMs: transaction.gracePeriodMs,
2847
3186
  continueSessions: transaction.continueSessions,
2848
3187
  };
3188
+ await optionsArg.coordinator.removeWorkerAcknowledgement(optionsArg.token);
2849
3189
  candidate = await (dependenciesArg.launchRecoveryWorker ?? launchDetachedRecoveryWorker)({
2850
3190
  installation: recoveryInstallation,
2851
3191
  payload,
2852
3192
  });
2853
- const launchedTransaction = await optionsArg.coordinator.recordWorkerLaunch(
2854
- optionsArg.token,
2855
- candidate.pid,
2856
- candidate.cliPath,
2857
- candidate.logFilePath,
2858
- );
2859
- const worker = launchedTransaction.worker;
2860
- if (
2861
- !worker
2862
- || worker.pid !== candidate.pid
2863
- || worker.processGroupId !== candidate.processGroupId
2864
- || worker.fingerprint !== candidate.fingerprint
2865
- || worker.cliPath !== candidate.cliPath
2866
- || worker.logFilePath !== candidate.logFilePath
2867
- ) throw new Error('The recovery worker launch identity was not retained exactly.');
2868
- await optionsArg.coordinator.transferUpgradeLockToWorker({
2869
- lock,
2870
- token: optionsArg.token,
2871
- worker,
2872
- });
2873
- lockTransferred = true;
2874
- await optionsArg.coordinator.acknowledgeWorker(optionsArg.token);
2875
- return true;
2876
3193
  } catch (errorArg) {
2877
- const errors: unknown[] = [errorArg];
2878
- if (candidate) {
2879
- await retryCandidateCleanupUntilDrained(
2880
- async () => await (dependenciesArg.terminateWorkerCandidate
2881
- ?? ((candidateArg) => optionsArg.coordinator.terminateUpgradeWorkerCandidate(candidateArg)))(
2882
- candidate!,
2883
- ),
2884
- dependenciesArg.candidateCleanup,
2885
- candidate.pid,
2886
- );
2887
- }
3194
+ if (errorArg instanceof UpgradeWorkerCandidateDrainageError) throw errorArg;
2888
3195
  try {
2889
- if (!lockTransferred) {
2890
- try {
2891
- await lock.release();
2892
- } catch {
2893
- const replacementLock = await optionsArg.coordinator.acquireUpgradeLock({
2894
- token: optionsArg.token,
2895
- cliPath: optionsArg.installation.cliPath,
2896
- command: optionsArg.command,
2897
- });
2898
- await replacementLock.release();
2899
- }
2900
- } else {
2901
- const replacementLock = await optionsArg.coordinator.acquireUpgradeLock({
2902
- token: optionsArg.token,
2903
- cliPath: optionsArg.installation.cliPath,
2904
- command: optionsArg.command,
2905
- });
2906
- await replacementLock.release();
2907
- }
3196
+ await optionsArg.lock.release();
2908
3197
  } catch (lockCleanupErrorArg) {
2909
- errors.push(lockCleanupErrorArg);
2910
- }
2911
- if (errors.length > 1) {
2912
3198
  throw new AggregateError(
2913
- errors,
2914
- 'Stalled upgrade recovery handoff failed and cleanup was incomplete.',
3199
+ [errorArg, lockCleanupErrorArg],
3200
+ 'Stalled upgrade recovery launch failed and its lock could not be released.',
2915
3201
  );
2916
3202
  }
2917
3203
  throw errorArg;
2918
3204
  }
3205
+ return await handoffLaunchedUpgradeWorkerUnderLock({
3206
+ ...optionsArg,
3207
+ candidate,
3208
+ }, dependenciesArg);
3209
+ };
3210
+
3211
+ export function recoverStalledUpgrade(
3212
+ optionsArg: IRecoverStalledUpgradeOptions,
3213
+ ): Promise<boolean>;
3214
+ export async function recoverStalledUpgrade(
3215
+ optionsArg: IRecoverStalledUpgradeOptions,
3216
+ dependenciesArg: IRecoverStalledUpgradeDependencies = {},
3217
+ ): Promise<boolean> {
3218
+ await optionsArg.coordinator.terminateTransactionWorker(optionsArg.transaction);
3219
+ let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>>;
3220
+ try {
3221
+ lock = await optionsArg.coordinator.acquireUpgradeLock({
3222
+ token: optionsArg.token,
3223
+ cliPath: optionsArg.installation.cliPath,
3224
+ command: optionsArg.command,
3225
+ });
3226
+ } catch (errorArg) {
3227
+ if (
3228
+ errorArg instanceof Error
3229
+ && (errorArg.message.includes('already running')
3230
+ || errorArg.message.includes('initializing'))
3231
+ ) return false;
3232
+ throw errorArg;
3233
+ }
3234
+
3235
+ await handoffStalledUpgradeRecoveryUnderLock({
3236
+ coordinator: optionsArg.coordinator,
3237
+ token: optionsArg.token,
3238
+ installation: optionsArg.installation,
3239
+ command: optionsArg.command,
3240
+ lock,
3241
+ }, dependenciesArg);
3242
+ return true;
2919
3243
  }
2920
3244
 
2921
3245
  export const createUpgradeWorkerPayload = (optionsArg: {