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
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
controllerUpgradeManagementVersion,
|
|
7
7
|
type IControllerStatus,
|
|
8
8
|
} from '../ts_interfaces/index.js';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveAGLHomePaths } from './classes.aglhome.js';
|
|
10
10
|
import {
|
|
11
11
|
isLoopbackPortListening,
|
|
12
12
|
queryControllerStatus,
|
|
@@ -33,9 +33,12 @@ import {
|
|
|
33
33
|
upgradePackageTransitionTransactionVersion,
|
|
34
34
|
upgradePackageTransitionSource,
|
|
35
35
|
upgradePackageTransitionTarget,
|
|
36
|
-
upgradeTokenEnvironmentVariable,
|
|
37
36
|
serializeUpgradeWorkerPayload,
|
|
38
37
|
} from './classes.upgradecoordinator.js';
|
|
38
|
+
import {
|
|
39
|
+
upgradeCoordinationRootEnvironmentVariable,
|
|
40
|
+
upgradeTokenEnvironmentVariable,
|
|
41
|
+
} from './constants.upgradeenvironment.js';
|
|
39
42
|
|
|
40
43
|
const npmRegistryUrl = 'https://registry.npmjs.org/';
|
|
41
44
|
const commandOutputByteLimit = 1024 * 1024;
|
|
@@ -50,6 +53,11 @@ const upgradePreparationPollMs = 250;
|
|
|
50
53
|
const upgradePreparationSourceCheckMs = 500;
|
|
51
54
|
const upgradePreparationAcceptanceReconciliationMs = 5_000;
|
|
52
55
|
const upgradePreparationCleanupAllowanceMs = 60_000;
|
|
56
|
+
|
|
57
|
+
const scrubUpgradeCoordinationEnvironment = (): void => {
|
|
58
|
+
delete process.env[upgradeTokenEnvironmentVariable];
|
|
59
|
+
delete process.env[upgradeCoordinationRootEnvironmentVariable];
|
|
60
|
+
};
|
|
53
61
|
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$/;
|
|
54
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-]+)*)?$/;
|
|
55
63
|
let upgradeWorkerCliPath: string | undefined;
|
|
@@ -1110,7 +1118,7 @@ const openUpgradeLog = async (tokenArg: string): Promise<{
|
|
|
1110
1118
|
handle: plugins.fs.promises.FileHandle;
|
|
1111
1119
|
path: string;
|
|
1112
1120
|
}> => {
|
|
1113
|
-
const directory =
|
|
1121
|
+
const directory = resolveAGLHomePaths().logs;
|
|
1114
1122
|
await plugins.fs.promises.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
1115
1123
|
const directoryStats = await plugins.fs.promises.lstat(directory);
|
|
1116
1124
|
if (
|
|
@@ -1290,6 +1298,17 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
|
|
|
1290
1298
|
let child: plugins.childProcess.ChildProcess | undefined;
|
|
1291
1299
|
let logClosed = false;
|
|
1292
1300
|
try {
|
|
1301
|
+
const workerEnvironment = { ...(optionsArg.environment ?? process.env) };
|
|
1302
|
+
const inheritedToken = workerEnvironment[upgradeTokenEnvironmentVariable];
|
|
1303
|
+
const useLegacyCoordination = workerEnvironment[upgradeCoordinationRootEnvironmentVariable] === undefined
|
|
1304
|
+
&& inheritedToken === optionsArg.payload.token;
|
|
1305
|
+
if (useLegacyCoordination) {
|
|
1306
|
+
delete workerEnvironment[upgradeCoordinationRootEnvironmentVariable];
|
|
1307
|
+
} else if (workerEnvironment[upgradeCoordinationRootEnvironmentVariable] === undefined) {
|
|
1308
|
+
workerEnvironment[upgradeCoordinationRootEnvironmentVariable] = new UpgradeCoordinator(
|
|
1309
|
+
optionsArg.installation.globalRoot,
|
|
1310
|
+
).baseDirectory;
|
|
1311
|
+
}
|
|
1293
1312
|
const launchedChild = plugins.childProcess.spawn(
|
|
1294
1313
|
process.execPath,
|
|
1295
1314
|
[
|
|
@@ -1302,7 +1321,7 @@ const launchDetachedUpgradeWorkerProcess = async (optionsArg: {
|
|
|
1302
1321
|
cwd: plugins.os.homedir(),
|
|
1303
1322
|
detached: true,
|
|
1304
1323
|
env: {
|
|
1305
|
-
...
|
|
1324
|
+
...workerEnvironment,
|
|
1306
1325
|
[upgradeTokenEnvironmentVariable]: optionsArg.payload.token,
|
|
1307
1326
|
},
|
|
1308
1327
|
shell: false,
|
|
@@ -2185,8 +2204,14 @@ const runRecoveryUpgradeWorker = async (
|
|
|
2185
2204
|
payloadArg: IUpgradeWorkerPayload,
|
|
2186
2205
|
internalOptionsArg: IUpgradeWorkerInternalOptions,
|
|
2187
2206
|
): Promise<void> => {
|
|
2188
|
-
const context =
|
|
2189
|
-
|
|
2207
|
+
const [context, coordinator] = (() => {
|
|
2208
|
+
try {
|
|
2209
|
+
const parsedContext = parseUpgradeRecoveryWorkerContext(internalOptionsArg.recoveryContext);
|
|
2210
|
+
return [parsedContext, new UpgradeCoordinator(parsedContext.canonicalGlobalRoot)] as const;
|
|
2211
|
+
} finally {
|
|
2212
|
+
scrubUpgradeCoordinationEnvironment();
|
|
2213
|
+
}
|
|
2214
|
+
})();
|
|
2190
2215
|
let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>> | undefined;
|
|
2191
2216
|
let releaseLock = true;
|
|
2192
2217
|
try {
|
|
@@ -2252,16 +2277,26 @@ export async function runUpgradeWorker(
|
|
|
2252
2277
|
): Promise<void> {
|
|
2253
2278
|
const environmentToken = process.env[upgradeTokenEnvironmentVariable];
|
|
2254
2279
|
if (environmentToken !== payloadArg.token) {
|
|
2280
|
+
scrubUpgradeCoordinationEnvironment();
|
|
2255
2281
|
throw new Error(`The detached ${currentCliName} upgrade worker token binding is invalid.`);
|
|
2256
2282
|
}
|
|
2257
|
-
delete process.env[upgradeTokenEnvironmentVariable];
|
|
2258
2283
|
if (internalOptionsArg.recoveryContext !== undefined) {
|
|
2259
|
-
|
|
2284
|
+
try {
|
|
2285
|
+
await runRecoveryUpgradeWorker(payloadArg, internalOptionsArg);
|
|
2286
|
+
} finally {
|
|
2287
|
+
scrubUpgradeCoordinationEnvironment();
|
|
2288
|
+
}
|
|
2260
2289
|
return;
|
|
2261
2290
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2291
|
+
let installation: IPnpmGlobalInstallation;
|
|
2292
|
+
let coordinator: UpgradeCoordinator;
|
|
2293
|
+
try {
|
|
2294
|
+
installation = await resolveCurrentPnpmGlobalInstallation();
|
|
2295
|
+
upgradeWorkerCliPath = installation.cliPath;
|
|
2296
|
+
coordinator = new UpgradeCoordinator(installation.globalRoot);
|
|
2297
|
+
} finally {
|
|
2298
|
+
scrubUpgradeCoordinationEnvironment();
|
|
2299
|
+
}
|
|
2265
2300
|
let lock: Awaited<ReturnType<UpgradeCoordinator['acquireUpgradeLock']>> | undefined;
|
|
2266
2301
|
let releaseLock = true;
|
|
2267
2302
|
try {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { resolveAGLHomePaths } from './classes.aglhome.js';
|
|
3
|
+
import { readControllerProcessIdentity } from './classes.processinspection.js';
|
|
2
4
|
import {
|
|
3
5
|
controllerMaxAttachmentPromptSuffixBytes,
|
|
4
6
|
controllerMaxDraftAttachmentBytes,
|
|
@@ -9,6 +11,40 @@ import {
|
|
|
9
11
|
|
|
10
12
|
const maximumActiveUploads = 64;
|
|
11
13
|
const maximumActiveUploadBytes = 64 * 1024 * 1024;
|
|
14
|
+
const uploadOwnerFileName = '.controller-upload-owner.json';
|
|
15
|
+
const uploadOwnerMaximumBytes = 8 * 1024;
|
|
16
|
+
const uploadOrphanRetentionMs = 24 * 60 * 60 * 1000;
|
|
17
|
+
const maximumStaleUploadNodes = 8_192;
|
|
18
|
+
const maximumStaleUploadDepth = 32;
|
|
19
|
+
|
|
20
|
+
interface IControllerUploadOwner {
|
|
21
|
+
version: 1;
|
|
22
|
+
pid: number;
|
|
23
|
+
fingerprint: string;
|
|
24
|
+
nonce: string;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const parseUploadOwner = (valueArg: unknown): IControllerUploadOwner => {
|
|
29
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
30
|
+
throw new Error('Controller upload owner metadata is malformed.');
|
|
31
|
+
}
|
|
32
|
+
const value = valueArg as Record<string, unknown>;
|
|
33
|
+
if (
|
|
34
|
+
Object.keys(value).sort().join('\0')
|
|
35
|
+
!== ['createdAt', 'fingerprint', 'nonce', 'pid', 'version'].sort().join('\0')
|
|
36
|
+
|| value.version !== 1
|
|
37
|
+
|| !Number.isSafeInteger(value.pid)
|
|
38
|
+
|| (value.pid as number) < 2
|
|
39
|
+
|| typeof value.fingerprint !== 'string'
|
|
40
|
+
|| value.fingerprint.length === 0
|
|
41
|
+
|| value.fingerprint.length > 512
|
|
42
|
+
|| typeof value.nonce !== 'string'
|
|
43
|
+
|| !/^[a-f0-9]{32}$/.test(value.nonce)
|
|
44
|
+
|| !Number.isSafeInteger(value.createdAt)
|
|
45
|
+
) throw new Error('Controller upload owner metadata is invalid.');
|
|
46
|
+
return value as unknown as IControllerUploadOwner;
|
|
47
|
+
};
|
|
12
48
|
|
|
13
49
|
export interface IControllerUploadHandle {
|
|
14
50
|
id: string;
|
|
@@ -24,7 +60,13 @@ interface IActiveUpload {
|
|
|
24
60
|
cleanupPromise?: Promise<void>;
|
|
25
61
|
}
|
|
26
62
|
|
|
63
|
+
export interface IControllerUploadManagerOptions {
|
|
64
|
+
rootDirectory?: string;
|
|
65
|
+
readProcessIdentity?: typeof readControllerProcessIdentity;
|
|
66
|
+
}
|
|
67
|
+
|
|
27
68
|
export class ControllerUploadManager {
|
|
69
|
+
private readonly readProcessIdentity: typeof readControllerProcessIdentity;
|
|
28
70
|
private rootPromise?: Promise<string>;
|
|
29
71
|
private readonly activeUploads = new Map<string, IActiveUpload>();
|
|
30
72
|
private readonly pendingCreates = new Set<Promise<IControllerUploadHandle>>();
|
|
@@ -33,6 +75,10 @@ export class ControllerUploadManager {
|
|
|
33
75
|
private closed = false;
|
|
34
76
|
private closePromise?: Promise<void>;
|
|
35
77
|
|
|
78
|
+
constructor(private readonly options: IControllerUploadManagerOptions = {}) {
|
|
79
|
+
this.readProcessIdentity = options.readProcessIdentity ?? readControllerProcessIdentity;
|
|
80
|
+
}
|
|
81
|
+
|
|
36
82
|
public create(
|
|
37
83
|
attachmentsArg: readonly IControllerDraftAttachment[],
|
|
38
84
|
): Promise<IControllerUploadHandle | undefined> {
|
|
@@ -193,10 +239,23 @@ export class ControllerUploadManager {
|
|
|
193
239
|
this.rootPromise = (async () => {
|
|
194
240
|
let root: string | undefined;
|
|
195
241
|
try {
|
|
242
|
+
const baseRoot = this.options.rootDirectory ?? resolveAGLHomePaths().uploads;
|
|
243
|
+
await plugins.fs.promises.mkdir(baseRoot, { recursive: true, mode: 0o700 });
|
|
244
|
+
await plugins.fs.promises.chmod(baseRoot, 0o700);
|
|
245
|
+
await this.cleanupStaleRoots(baseRoot);
|
|
196
246
|
root = await plugins.fs.promises.mkdtemp(
|
|
197
|
-
plugins.path.join(
|
|
247
|
+
plugins.path.join(baseRoot, 'controller-'),
|
|
198
248
|
);
|
|
199
249
|
await plugins.fs.promises.chmod(root, 0o700);
|
|
250
|
+
const identity = await readControllerProcessIdentity(process.pid);
|
|
251
|
+
if (!identity) throw new Error('Unable to establish controller upload ownership.');
|
|
252
|
+
await this.writeOwnerMarker(root, {
|
|
253
|
+
version: 1,
|
|
254
|
+
pid: identity.pid,
|
|
255
|
+
fingerprint: identity.fingerprint,
|
|
256
|
+
nonce: plugins.crypto.randomBytes(16).toString('hex'),
|
|
257
|
+
createdAt: Date.now(),
|
|
258
|
+
});
|
|
200
259
|
return await plugins.fs.promises.realpath(root);
|
|
201
260
|
} catch (errorArg) {
|
|
202
261
|
if (root) {
|
|
@@ -216,6 +275,177 @@ export class ControllerUploadManager {
|
|
|
216
275
|
}
|
|
217
276
|
}
|
|
218
277
|
|
|
278
|
+
private async writeOwnerMarker(
|
|
279
|
+
rootArg: string,
|
|
280
|
+
ownerArg: IControllerUploadOwner,
|
|
281
|
+
): Promise<void> {
|
|
282
|
+
const markerPath = plugins.path.join(rootArg, uploadOwnerFileName);
|
|
283
|
+
const temporaryPath = `${markerPath}.tmp-${ownerArg.pid}-${ownerArg.nonce.slice(0, 16)}`;
|
|
284
|
+
let handle: plugins.fs.promises.FileHandle | undefined;
|
|
285
|
+
let temporaryCreated = false;
|
|
286
|
+
try {
|
|
287
|
+
handle = await plugins.fs.promises.open(
|
|
288
|
+
temporaryPath,
|
|
289
|
+
plugins.fs.constants.O_WRONLY
|
|
290
|
+
| plugins.fs.constants.O_CREAT
|
|
291
|
+
| plugins.fs.constants.O_EXCL
|
|
292
|
+
| (plugins.fs.constants.O_NOFOLLOW ?? 0),
|
|
293
|
+
0o600,
|
|
294
|
+
);
|
|
295
|
+
temporaryCreated = true;
|
|
296
|
+
await handle.writeFile(`${JSON.stringify(ownerArg)}\n`, 'utf8');
|
|
297
|
+
await handle.chmod(0o600);
|
|
298
|
+
await handle.sync();
|
|
299
|
+
await handle.close();
|
|
300
|
+
handle = undefined;
|
|
301
|
+
await plugins.fs.promises.rename(temporaryPath, markerPath);
|
|
302
|
+
await plugins.fs.promises.open(rootArg, plugins.fs.constants.O_RDONLY)
|
|
303
|
+
.then(async (directoryHandle) => {
|
|
304
|
+
try {
|
|
305
|
+
await directoryHandle.sync();
|
|
306
|
+
} finally {
|
|
307
|
+
await directoryHandle.close();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
temporaryCreated = false;
|
|
311
|
+
} catch (errorArg) {
|
|
312
|
+
const cleanupErrors: unknown[] = [];
|
|
313
|
+
if (handle) {
|
|
314
|
+
try {
|
|
315
|
+
await handle.close();
|
|
316
|
+
} catch (cleanupErrorArg) {
|
|
317
|
+
cleanupErrors.push(cleanupErrorArg);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (temporaryCreated) {
|
|
321
|
+
try {
|
|
322
|
+
await plugins.fs.promises.unlink(temporaryPath);
|
|
323
|
+
} catch (cleanupErrorArg) {
|
|
324
|
+
if ((cleanupErrorArg as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
325
|
+
cleanupErrors.push(cleanupErrorArg);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (cleanupErrors.length > 0) {
|
|
330
|
+
throw new AggregateError(
|
|
331
|
+
[errorArg, ...cleanupErrors],
|
|
332
|
+
'Controller upload owner publication failed and cleanup was incomplete.',
|
|
333
|
+
{ cause: errorArg },
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
throw errorArg;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private async cleanupStaleRoots(baseRootArg: string): Promise<void> {
|
|
341
|
+
const entries = await plugins.fs.promises.readdir(baseRootArg, { withFileTypes: true });
|
|
342
|
+
if (entries.length > 256) throw new Error('The controller upload root contains too many entries.');
|
|
343
|
+
for (const entry of entries) {
|
|
344
|
+
if (!entry.name.startsWith('controller-')) {
|
|
345
|
+
throw new Error(`Unexpected controller upload root entry: ${entry.name}`);
|
|
346
|
+
}
|
|
347
|
+
const root = plugins.path.join(baseRootArg, entry.name);
|
|
348
|
+
const stats = await plugins.fs.promises.lstat(root);
|
|
349
|
+
if (
|
|
350
|
+
!stats.isDirectory()
|
|
351
|
+
|| stats.isSymbolicLink()
|
|
352
|
+
|| (typeof process.getuid === 'function' && stats.uid !== process.getuid())
|
|
353
|
+
|| (stats.mode & 0o077) !== 0
|
|
354
|
+
) throw new Error(`Controller upload root is unsafe: ${root}`);
|
|
355
|
+
const markerPath = plugins.path.join(root, uploadOwnerFileName);
|
|
356
|
+
const markerStats = await plugins.fs.promises.lstat(markerPath).catch((errorArg) => {
|
|
357
|
+
if ((errorArg as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
|
|
358
|
+
throw errorArg;
|
|
359
|
+
});
|
|
360
|
+
let stale = false;
|
|
361
|
+
if (!markerStats) {
|
|
362
|
+
stale = Date.now() - stats.mtimeMs > uploadOrphanRetentionMs;
|
|
363
|
+
} else {
|
|
364
|
+
if (
|
|
365
|
+
!markerStats.isFile()
|
|
366
|
+
|| markerStats.isSymbolicLink()
|
|
367
|
+
|| (typeof process.getuid === 'function' && markerStats.uid !== process.getuid())
|
|
368
|
+
|| markerStats.nlink !== 1
|
|
369
|
+
|| (markerStats.mode & 0o077) !== 0
|
|
370
|
+
|| markerStats.size > uploadOwnerMaximumBytes
|
|
371
|
+
) throw new Error(`Controller upload owner marker is unsafe: ${markerPath}`);
|
|
372
|
+
const owner = parseUploadOwner(JSON.parse(
|
|
373
|
+
await plugins.fs.promises.readFile(markerPath, 'utf8'),
|
|
374
|
+
) as unknown);
|
|
375
|
+
let identity: Awaited<ReturnType<typeof readControllerProcessIdentity>>;
|
|
376
|
+
try {
|
|
377
|
+
identity = await this.readProcessIdentity(owner.pid);
|
|
378
|
+
} catch (errorArg) {
|
|
379
|
+
throw new Error(`Controller upload owner cannot be verified safely: ${root}`, {
|
|
380
|
+
cause: errorArg,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
stale = !identity || identity.fingerprint !== owner.fingerprint;
|
|
384
|
+
}
|
|
385
|
+
if (!stale) continue;
|
|
386
|
+
await this.removeStaleRoot(root, stats);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private async removeStaleRoot(
|
|
391
|
+
rootArg: string,
|
|
392
|
+
identityArg: plugins.fs.Stats,
|
|
393
|
+
): Promise<void> {
|
|
394
|
+
const current = await plugins.fs.promises.lstat(rootArg);
|
|
395
|
+
if (
|
|
396
|
+
!current.isDirectory()
|
|
397
|
+
|| current.isSymbolicLink()
|
|
398
|
+
|| current.dev !== identityArg.dev
|
|
399
|
+
|| current.ino !== identityArg.ino
|
|
400
|
+
) throw new Error(`Controller upload root changed before stale cleanup: ${rootArg}`);
|
|
401
|
+
const tombstone = `${rootArg}.stale-${process.pid}-${plugins.crypto.randomBytes(8).toString('hex')}`;
|
|
402
|
+
await plugins.fs.promises.rename(rootArg, tombstone);
|
|
403
|
+
try {
|
|
404
|
+
const moved = await plugins.fs.promises.lstat(tombstone);
|
|
405
|
+
if (moved.dev !== identityArg.dev || moved.ino !== identityArg.ino) {
|
|
406
|
+
throw new Error(`Controller upload stale root identity changed: ${rootArg}`);
|
|
407
|
+
}
|
|
408
|
+
await this.validateStaleTree(tombstone, identityArg.dev);
|
|
409
|
+
await plugins.fs.promises.rm(tombstone, { recursive: true, force: true });
|
|
410
|
+
} finally {
|
|
411
|
+
const parentHandle = await plugins.fs.promises.open(
|
|
412
|
+
plugins.path.dirname(rootArg),
|
|
413
|
+
plugins.fs.constants.O_RDONLY,
|
|
414
|
+
);
|
|
415
|
+
try {
|
|
416
|
+
await parentHandle.sync();
|
|
417
|
+
} finally {
|
|
418
|
+
await parentHandle.close();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private async validateStaleTree(rootArg: string, deviceArg: number): Promise<void> {
|
|
424
|
+
const pending = [{ path: rootArg, depth: 0 }];
|
|
425
|
+
let inspected = 0;
|
|
426
|
+
while (pending.length > 0) {
|
|
427
|
+
const current = pending.pop()!;
|
|
428
|
+
const entries = await plugins.fs.promises.readdir(current.path, { withFileTypes: true });
|
|
429
|
+
inspected += entries.length;
|
|
430
|
+
if (inspected > maximumStaleUploadNodes) {
|
|
431
|
+
throw new Error(`Controller upload stale tree contains too many entries: ${rootArg}`);
|
|
432
|
+
}
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
const path = plugins.path.join(current.path, entry.name);
|
|
435
|
+
const stats = await plugins.fs.promises.lstat(path);
|
|
436
|
+
if (
|
|
437
|
+
stats.dev !== deviceArg
|
|
438
|
+
|| (typeof process.getuid === 'function' && stats.uid !== process.getuid())
|
|
439
|
+
) throw new Error(`Controller upload stale tree contains a foreign node: ${path}`);
|
|
440
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) continue;
|
|
441
|
+
if (current.depth >= maximumStaleUploadDepth) {
|
|
442
|
+
throw new Error(`Controller upload stale tree is too deep: ${path}`);
|
|
443
|
+
}
|
|
444
|
+
pending.push({ path, depth: current.depth + 1 });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
219
449
|
private async cleanup(idArg: string): Promise<void> {
|
|
220
450
|
const active = this.activeUploads.get(idArg);
|
|
221
451
|
if (!active) return;
|
|
@@ -1,55 +1,19 @@
|
|
|
1
|
-
import * as plugins from './plugins.js';
|
|
2
1
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
} from '
|
|
7
|
-
import {
|
|
8
|
-
listControllerDataWriterProcessesForCliPaths,
|
|
9
|
-
} from './classes.processinspection.js';
|
|
10
|
-
import { embeddedDatabaseSocketPath } from './functions.embeddeddb.js';
|
|
11
|
-
import {
|
|
12
|
-
ControllerDataRootMigrationRunner,
|
|
13
|
-
type IControllerDataRootMigrationResult,
|
|
14
|
-
type IControllerDataWriterProcessRecord,
|
|
15
|
-
} from '../ts_migration/v2_controllerdataroot.js';
|
|
2
|
+
ensureAGLHomeDataRoot,
|
|
3
|
+
preflightAGLHomeDataRootMigration,
|
|
4
|
+
type IAGLHomeDataRootMigrationOptions,
|
|
5
|
+
} from '../ts_migration/v23_aglhome.js';
|
|
6
|
+
import type { IControllerDataRootMigrationResult } from '../ts_migration/v2_controllerdataroot.js';
|
|
16
7
|
|
|
17
|
-
export interface IEnsureControllerDataRootOptions extends
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
8
|
+
export interface IEnsureControllerDataRootOptions extends IAGLHomeDataRootMigrationOptions {}
|
|
9
|
+
|
|
10
|
+
export const preflightAGLHomeMigration = async (
|
|
11
|
+
optionsArg: IEnsureControllerDataRootOptions = {},
|
|
12
|
+
): Promise<void> => await preflightAGLHomeDataRootMigration(optionsArg);
|
|
21
13
|
|
|
22
14
|
export const ensureControllerDataRoot = async (
|
|
23
15
|
optionsArg: IEnsureControllerDataRootOptions = {},
|
|
24
|
-
): Promise<IControllerDataRootMigrationResult> =>
|
|
25
|
-
const roots = controllerDataRootPaths(optionsArg);
|
|
26
|
-
const databaseConfig = readDatabaseConfig(optionsArg);
|
|
27
|
-
if (!roots.migrationRequired) {
|
|
28
|
-
return { directoryPath: roots.targetRoot, databaseConfig };
|
|
29
|
-
}
|
|
30
|
-
const listDataWriterProcesses = optionsArg.listDataWriterProcesses ?? (async () => {
|
|
31
|
-
const cliPath = await plugins.fs.promises.realpath(
|
|
32
|
-
plugins.url.fileURLToPath(new URL('../cli.js', import.meta.url)),
|
|
33
|
-
);
|
|
34
|
-
return await listControllerDataWriterProcessesForCliPaths([cliPath], {
|
|
35
|
-
includeInstalledPackagePaths: true,
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
return await new ControllerDataRootMigrationRunner({
|
|
39
|
-
oldRoot: roots.legacyRoot,
|
|
40
|
-
newRoot: roots.targetRoot,
|
|
41
|
-
databaseConfig,
|
|
42
|
-
oldEmbeddedSocketPath: embeddedDatabaseSocketPath(
|
|
43
|
-
plugins.path.join(roots.legacyRoot, 'smartdb'),
|
|
44
|
-
),
|
|
45
|
-
newEmbeddedSocketPath: embeddedDatabaseSocketPath(
|
|
46
|
-
plugins.path.join(roots.targetRoot, 'smartdb'),
|
|
47
|
-
),
|
|
48
|
-
invokerPid: process.pid,
|
|
49
|
-
listDataWriterProcesses,
|
|
50
|
-
...(optionsArg.signal ? { signal: optionsArg.signal } : {}),
|
|
51
|
-
}).run();
|
|
52
|
-
};
|
|
16
|
+
): Promise<IControllerDataRootMigrationResult> => await ensureAGLHomeDataRoot(optionsArg);
|
|
53
17
|
|
|
54
18
|
export interface IEnsureControllerDataRootForStartupOptions
|
|
55
19
|
extends IEnsureControllerDataRootOptions {
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
|
+
import { resolveAGLHomePaths } from './classes.aglhome.js';
|
|
2
3
|
|
|
3
|
-
export const embeddedDatabaseSocketPath = (
|
|
4
|
+
export const embeddedDatabaseSocketPath = (
|
|
5
|
+
dataDirectoryArg: string,
|
|
6
|
+
socketDirectoryArg = resolveAGLHomePaths().sockets,
|
|
7
|
+
): string => {
|
|
4
8
|
const directoryHash = plugins.crypto
|
|
5
9
|
.createHash('sha256')
|
|
6
10
|
.update(dataDirectoryArg)
|
|
7
11
|
.digest('hex')
|
|
8
12
|
.slice(0, 16);
|
|
9
|
-
|
|
13
|
+
const socketPath = plugins.path.join(
|
|
14
|
+
socketDirectoryArg,
|
|
15
|
+
`smartdb-${directoryHash}.sock`,
|
|
16
|
+
);
|
|
17
|
+
if (Buffer.byteLength(socketPath, 'utf8') > 100) {
|
|
18
|
+
throw new Error('AGL_HOME is too long for the embedded database Unix socket path.');
|
|
19
|
+
}
|
|
20
|
+
return socketPath;
|
|
10
21
|
};
|
|
11
22
|
|
|
12
23
|
export const isSocketListening = (socketPathArg: string): Promise<boolean> => new Promise((resolve) => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const controllerEnvironmentPrefix = 'HARNESS_CONTROLLER_';
|
|
2
2
|
const forwardedEnvironmentVariable = 'HARNESS_CONTROLLER_FORWARD_ENV';
|
|
3
3
|
const runtimeEnvironmentAllowlist = new Set([
|
|
4
|
+
'AGL_HOME',
|
|
4
5
|
'APPDATA', 'COLORTERM', 'COMSPEC', 'FORCE_COLOR', 'HOME', 'HOMEDRIVE',
|
|
5
6
|
'HOMEPATH', 'HTTP_PROXY', 'HTTPS_PROXY', 'LANG', 'LOCALAPPDATA', 'LOGNAME',
|
|
6
7
|
'NODE_EXTRA_CA_CERTS', 'NO_PROXY', 'PATH', 'PATHEXT', 'SHELL', 'SSL_CERT_DIR',
|
package/ts/index.ts
CHANGED
package/ts/interfaces.config.ts
CHANGED
|
@@ -4,14 +4,11 @@ export interface IControllerRuntimeConfig {
|
|
|
4
4
|
controllerPort: number;
|
|
5
5
|
publicOrigin: string;
|
|
6
6
|
rpId: string;
|
|
7
|
-
workspaceDirectory: string;
|
|
8
7
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* once (from --projects-root, else the workspace directory), after which it
|
|
12
|
-
* is immutable like every other boundary.
|
|
8
|
+
* Default base for relative project paths and suggestions. Absolute projects
|
|
9
|
+
* remain independently identity-bound and may live outside this directory.
|
|
13
10
|
*/
|
|
14
|
-
projectsRoot
|
|
11
|
+
projectsRoot: string;
|
|
15
12
|
opencodePort: number;
|
|
16
13
|
tlsMode: TControllerTlsMode;
|
|
17
14
|
}
|
|
@@ -20,7 +17,8 @@ export interface IControllerRuntimeOverrides {
|
|
|
20
17
|
controllerPort: number;
|
|
21
18
|
publicOrigin?: string;
|
|
22
19
|
rpId?: string;
|
|
23
|
-
|
|
20
|
+
/** Explicit one-time project registration; never persisted as runtime config. */
|
|
21
|
+
initialProjectDirectory?: string;
|
|
24
22
|
projectsRoot?: string;
|
|
25
23
|
opencodePort?: number;
|
|
26
24
|
behindTlsProxy?: boolean;
|
|
@@ -2,6 +2,7 @@ import * as plugins from '../ts/plugins.js';
|
|
|
2
2
|
import { ProtocolV6DocumentMigration } from './v6_protocoldocumentmigration.js';
|
|
3
3
|
import { ProtocolV18ResourceLayoutMigration } from './v18_resourcelayoutmigration.js';
|
|
4
4
|
import { ProtocolV22ProjectFilesystemIdentityMigration } from './v22_projectfilesystemidentitymigration.js';
|
|
5
|
+
import { RuntimeConfigV23Migration } from './v23_runtimeconfig.js';
|
|
5
6
|
|
|
6
7
|
interface IDocumentMigration {
|
|
7
8
|
run(): Promise<void>;
|
|
@@ -12,6 +13,7 @@ export class PostDatabaseDocumentMigrationRunner {
|
|
|
12
13
|
|
|
13
14
|
constructor(private readonly database: plugins.smartdata.SmartdataDb | undefined) {
|
|
14
15
|
this.migrations = [
|
|
16
|
+
new RuntimeConfigV23Migration(this.database),
|
|
15
17
|
new ProtocolV6DocumentMigration(this.database),
|
|
16
18
|
new ProtocolV18ResourceLayoutMigration(this.database),
|
|
17
19
|
];
|
package/ts_migration/index.ts
CHANGED
|
@@ -10,3 +10,5 @@ export * from './v18_resourcelayoutmigration.js';
|
|
|
10
10
|
export * from './classes.flexpostharnessmigrationrunner.js';
|
|
11
11
|
export * from './v19_flexprojectmanagement.js';
|
|
12
12
|
export * from './v22_projectfilesystemidentitymigration.js';
|
|
13
|
+
export * from './v23_runtimeconfig.js';
|
|
14
|
+
export * from './v23_aglhome.js';
|