agl 21.0.2 → 22.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/changelog.md +9 -0
- package/dist_serve/bundle.js +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.aglhome.d.ts +25 -0
- package/dist_ts/classes.aglhome.js +65 -0
- package/dist_ts/classes.authmodels.js +7 -12
- package/dist_ts/classes.authstore.js +2 -29
- package/dist_ts/classes.cli.js +93 -35
- package/dist_ts/classes.config.d.ts +2 -11
- package/dist_ts/classes.config.js +14 -80
- package/dist_ts/classes.controller.d.ts +4 -1
- package/dist_ts/classes.controller.js +64 -26
- package/dist_ts/classes.embeddeddb.js +6 -5
- package/dist_ts/classes.gitreversion.js +3 -2
- package/dist_ts/classes.upgradecoordinator.d.ts +6 -1
- package/dist_ts/classes.upgradecoordinator.js +588 -177
- package/dist_ts/classes.upgradetransaction.js +46 -12
- package/dist_ts/classes.uploadmanager.d.ts +12 -0
- package/dist_ts/classes.uploadmanager.js +204 -2
- package/dist_ts/constants.upgradeenvironment.d.ts +2 -0
- package/dist_ts/constants.upgradeenvironment.js +3 -0
- package/dist_ts/functions.controllerdataroot.d.ts +4 -5
- package/dist_ts/functions.controllerdataroot.js +4 -29
- package/dist_ts/functions.embeddeddb.d.ts +1 -1
- package/dist_ts/functions.embeddeddb.js +8 -3
- package/dist_ts/functions.runtimeenvironment.js +2 -1
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.config.d.ts +5 -7
- 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/v23_aglhome.d.ts +91 -0
- package/dist_ts_migration/v23_aglhome.js +1775 -0
- package/dist_ts_migration/v23_runtimeconfig.d.ts +11 -0
- package/dist_ts_migration/v23_runtimeconfig.js +87 -0
- package/dist_ts_migration/v2_controllerdataroot.d.ts +5 -0
- package/dist_ts_migration/v2_controllerdataroot.js +176 -15
- package/package.json +1 -1
- package/readme.md +116 -35
- package/readme.plan.md +24 -9
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.aglhome.ts +116 -0
- package/ts/classes.authmodels.ts +5 -13
- package/ts/classes.authstore.ts +1 -39
- package/ts/classes.cli.ts +92 -34
- package/ts/classes.config.ts +20 -117
- package/ts/classes.controller.ts +92 -26
- package/ts/classes.embeddeddb.ts +5 -4
- package/ts/classes.gitreversion.ts +3 -2
- package/ts/classes.upgradecoordinator.ts +668 -191
- package/ts/classes.upgradetransaction.ts +46 -11
- package/ts/classes.uploadmanager.ts +231 -1
- package/ts/constants.upgradeenvironment.ts +2 -0
- package/ts/functions.controllerdataroot.ts +11 -47
- package/ts/functions.embeddeddb.ts +13 -2
- package/ts/functions.runtimeenvironment.ts +1 -0
- package/ts/index.ts +1 -0
- package/ts/interfaces.config.ts +5 -7
- package/ts_migration/classes.documentmigrationrunner.ts +2 -0
- package/ts_migration/index.ts +2 -0
- package/ts_migration/v23_aglhome.ts +2101 -0
- package/ts_migration/v23_runtimeconfig.ts +112 -0
- package/ts_migration/v2_controllerdataroot.ts +174 -16
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as plugins from '../ts/plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
assertControllerAuthDocument,
|
|
4
|
+
ControllerAuthModel,
|
|
5
|
+
} from '../ts/classes.authmodels.js';
|
|
6
|
+
import { AuthError, type IControllerAuthDocument } from '../ts/interfaces.auth.js';
|
|
7
|
+
|
|
8
|
+
const migrationPageLimit = 128;
|
|
9
|
+
const assertRuntimeConfigDocument: (
|
|
10
|
+
valueArg: unknown,
|
|
11
|
+
) => asserts valueArg is IControllerAuthDocument = assertControllerAuthDocument;
|
|
12
|
+
|
|
13
|
+
const persistedBodyFromRawDocument = (
|
|
14
|
+
valueArg: Record<string, unknown>,
|
|
15
|
+
): Record<string, unknown> => {
|
|
16
|
+
const body = { ...valueArg };
|
|
17
|
+
delete body._id;
|
|
18
|
+
delete body._smartdataRevision;
|
|
19
|
+
return body;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isAbsoluteNormalizedPath = (valueArg: unknown): valueArg is string => (
|
|
23
|
+
typeof valueArg === 'string'
|
|
24
|
+
&& valueArg.length > 0
|
|
25
|
+
&& plugins.path.isAbsolute(valueArg)
|
|
26
|
+
&& plugins.path.normalize(valueArg) === valueArg
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const migrateV23RuntimeConfigDocument = (
|
|
30
|
+
valueArg: Record<string, unknown>,
|
|
31
|
+
): { document: IControllerAuthDocument; migrated: boolean } => {
|
|
32
|
+
const config = valueArg.config;
|
|
33
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
34
|
+
throw new Error('Invalid legacy controller runtime configuration.');
|
|
35
|
+
}
|
|
36
|
+
const configRecord = config as Record<string, unknown>;
|
|
37
|
+
if (!Object.hasOwn(configRecord, 'workspaceDirectory')) {
|
|
38
|
+
assertRuntimeConfigDocument(valueArg);
|
|
39
|
+
return { document: valueArg, migrated: false };
|
|
40
|
+
}
|
|
41
|
+
if (!isAbsoluteNormalizedPath(configRecord.workspaceDirectory)) {
|
|
42
|
+
throw new Error('Legacy controller workspace is not an absolute normalized path.');
|
|
43
|
+
}
|
|
44
|
+
const projectsRoot = Object.hasOwn(configRecord, 'projectsRoot')
|
|
45
|
+
? configRecord.projectsRoot
|
|
46
|
+
: configRecord.workspaceDirectory;
|
|
47
|
+
if (!isAbsoluteNormalizedPath(projectsRoot)) {
|
|
48
|
+
throw new Error('Legacy controller projects root is not an absolute normalized path.');
|
|
49
|
+
}
|
|
50
|
+
const migratedConfig: Record<string, unknown> = { ...configRecord, projectsRoot };
|
|
51
|
+
delete migratedConfig.workspaceDirectory;
|
|
52
|
+
const migrated: Record<string, unknown> = { ...valueArg, config: migratedConfig };
|
|
53
|
+
assertRuntimeConfigDocument(migrated);
|
|
54
|
+
return { document: migrated, migrated: true };
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export class RuntimeConfigV23Migration {
|
|
58
|
+
constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {}
|
|
59
|
+
|
|
60
|
+
public async run(): Promise<void> {
|
|
61
|
+
if (!this.database) {
|
|
62
|
+
throw new AuthError('not_initialized', 'The authentication store database is unavailable.');
|
|
63
|
+
}
|
|
64
|
+
const collection = ControllerAuthModel.collection.mongoDbCollection;
|
|
65
|
+
let lastId: plugins.smartdata.TStoredDocument<IControllerAuthDocument>['_id'] | undefined;
|
|
66
|
+
while (true) {
|
|
67
|
+
const cursor = collection.find(
|
|
68
|
+
lastId ? { _id: { $gt: lastId } } : {},
|
|
69
|
+
).sort({ _id: 1 }).limit(migrationPageLimit);
|
|
70
|
+
let documents: Awaited<ReturnType<typeof cursor.toArray>>;
|
|
71
|
+
try {
|
|
72
|
+
documents = await cursor.toArray();
|
|
73
|
+
} finally {
|
|
74
|
+
await cursor.close();
|
|
75
|
+
}
|
|
76
|
+
for (const raw of documents) {
|
|
77
|
+
const migration = migrateV23RuntimeConfigDocument(
|
|
78
|
+
persistedBodyFromRawDocument(raw),
|
|
79
|
+
);
|
|
80
|
+
if (!migration.migrated) continue;
|
|
81
|
+
const selector = raw._smartdataRevision === undefined
|
|
82
|
+
? { _id: raw._id, _smartdataRevision: { $exists: false } }
|
|
83
|
+
: { _id: raw._id, _smartdataRevision: raw._smartdataRevision };
|
|
84
|
+
const replaced = await collection.findOneAndReplace(
|
|
85
|
+
selector,
|
|
86
|
+
{ _id: raw._id, ...migration.document, _smartdataRevision: plugins.crypto.randomUUID() },
|
|
87
|
+
{ returnDocument: 'after', includeResultMetadata: false, upsert: false },
|
|
88
|
+
);
|
|
89
|
+
if (replaced) continue;
|
|
90
|
+
const concurrent = await collection.findOne({ _id: raw._id });
|
|
91
|
+
if (!concurrent) {
|
|
92
|
+
throw new AuthError(
|
|
93
|
+
'concurrent_change',
|
|
94
|
+
'The runtime configuration migration changed concurrently.',
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
const reconciled = migrateV23RuntimeConfigDocument(
|
|
98
|
+
persistedBodyFromRawDocument(concurrent),
|
|
99
|
+
);
|
|
100
|
+
if (reconciled.migrated) {
|
|
101
|
+
throw new AuthError(
|
|
102
|
+
'concurrent_change',
|
|
103
|
+
'The runtime configuration migration changed concurrently.',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const last = documents.at(-1);
|
|
108
|
+
if (!last || documents.length < migrationPageLimit) break;
|
|
109
|
+
lastId = last._id;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -31,6 +31,7 @@ const maximumUpgradeLogs = 64;
|
|
|
31
31
|
const maximumLockArtifacts = 64;
|
|
32
32
|
const maximumLockAttempts = 200;
|
|
33
33
|
const lockWaitMilliseconds = 50;
|
|
34
|
+
const lockGuardInitializationGraceMs = 30_000;
|
|
34
35
|
|
|
35
36
|
const credentialHashPattern = /^[a-f0-9]{64}$/;
|
|
36
37
|
const digestPattern = /^[a-f0-9]{64}$/;
|
|
@@ -39,6 +40,7 @@ const decimalPattern = /^(0|[1-9][0-9]*)$/;
|
|
|
39
40
|
const controllerLogPattern = /^controller-([1-9][0-9]{0,4})\.log(?:\.old)?$/;
|
|
40
41
|
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$/;
|
|
41
42
|
const embeddedDatabaseJournalTemporaryPattern = /^\.smartdb-location-migration\.json\.[1-9][0-9]*-[a-f0-9]{16}\.tmp$/;
|
|
43
|
+
const lockTemporarySuffixPattern = /^\.tmp-([0-9]+)-([1-9][0-9]*)-([a-f0-9]{64})$/;
|
|
42
44
|
|
|
43
45
|
export type TControllerDataRootMigrationPhase =
|
|
44
46
|
| 'legacy-authoritative'
|
|
@@ -116,6 +118,7 @@ export interface IControllerDataRootMigrationOptions {
|
|
|
116
118
|
newEmbeddedSocketPath: string;
|
|
117
119
|
invokerPid: number;
|
|
118
120
|
listDataWriterProcesses: () => Promise<readonly IControllerDataWriterProcessRecord[]>;
|
|
121
|
+
preserveEmbeddedDataDirectory?: boolean;
|
|
119
122
|
isSocketListening?: (socketPathArg: string) => Promise<boolean>;
|
|
120
123
|
relocateCredentialStore?: (
|
|
121
124
|
intentArg: Readonly<IControllerCredentialRelocationIntent>,
|
|
@@ -729,6 +732,14 @@ const closeKernelStoreConfirmed = async (
|
|
|
729
732
|
|
|
730
733
|
const retainedEmbeddedDatabaseMigrations = new Set<EmbeddedDatabaseLocationMigration>();
|
|
731
734
|
const retainedMigratedLocalSmartDbs = new Set<plugins.smartdb.LocalSmartDb>();
|
|
735
|
+
const retainedCredentialKernelStores = new Set<plugins.smartsecret.SmartSecretKernelStore>();
|
|
736
|
+
|
|
737
|
+
const drainRetainedCredentialKernelStores = async (): Promise<void> => {
|
|
738
|
+
for (const kernelStore of retainedCredentialKernelStores) {
|
|
739
|
+
await closeKernelStoreConfirmed(kernelStore);
|
|
740
|
+
retainedCredentialKernelStores.delete(kernelStore);
|
|
741
|
+
}
|
|
742
|
+
};
|
|
732
743
|
|
|
733
744
|
const stopLocalSmartDbConfirmed = async (
|
|
734
745
|
localDbArg: plugins.smartdb.LocalSmartDb,
|
|
@@ -829,6 +840,7 @@ const defaultRelocateCredentialStore = async (
|
|
|
829
840
|
signalArg?: AbortSignal,
|
|
830
841
|
): Promise<void> => {
|
|
831
842
|
signalArg?.throwIfAborted();
|
|
843
|
+
await drainRetainedCredentialKernelStores();
|
|
832
844
|
let kernelStore: plugins.smartsecret.SmartSecretKernelStore | undefined;
|
|
833
845
|
let sealedStore: plugins.smartsecret.SmartSecretSealedFileStore | undefined;
|
|
834
846
|
let operationError: unknown;
|
|
@@ -836,6 +848,7 @@ const defaultRelocateCredentialStore = async (
|
|
|
836
848
|
kernelStore = await plugins.smartsecret.SmartSecretKernelStore.create({
|
|
837
849
|
service: intentArg.service,
|
|
838
850
|
});
|
|
851
|
+
retainedCredentialKernelStores.add(kernelStore);
|
|
839
852
|
signalArg?.throwIfAborted();
|
|
840
853
|
sealedStore = await plugins.smartsecret.SmartSecretSealedFileStore.relocate({
|
|
841
854
|
kernelStore,
|
|
@@ -864,6 +877,7 @@ const defaultRelocateCredentialStore = async (
|
|
|
864
877
|
if (kernelStore) {
|
|
865
878
|
try {
|
|
866
879
|
await closeKernelStoreConfirmed(kernelStore);
|
|
880
|
+
retainedCredentialKernelStores.delete(kernelStore);
|
|
867
881
|
} catch (errorArg) {
|
|
868
882
|
operationError = operationError
|
|
869
883
|
? new AggregateError(
|
|
@@ -952,6 +966,56 @@ export class ControllerDataRootMigrationRunner {
|
|
|
952
966
|
return { directoryPath: this.newRoot, createDirectory: true };
|
|
953
967
|
}
|
|
954
968
|
|
|
969
|
+
/** Validates all authoritative v2 state without creating roots, locks, or journals. */
|
|
970
|
+
public async preflight(): Promise<void> {
|
|
971
|
+
this.options.signal?.throwIfAborted();
|
|
972
|
+
if (this.options.invokerPid !== process.pid) {
|
|
973
|
+
throw new Error('Controller data-root migration invoker PID is not the current process.');
|
|
974
|
+
}
|
|
975
|
+
if (!await this.validateCommonParent(false)) return;
|
|
976
|
+
const journal = await this.readJournal();
|
|
977
|
+
if (journal) {
|
|
978
|
+
if (journal.phase === 'target-committed') {
|
|
979
|
+
this.assertJournalRootBinding(journal);
|
|
980
|
+
await this.assertTargetIdentity(journal);
|
|
981
|
+
await this.assertTargetMarker(journal.targetCreationNonce);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
this.assertJournalMatches(journal);
|
|
985
|
+
await this.assertSourceIdentity(journal);
|
|
986
|
+
await this.inspectLegacyRootInventory(journal);
|
|
987
|
+
await this.validateCredentialLocations(journal, false);
|
|
988
|
+
if (journal.phase !== 'legacy-authoritative') {
|
|
989
|
+
await this.inspectTargetStagingInventory(journal);
|
|
990
|
+
}
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
const sourceIdentity = await readDirectoryIdentity(this.oldRoot, 'Legacy controller root');
|
|
994
|
+
const targetIdentity = await readDirectoryIdentity(
|
|
995
|
+
this.newRoot,
|
|
996
|
+
'Target controller root',
|
|
997
|
+
0o700,
|
|
998
|
+
);
|
|
999
|
+
const targetContainsPreservedDatabase = targetIdentity
|
|
1000
|
+
? await this.targetContainsOnlyPreservedDatabase()
|
|
1001
|
+
: false;
|
|
1002
|
+
if (sourceIdentity && targetIdentity && !targetContainsPreservedDatabase) {
|
|
1003
|
+
throw new Error('Both unjournaled controller product roots exist; refusing to choose authority.');
|
|
1004
|
+
}
|
|
1005
|
+
if (sourceIdentity) {
|
|
1006
|
+
await this.inspectLegacyRootInventory();
|
|
1007
|
+
await this.initialCredentialTuples(true);
|
|
1008
|
+
await this.initialDatabaseJournal();
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (targetIdentity) {
|
|
1012
|
+
const entries = await this.readBoundedDirectory(this.newRoot, 'Target controller root');
|
|
1013
|
+
if (entries.length !== 0 && !targetContainsPreservedDatabase) {
|
|
1014
|
+
throw new Error('An unjournaled target controller root is not empty.');
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
955
1019
|
public async run(): Promise<IControllerDataRootMigrationResult> {
|
|
956
1020
|
this.options.signal?.throwIfAborted();
|
|
957
1021
|
if (this.options.invokerPid !== process.pid) {
|
|
@@ -1029,6 +1093,23 @@ export class ControllerDataRootMigrationRunner {
|
|
|
1029
1093
|
requireString(value.legacyEmbeddedDataDirectory, 'Historical embedded database path'),
|
|
1030
1094
|
'Historical embedded database path',
|
|
1031
1095
|
);
|
|
1096
|
+
if (this.options.preserveEmbeddedDataDirectory === true) {
|
|
1097
|
+
if (historicalDirectory !== null) {
|
|
1098
|
+
throw new Error('An explicit embedded database override must not include a historical path.');
|
|
1099
|
+
}
|
|
1100
|
+
return {
|
|
1101
|
+
mode: 'override',
|
|
1102
|
+
configurationDigestSha256: databaseConfigurationDigest({
|
|
1103
|
+
mode: 'override',
|
|
1104
|
+
mongoDbName,
|
|
1105
|
+
embeddedDataDirectory,
|
|
1106
|
+
}),
|
|
1107
|
+
oldDirectory: null,
|
|
1108
|
+
targetDirectory: null,
|
|
1109
|
+
historicalDirectory: null,
|
|
1110
|
+
effectiveConfig: { mongoDbName, embeddedDataDirectory },
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1032
1113
|
if (embeddedDataDirectory !== oldDirectory && embeddedDataDirectory !== targetDirectory) {
|
|
1033
1114
|
if (historicalDirectory !== null) {
|
|
1034
1115
|
throw new Error('An explicit embedded database override must not include a historical path.');
|
|
@@ -1318,6 +1399,24 @@ export class ControllerDataRootMigrationRunner {
|
|
|
1318
1399
|
};
|
|
1319
1400
|
}
|
|
1320
1401
|
|
|
1402
|
+
private preservedTargetDatabasePath(): string | undefined {
|
|
1403
|
+
if (
|
|
1404
|
+
this.databaseConfiguration.mode !== 'override'
|
|
1405
|
+
|| this.databaseConfiguration.effectiveConfig.embeddedDataDirectory
|
|
1406
|
+
!== plugins.path.join(this.newRoot, 'smartdb')
|
|
1407
|
+
) return undefined;
|
|
1408
|
+
return this.databaseConfiguration.effectiveConfig.embeddedDataDirectory;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
private async targetContainsOnlyPreservedDatabase(): Promise<boolean> {
|
|
1412
|
+
const databasePath = this.preservedTargetDatabasePath();
|
|
1413
|
+
if (!databasePath) return false;
|
|
1414
|
+
const names = await this.readBoundedDirectory(this.newRoot, 'Target controller root');
|
|
1415
|
+
if (names.length !== 1 || names[0] !== 'smartdb') return false;
|
|
1416
|
+
await this.requirePrivateNode(databasePath, 'directory', 'Explicit target database override');
|
|
1417
|
+
return true;
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1321
1420
|
private async assertNoConflictingWriters(
|
|
1322
1421
|
invokerIdentityArg: IControllerProcessIdentity,
|
|
1323
1422
|
): Promise<void> {
|
|
@@ -1349,13 +1448,16 @@ export class ControllerDataRootMigrationRunner {
|
|
|
1349
1448
|
private async createInitialJournal(): Promise<IControllerDataRootMigrationJournal> {
|
|
1350
1449
|
const sourceIdentity = await readDirectoryIdentity(this.oldRoot, 'Legacy controller root');
|
|
1351
1450
|
const targetIdentity = await readDirectoryIdentity(this.newRoot, 'Target controller root', 0o700);
|
|
1352
|
-
|
|
1451
|
+
const targetContainsPreservedDatabase = targetIdentity
|
|
1452
|
+
? await this.targetContainsOnlyPreservedDatabase()
|
|
1453
|
+
: false;
|
|
1454
|
+
if (sourceIdentity && targetIdentity && !targetContainsPreservedDatabase) {
|
|
1353
1455
|
throw new Error('Both unjournaled controller product roots exist; refusing to choose authority.');
|
|
1354
1456
|
}
|
|
1355
1457
|
if (sourceIdentity) await this.inspectLegacyRootInventory();
|
|
1356
1458
|
if (targetIdentity) {
|
|
1357
1459
|
const entries = await this.readBoundedDirectory(this.newRoot, 'Target controller root');
|
|
1358
|
-
if (entries.length !== 0) {
|
|
1460
|
+
if (entries.length !== 0 && !targetContainsPreservedDatabase) {
|
|
1359
1461
|
throw new Error('An unjournaled target controller root is not empty.');
|
|
1360
1462
|
}
|
|
1361
1463
|
}
|
|
@@ -1666,10 +1768,9 @@ export class ControllerDataRootMigrationRunner {
|
|
|
1666
1768
|
if (!targetIdentity) throw new Error('Target controller root was not created.');
|
|
1667
1769
|
}
|
|
1668
1770
|
const entries = await this.readBoundedDirectory(this.newRoot, 'Target controller root');
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
) {
|
|
1771
|
+
const allowedEntries = new Set([markerFileName]);
|
|
1772
|
+
if (this.preservedTargetDatabasePath()) allowedEntries.add('smartdb');
|
|
1773
|
+
if (entries.some((entry) => !allowedEntries.has(entry))) {
|
|
1673
1774
|
throw new Error('Legacy-authoritative retry found an unexpected staged target entry.');
|
|
1674
1775
|
}
|
|
1675
1776
|
await this.ensureTargetMarker(journalArg.targetCreationNonce);
|
|
@@ -2321,26 +2422,83 @@ export class ControllerDataRootMigrationRunner {
|
|
|
2321
2422
|
let directoryChanged = false;
|
|
2322
2423
|
for (const name of artifacts.temporaries) {
|
|
2323
2424
|
const path = plugins.path.join(this.commonParent, name);
|
|
2324
|
-
const
|
|
2325
|
-
if (!
|
|
2326
|
-
|
|
2425
|
+
const match = lockTemporarySuffixPattern.exec(name.slice(plugins.path.basename(this.lockPath).length));
|
|
2426
|
+
if (!match) throw new Error(`Controller migration lock temporary name is invalid: ${name}`);
|
|
2427
|
+
const uid = Number(match[1]);
|
|
2428
|
+
const pid = Number(match[2]);
|
|
2429
|
+
if (!Number.isSafeInteger(uid) || uid !== currentUid() || !Number.isSafeInteger(pid)) {
|
|
2430
|
+
throw new Error(`Controller migration lock temporary binding is invalid: ${name}`);
|
|
2327
2431
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2432
|
+
let stats: plugins.fs.BigIntStats;
|
|
2433
|
+
try {
|
|
2434
|
+
stats = await plugins.fs.promises.lstat(path, { bigint: true });
|
|
2435
|
+
} catch (errorArg) {
|
|
2436
|
+
if (isMissingError(errorArg)) continue;
|
|
2437
|
+
throw errorArg;
|
|
2438
|
+
}
|
|
2439
|
+
if (
|
|
2440
|
+
!stats.isFile()
|
|
2441
|
+
|| stats.isSymbolicLink()
|
|
2442
|
+
|| stats.uid !== BigInt(currentUid())
|
|
2443
|
+
|| (stats.nlink !== 1n && stats.nlink !== 2n)
|
|
2444
|
+
|| Number(stats.mode & 0o777n) !== 0o600
|
|
2445
|
+
|| stats.size > BigInt(maximumLockBytes)
|
|
2446
|
+
) throw new Error(`Controller migration lock temporary artifact is unsafe: ${path}`);
|
|
2447
|
+
const identity = pathIdentity(stats);
|
|
2448
|
+
let snapshot: ILockSnapshot | undefined;
|
|
2449
|
+
try {
|
|
2450
|
+
snapshot = await this.readLockSnapshot(path);
|
|
2451
|
+
} catch (errorArg) {
|
|
2452
|
+
if (await readControllerProcessIdentity(pid)) continue;
|
|
2453
|
+
await unlinkExactPath(path, identity);
|
|
2330
2454
|
directoryChanged = true;
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (!snapshot) continue;
|
|
2458
|
+
if (!identitiesEqual(snapshot.identity, identity)) {
|
|
2459
|
+
throw new Error(`Controller migration lock temporary changed during inspection: ${path}`);
|
|
2460
|
+
}
|
|
2461
|
+
if (
|
|
2462
|
+
snapshot.owner.uid !== uid
|
|
2463
|
+
|| snapshot.owner.pid !== pid
|
|
2464
|
+
|| snapshot.owner.nonce !== match[3]
|
|
2465
|
+
) {
|
|
2466
|
+
throw new Error(`Controller migration lock temporary owner binding is invalid: ${path}`);
|
|
2331
2467
|
}
|
|
2468
|
+
if (await this.lockOwnerIsLive(snapshot.owner)) continue;
|
|
2469
|
+
await unlinkExactPath(path, snapshot.identity);
|
|
2470
|
+
directoryChanged = true;
|
|
2332
2471
|
}
|
|
2333
2472
|
for (const name of artifacts.guards) {
|
|
2334
2473
|
const path = plugins.path.join(this.commonParent, name);
|
|
2474
|
+
let stats: plugins.fs.BigIntStats;
|
|
2475
|
+
try {
|
|
2476
|
+
stats = await plugins.fs.promises.lstat(path, { bigint: true });
|
|
2477
|
+
} catch (errorArg) {
|
|
2478
|
+
if (isMissingError(errorArg)) continue;
|
|
2479
|
+
throw errorArg;
|
|
2480
|
+
}
|
|
2481
|
+
if (
|
|
2482
|
+
!stats.isFile()
|
|
2483
|
+
|| stats.isSymbolicLink()
|
|
2484
|
+
|| stats.uid !== BigInt(currentUid())
|
|
2485
|
+
|| (stats.nlink !== 1n && stats.nlink !== 2n)
|
|
2486
|
+
|| Number(stats.mode & 0o777n) !== 0o600
|
|
2487
|
+
|| stats.size < 2n
|
|
2488
|
+
|| stats.size > BigInt(maximumLockBytes)
|
|
2489
|
+
) throw new Error(`Controller migration lock guard is unsafe: ${path}`);
|
|
2335
2490
|
const guard = await this.readLockSnapshot(path);
|
|
2336
|
-
if (!guard)
|
|
2491
|
+
if (!guard) continue;
|
|
2492
|
+
if (!identitiesEqual(guard.identity, pathIdentity(stats))) {
|
|
2493
|
+
throw new Error('Controller migration lock guard changed during inspection.');
|
|
2494
|
+
}
|
|
2337
2495
|
if (guard.owner.uid !== currentUid()) {
|
|
2338
2496
|
throw new Error('Controller migration lock guard belongs to another user.');
|
|
2339
2497
|
}
|
|
2340
|
-
if (
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2498
|
+
if (await this.lockOwnerIsLive(guard.owner)) continue;
|
|
2499
|
+
if (Date.now() - Number(stats.ctimeMs) < lockGuardInitializationGraceMs) continue;
|
|
2500
|
+
await unlinkExactPath(path, guard.identity);
|
|
2501
|
+
directoryChanged = true;
|
|
2344
2502
|
}
|
|
2345
2503
|
if (directoryChanged) {
|
|
2346
2504
|
await syncDirectory(this.commonParent);
|