vona-core 5.1.46 → 5.1.48
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/dist/index.js +874 -117
- package/dist/index.js.map +1 -1
- package/dist/lib/bean/beanBase.d.ts.map +1 -1
- package/dist/lib/core/asyncLocalStorage.d.ts +6 -0
- package/dist/lib/core/asyncLocalStorage.d.ts.map +1 -1
- package/dist/lib/core/index.d.ts +3 -0
- package/dist/lib/core/index.d.ts.map +1 -1
- package/dist/lib/core/logger/appLogger.d.ts +2 -0
- package/dist/lib/core/logger/appLogger.d.ts.map +1 -1
- package/dist/lib/core/logger/diagnosticLogger.d.ts +4 -0
- package/dist/lib/core/logger/diagnosticLogger.d.ts.map +1 -0
- package/dist/lib/core/logger/diagnosticLogger.test.d.ts +2 -0
- package/dist/lib/core/logger/diagnosticLogger.test.d.ts.map +1 -0
- package/dist/lib/core/logger/index.d.ts +1 -0
- package/dist/lib/core/logger/index.d.ts.map +1 -1
- package/dist/lib/core/logger/utils.d.ts +1 -0
- package/dist/lib/core/logger/utils.d.ts.map +1 -1
- package/dist/lib/core/requestBoundary.d.ts +22 -0
- package/dist/lib/core/requestBoundary.d.ts.map +1 -0
- package/dist/lib/core/testClaim.d.ts +10 -0
- package/dist/lib/core/testClaim.d.ts.map +1 -1
- package/dist/lib/core/testClaimFilesystem.d.ts +13 -0
- package/dist/lib/core/testClaimFilesystem.d.ts.map +1 -0
- package/dist/lib/core/testClaimFilesystem.test.d.ts +2 -0
- package/dist/lib/core/testClaimFilesystem.test.d.ts.map +1 -0
- package/dist/lib/core/testClaimFilesystemWindows.d.ts +19 -0
- package/dist/lib/core/testClaimFilesystemWindows.d.ts.map +1 -0
- package/dist/lib/core/testFreshness.d.ts +21 -0
- package/dist/lib/core/testFreshness.d.ts.map +1 -0
- package/dist/lib/core/testFreshness.test.d.ts +2 -0
- package/dist/lib/core/testFreshness.test.d.ts.map +1 -0
- package/dist/lib/core/testProcessWindowsNative.d.ts +59 -0
- package/dist/lib/core/testProcessWindowsNative.d.ts.map +1 -0
- package/dist/lib/framework/bootstrap.test.d.ts +2 -0
- package/dist/lib/framework/bootstrap.test.d.ts.map +1 -0
- package/dist/lib/framework/index.d.ts +1 -0
- package/dist/lib/framework/index.d.ts.map +1 -1
- package/dist/lib/framework/processWorker.d.ts +1 -0
- package/dist/lib/framework/processWorker.d.ts.map +1 -1
- package/dist/lib/framework/processWorker.test.d.ts +2 -0
- package/dist/lib/framework/processWorker.test.d.ts.map +1 -0
- package/dist/lib/framework/start.d.ts.map +1 -1
- package/dist/lib/framework/useApp.test.d.ts +2 -0
- package/dist/lib/framework/useApp.test.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/lib/bean/beanBase.ts +4 -0
- package/src/lib/core/asyncLocalStorage.ts +84 -12
- package/src/lib/core/index.ts +14 -0
- package/src/lib/core/logger/appLogger.ts +11 -3
- package/src/lib/core/logger/diagnosticLogger.test.ts +146 -0
- package/src/lib/core/logger/diagnosticLogger.ts +138 -0
- package/src/lib/core/logger/index.ts +1 -0
- package/src/lib/core/logger/utils.ts +25 -5
- package/src/lib/core/requestBoundary.ts +89 -0
- package/src/lib/core/testClaim.test.ts +298 -17
- package/src/lib/core/testClaim.ts +205 -97
- package/src/lib/core/testClaimFilesystem.test.ts +247 -0
- package/src/lib/core/testClaimFilesystem.ts +224 -0
- package/src/lib/core/testClaimFilesystemWindows.cs +229 -0
- package/src/lib/core/testClaimFilesystemWindows.ps1 +20 -0
- package/src/lib/core/testClaimFilesystemWindows.ts +200 -0
- package/src/lib/core/testFreshness.test.ts +379 -0
- package/src/lib/core/testFreshness.ts +238 -0
- package/src/lib/core/testProcessWindowsNative.cs +336 -0
- package/src/lib/core/testProcessWindowsNative.ps1 +20 -0
- package/src/lib/core/testProcessWindowsNative.ts +531 -0
- package/src/lib/framework/bootstrap.test.ts +61 -0
- package/src/lib/framework/index.ts +1 -0
- package/src/lib/framework/processWorker.test.ts +59 -0
- package/src/lib/framework/processWorker.ts +8 -5
- package/src/lib/framework/start.test.ts +4 -3
- package/src/lib/framework/start.ts +2 -0
- package/src/lib/framework/useApp.test.ts +84 -0
- package/src/lib/framework/useApp.ts +2 -2
- package/src/lib/utils/util.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import '@cabloy/json5';
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
3
|
import { combineApiPathControllerAndActionRaw, combineApiPathControllerAndAction, combineApiPath, combineResourceName, forEach, forEachSync, sleep, isUndefined, isClass, isNilOrEmptyString, evaluateExpressions, catchErrorSync, isEmptyObject, catchError, hashkey } from '@cabloy/utils';
|
|
3
4
|
import { toLowerCaseFirstChar, splitWords } from '@cabloy/word-utils';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import fse from 'fs-extra';
|
|
6
7
|
import crypto, { createHash as createHash$1, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
7
|
-
import fs, { lstatSync, constants } from 'node:fs';
|
|
8
|
+
import fs, { lstatSync, existsSync, readFileSync, constants } from 'node:fs';
|
|
8
9
|
import { compose as compose$1 } from '@cabloy/compose';
|
|
9
10
|
import { extend } from '@cabloy/extend';
|
|
10
11
|
import { createRequire } from 'node:module';
|
|
11
|
-
import { pathToFileURL } from 'node:url';
|
|
12
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
12
13
|
import * as uuid from 'uuid';
|
|
13
14
|
import { getPlatformArch as getPlatformArch$1 } from 'vona';
|
|
14
15
|
import os from 'node:os';
|
|
15
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
16
16
|
import { isDeepStrictEqual } from 'node:util';
|
|
17
17
|
import { setLocaleErrors, setLocaleAdapter, translateError } from '@cabloy/zod-errors-custom';
|
|
18
18
|
import { ZodMetadata } from '@cabloy/zod-openapi';
|
|
@@ -26,8 +26,9 @@ import chalk from 'chalk';
|
|
|
26
26
|
import * as Winston from 'winston';
|
|
27
27
|
import DailyRotateFile from 'winston-daily-rotate-file';
|
|
28
28
|
import { MESSAGE, LEVEL } from 'triple-beam';
|
|
29
|
-
import { mkdir, open, rename, unlink } from 'node:fs/promises';
|
|
30
29
|
import { createServer } from 'node:http';
|
|
30
|
+
import { mkdir, open, rename, unlink, lstat, readdir } from 'node:fs/promises';
|
|
31
|
+
import { execFileSync, execFile } from 'node:child_process';
|
|
31
32
|
import cluster from 'node:cluster';
|
|
32
33
|
import React from 'react';
|
|
33
34
|
import { extendZodWithOpenApi } from '@cabloy/zod-to-openapi';
|
|
@@ -35,6 +36,179 @@ import { z } from 'zod';
|
|
|
35
36
|
import * as Retry from 'retry';
|
|
36
37
|
export { Retry };
|
|
37
38
|
|
|
39
|
+
const boundaries = new WeakMap();
|
|
40
|
+
const boundaryStorage = new AsyncLocalStorage();
|
|
41
|
+
function createRequestBoundary(options) {
|
|
42
|
+
const onFailure = options?.onFailure;
|
|
43
|
+
const boundary = Object.freeze({});
|
|
44
|
+
let portFault = false;
|
|
45
|
+
let reporting = false;
|
|
46
|
+
const reported = new Set();
|
|
47
|
+
const capture = Object.freeze({
|
|
48
|
+
diagnostics: 'suppress',
|
|
49
|
+
reportFailure(failure) {
|
|
50
|
+
if (failure !== 'observer_failed' && failure !== 'diagnostic_failed' && failure !== 'late_callback_failed') {
|
|
51
|
+
portFault = true;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
portFault = true;
|
|
55
|
+
if (reported.has(failure)) return;
|
|
56
|
+
reported.add(failure);
|
|
57
|
+
if (reporting || !onFailure) return;
|
|
58
|
+
reporting = true;
|
|
59
|
+
try {
|
|
60
|
+
const result = onFailure(failure);
|
|
61
|
+
if (result && (typeof result === 'object' || typeof result === 'function')) {
|
|
62
|
+
const then = result.then;
|
|
63
|
+
if (typeof then === 'function') {
|
|
64
|
+
portFault = true;
|
|
65
|
+
void new Promise((resolve, reject) => {
|
|
66
|
+
then.call(result, resolve, reject);
|
|
67
|
+
}).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
portFault = true;
|
|
72
|
+
} finally {
|
|
73
|
+
reporting = false;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
snapshot() {
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
portFault
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
boundaries.set(boundary, capture);
|
|
83
|
+
return boundary;
|
|
84
|
+
}
|
|
85
|
+
function runWithRequestBoundary(boundary, continuation) {
|
|
86
|
+
if (!boundaries.has(boundary)) throw new Error('Invalid request boundary');
|
|
87
|
+
const current = boundaryStorage.getStore();
|
|
88
|
+
if (current && current !== boundary) throw new Error('Request boundary cannot be replaced');
|
|
89
|
+
if (current === boundary) return continuation();
|
|
90
|
+
return boundaryStorage.run(boundary, continuation);
|
|
91
|
+
}
|
|
92
|
+
function getRequestBoundary() {
|
|
93
|
+
return boundaryStorage.getStore();
|
|
94
|
+
}
|
|
95
|
+
function captureRequestBoundary() {
|
|
96
|
+
const boundary = boundaryStorage.getStore();
|
|
97
|
+
return boundary ? boundaries.get(boundary) : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const loggingMethods = new Set(['log', 'error', 'warn', 'info', 'http', 'verbose', 'debug', 'silly']);
|
|
101
|
+
function completeWrite(args) {
|
|
102
|
+
const callback = typeof args[1] === 'function' ? args[1] : args[2];
|
|
103
|
+
if (typeof callback === 'function') {
|
|
104
|
+
try {
|
|
105
|
+
callback();
|
|
106
|
+
} catch {
|
|
107
|
+
captureRequestBoundary()?.reportFailure('diagnostic_failed');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const suppressedLogger = createFacade();
|
|
113
|
+
function getSuppressedDiagnosticLogger() {
|
|
114
|
+
return suppressedLogger;
|
|
115
|
+
}
|
|
116
|
+
function createDiagnosticLogger(raw) {
|
|
117
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
118
|
+
return createFacade(raw);
|
|
119
|
+
}
|
|
120
|
+
function createFacade(raw) {
|
|
121
|
+
const methods = new Map();
|
|
122
|
+
const handlers = new WeakMap();
|
|
123
|
+
const inertProfiler = Object.freeze({
|
|
124
|
+
get logger() {
|
|
125
|
+
return getSuppressedDiagnosticLogger();
|
|
126
|
+
},
|
|
127
|
+
done: () => getSuppressedDiagnosticLogger()
|
|
128
|
+
});
|
|
129
|
+
const facade = new Proxy({}, {
|
|
130
|
+
get(_target, key) {
|
|
131
|
+
if (loggingMethods.has(key) || ['write', 'child', 'startTimer', 'profile'].includes(key)) {
|
|
132
|
+
if (!methods.has(key)) {
|
|
133
|
+
methods.set(key, (...args) => {
|
|
134
|
+
if (!raw || getRequestBoundary()) {
|
|
135
|
+
if (key === 'write') return completeWrite(args);
|
|
136
|
+
if (key === 'child') return getSuppressedDiagnosticLogger();
|
|
137
|
+
if (key === 'startTimer') return inertProfiler;
|
|
138
|
+
return facade;
|
|
139
|
+
}
|
|
140
|
+
const result = Reflect.apply(Reflect.get(raw, key, raw), raw, args);
|
|
141
|
+
if (key === 'child') return createFacade(result);
|
|
142
|
+
if (key === 'startTimer') return wrapProfiler(result, facade);
|
|
143
|
+
return result === raw ? facade : result;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return methods.get(key);
|
|
147
|
+
}
|
|
148
|
+
if (!raw) return undefined;
|
|
149
|
+
const value = Reflect.get(raw, key, raw);
|
|
150
|
+
if ((key === 'exceptions' || key === 'rejections') && value) {
|
|
151
|
+
if (!handlers.has(value)) handlers.set(value, wrapHandler(value, raw, facade));
|
|
152
|
+
return handlers.get(value);
|
|
153
|
+
}
|
|
154
|
+
if (typeof value !== 'function') return value;
|
|
155
|
+
if (!methods.has(key)) {
|
|
156
|
+
methods.set(key, (...args) => {
|
|
157
|
+
const result = Reflect.apply(value, raw, args);
|
|
158
|
+
return result === raw ? facade : result;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return methods.get(key);
|
|
162
|
+
},
|
|
163
|
+
set(_target, key, value) {
|
|
164
|
+
return raw ? Reflect.set(raw, key, value, raw) : true;
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return facade;
|
|
168
|
+
}
|
|
169
|
+
function wrapHandler(raw, rawLogger, logger) {
|
|
170
|
+
const methods = new Map();
|
|
171
|
+
const handler = new Proxy({}, {
|
|
172
|
+
get(_target, key) {
|
|
173
|
+
if (key === 'logger') return logger;
|
|
174
|
+
const value = Reflect.get(raw, key, raw);
|
|
175
|
+
if (typeof value !== 'function') return value;
|
|
176
|
+
if (!methods.has(key)) {
|
|
177
|
+
methods.set(key, (...args) => {
|
|
178
|
+
const result = Reflect.apply(value, raw, args);
|
|
179
|
+
if (result === raw) return handler;
|
|
180
|
+
return result === rawLogger ? logger : result;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return methods.get(key);
|
|
184
|
+
},
|
|
185
|
+
set(_target, key, value) {
|
|
186
|
+
if (key === 'logger') return false;
|
|
187
|
+
return Reflect.set(raw, key, value, raw);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
return handler;
|
|
191
|
+
}
|
|
192
|
+
function wrapProfiler(raw, logger) {
|
|
193
|
+
return new Proxy({}, {
|
|
194
|
+
get(_target, key) {
|
|
195
|
+
if (key === 'logger') return logger;
|
|
196
|
+
if (key === 'done') {
|
|
197
|
+
return (...args) => {
|
|
198
|
+
if (getRequestBoundary()) return logger;
|
|
199
|
+
const result = Reflect.apply(raw.done, raw, args);
|
|
200
|
+
return result === raw.logger ? logger : result;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return Reflect.get(raw, key, raw);
|
|
204
|
+
},
|
|
205
|
+
set(_target, key, value) {
|
|
206
|
+
if (key === 'logger') return false;
|
|
207
|
+
return Reflect.set(raw, key, value, raw);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
38
212
|
function cast(source) {
|
|
39
213
|
return source;
|
|
40
214
|
}
|
|
@@ -546,7 +720,7 @@ class AppUtil extends BeanSimple {
|
|
|
546
720
|
const globalPrefix = '/api/static';
|
|
547
721
|
if (!staticPath) staticPath = '';
|
|
548
722
|
// safe
|
|
549
|
-
staticPath = path.normalize(staticPath);
|
|
723
|
+
staticPath = path.posix.normalize(staticPath.replace(/\\/g, '/'));
|
|
550
724
|
if (staticPath.includes('..')) throw new Error(`unsafe path: ${staticPath}`);
|
|
551
725
|
// ignore globalPrefix
|
|
552
726
|
if (staticPath.startsWith('//')) return staticPath.substring(1);
|
|
@@ -811,9 +985,9 @@ async function createGeneralApp(projectPath, envRuntime) {
|
|
|
811
985
|
}
|
|
812
986
|
}
|
|
813
987
|
if (process.env.META_MODE === 'prod') {
|
|
814
|
-
const testFile = path.join(projectPath, `dist/${process.env.META_FLAVOR}/
|
|
988
|
+
const testFile = path.join(projectPath, `dist/${process.env.META_FLAVOR}/app.js`);
|
|
815
989
|
const testInstance = await import(pathToHref(testFile));
|
|
816
|
-
return await testInstance.
|
|
990
|
+
return await testInstance.createSingleApp();
|
|
817
991
|
} else {
|
|
818
992
|
const testFile = path.join(resolveVonaLocalPaths(projectPath, process.env.META_MODE).generatedRoot, 'app.ts');
|
|
819
993
|
const testInstance = await import(pathToHref(testFile));
|
|
@@ -1184,6 +1358,7 @@ class BeanBase extends BeanBaseSimple {
|
|
|
1184
1358
|
return this.$loggerClient('default');
|
|
1185
1359
|
}
|
|
1186
1360
|
$loggerClient(clientName = 'default') {
|
|
1361
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
1187
1362
|
if (!this[SymbolLogger][clientName]) {
|
|
1188
1363
|
this[SymbolLogger][clientName] = this.app.meta.logger.get(clientName).child({
|
|
1189
1364
|
beanFullName: this.$beanFullName
|
|
@@ -1192,6 +1367,7 @@ class BeanBase extends BeanBaseSimple {
|
|
|
1192
1367
|
return this[SymbolLogger][clientName];
|
|
1193
1368
|
}
|
|
1194
1369
|
$loggerChild(childName, clientName = 'default') {
|
|
1370
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
1195
1371
|
if (!this[SymbolLoggerChildren][clientName]) this[SymbolLoggerChildren][clientName] = {};
|
|
1196
1372
|
if (!this[SymbolLoggerChildren][clientName][childName]) {
|
|
1197
1373
|
this[SymbolLoggerChildren][clientName][childName] = this.app.meta.logger.get(clientName).child({
|
|
@@ -2455,21 +2631,72 @@ class VonaAsyncLocalStorage extends AsyncLocalStorage {
|
|
|
2455
2631
|
constructor(app) {
|
|
2456
2632
|
super();
|
|
2457
2633
|
this.app = void 0;
|
|
2634
|
+
this.observedRuns = new WeakMap();
|
|
2458
2635
|
this.app = app;
|
|
2459
2636
|
}
|
|
2460
2637
|
async run(store, callback, ...args) {
|
|
2461
2638
|
if (store === this.app.currentContext) {
|
|
2462
2639
|
return await callback(...args);
|
|
2463
2640
|
}
|
|
2464
|
-
|
|
2641
|
+
if (this.observedRuns.has(store)) throw new Error('Context scope already owned');
|
|
2642
|
+
return this._runOwned(store, () => callback(...args));
|
|
2643
|
+
}
|
|
2644
|
+
async runObserved(store, callback, observer) {
|
|
2645
|
+
if (store === this.app.currentContext || this.observedRuns.has(store)) {
|
|
2646
|
+
throw new Error('Context scope already owned');
|
|
2647
|
+
}
|
|
2648
|
+
const frame = {
|
|
2649
|
+
settled: false
|
|
2650
|
+
};
|
|
2651
|
+
this.observedRuns.set(store, frame);
|
|
2652
|
+
try {
|
|
2653
|
+
return await this._runOwned(store, callback, observer);
|
|
2654
|
+
} finally {
|
|
2655
|
+
frame.settled = true;
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
_runOwned(store, callback, observer) {
|
|
2659
|
+
const capture = captureRequestBoundary();
|
|
2660
|
+
return super.run(store, async () => {
|
|
2465
2661
|
try {
|
|
2466
2662
|
this.app.meta.ctxCounter.increment();
|
|
2467
|
-
return await callback(
|
|
2663
|
+
return await callback();
|
|
2468
2664
|
} finally {
|
|
2469
|
-
await
|
|
2470
|
-
|
|
2665
|
+
await this._disposeOwned(store, observer, capture);
|
|
2666
|
+
}
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
async _disposeOwned(store, observer, capture) {
|
|
2670
|
+
try {
|
|
2671
|
+
await store.bean.dispose();
|
|
2672
|
+
} catch (error) {
|
|
2673
|
+
this._notifyScopeOutcome(observer, 'dispose_failed', capture);
|
|
2674
|
+
throw error;
|
|
2675
|
+
}
|
|
2676
|
+
try {
|
|
2677
|
+
this.app.meta.ctxCounter.decrement();
|
|
2678
|
+
} catch (error) {
|
|
2679
|
+
capture?.reportFailure('diagnostic_failed');
|
|
2680
|
+
throw error;
|
|
2681
|
+
}
|
|
2682
|
+
this._notifyScopeOutcome(observer, 'disposed', capture);
|
|
2683
|
+
}
|
|
2684
|
+
_notifyScopeOutcome(observer, outcome, capture) {
|
|
2685
|
+
if (!observer) return;
|
|
2686
|
+
try {
|
|
2687
|
+
const result = observer(outcome);
|
|
2688
|
+
if (result && (typeof result === 'object' || typeof result === 'function')) {
|
|
2689
|
+
const then = result.then;
|
|
2690
|
+
if (typeof then === 'function') {
|
|
2691
|
+
capture?.reportFailure('observer_failed');
|
|
2692
|
+
void new Promise((resolve, reject) => {
|
|
2693
|
+
then.call(result, resolve, reject);
|
|
2694
|
+
}).catch(() => {});
|
|
2695
|
+
}
|
|
2471
2696
|
}
|
|
2472
|
-
}
|
|
2697
|
+
} catch {
|
|
2698
|
+
capture?.reportFailure('observer_failed');
|
|
2699
|
+
}
|
|
2473
2700
|
}
|
|
2474
2701
|
}
|
|
2475
2702
|
|
|
@@ -2646,25 +2873,31 @@ class AppHmr extends BeanSimple {
|
|
|
2646
2873
|
}
|
|
2647
2874
|
|
|
2648
2875
|
const SymbolLoggerInstances = Symbol('SymbolLoggerInstances');
|
|
2876
|
+
const SymbolRawLoggerInstances = Symbol('SymbolRawLoggerInstances');
|
|
2649
2877
|
class AppLogger extends BeanSimple {
|
|
2650
2878
|
constructor(...args) {
|
|
2651
2879
|
super(...args);
|
|
2652
2880
|
this[SymbolLoggerInstances] = {};
|
|
2881
|
+
this[SymbolRawLoggerInstances] = {};
|
|
2653
2882
|
}
|
|
2654
2883
|
async dispose() {
|
|
2655
|
-
for (const key in this[
|
|
2656
|
-
const logger = this[
|
|
2884
|
+
for (const key in this[SymbolRawLoggerInstances]) {
|
|
2885
|
+
const logger = this[SymbolRawLoggerInstances][key];
|
|
2657
2886
|
await _closeLogger(logger);
|
|
2658
2887
|
}
|
|
2659
2888
|
}
|
|
2660
2889
|
get(clientName) {
|
|
2890
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
2661
2891
|
clientName = clientName || 'default';
|
|
2662
2892
|
if (!this[SymbolLoggerInstances][clientName]) {
|
|
2663
|
-
|
|
2893
|
+
const raw = this._createClient(clientName);
|
|
2894
|
+
this[SymbolRawLoggerInstances][clientName] = raw;
|
|
2895
|
+
this[SymbolLoggerInstances][clientName] = createDiagnosticLogger(raw);
|
|
2664
2896
|
}
|
|
2665
2897
|
return this[SymbolLoggerInstances][clientName];
|
|
2666
2898
|
}
|
|
2667
2899
|
child(childName, clientName) {
|
|
2900
|
+
if (getRequestBoundary()) return getSuppressedDiagnosticLogger();
|
|
2668
2901
|
const logger = this.get(clientName);
|
|
2669
2902
|
if (!childName) return logger;
|
|
2670
2903
|
return logger.child({
|
|
@@ -3080,10 +3313,16 @@ var localesDefault = {
|
|
|
3080
3313
|
};
|
|
3081
3314
|
|
|
3082
3315
|
const SymbolLoggerMessage = Symbol('SymbolLoggerMessage');
|
|
3083
|
-
const
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
})
|
|
3316
|
+
const formatLoggerBoundary = Winston.format(info => {
|
|
3317
|
+
if (getRequestBoundary()) return false;
|
|
3318
|
+
return info;
|
|
3319
|
+
});
|
|
3320
|
+
const formatLoggerAxiosError = Winston.format((einfo, opts) => {
|
|
3321
|
+
if (getRequestBoundary()) return false;
|
|
3322
|
+
const {
|
|
3323
|
+
stack,
|
|
3324
|
+
cause
|
|
3325
|
+
} = opts;
|
|
3087
3326
|
if (einfo instanceof Error && einfo.constructor.name.includes('AxiosError') || einfo.name === 'AxiosError' || einfo.isAxiosError) {
|
|
3088
3327
|
const info = Object.assign({}, einfo, {
|
|
3089
3328
|
level: einfo.level,
|
|
@@ -3103,9 +3342,11 @@ const formatLoggerAxiosError = Winston.format((einfo, {
|
|
|
3103
3342
|
return einfo;
|
|
3104
3343
|
});
|
|
3105
3344
|
const formatLoggerErrors = opts => {
|
|
3106
|
-
|
|
3345
|
+
if (getRequestBoundary()) return formatLoggerBoundary();
|
|
3346
|
+
return Winston.format.combine(formatLoggerBoundary(), formatLoggerAxiosError(opts), Winston.format.errors(opts));
|
|
3107
3347
|
};
|
|
3108
3348
|
const formatLoggerCtx = Winston.format((info, _opts) => {
|
|
3349
|
+
if (getRequestBoundary()) return false;
|
|
3109
3350
|
const app = useApp();
|
|
3110
3351
|
if (!app.ctx) return info;
|
|
3111
3352
|
if (app.ctx.method) info.method = app.ctx.method;
|
|
@@ -3122,11 +3363,13 @@ const formatLoggerCtx = Winston.format((info, _opts) => {
|
|
|
3122
3363
|
return info;
|
|
3123
3364
|
});
|
|
3124
3365
|
const formatLoggerDummy = Winston.format(info => {
|
|
3366
|
+
if (getRequestBoundary()) return false;
|
|
3125
3367
|
const app = useApp();
|
|
3126
3368
|
if (app.meta.env.LOGGER_DUMMY === 'true') return false;
|
|
3127
3369
|
return info;
|
|
3128
3370
|
});
|
|
3129
3371
|
const formatLoggerFilter = Winston.format((info, opts) => {
|
|
3372
|
+
if (getRequestBoundary()) return false;
|
|
3130
3373
|
// check client
|
|
3131
3374
|
const child = typeof opts.child === 'function' ? opts.child() : opts.child;
|
|
3132
3375
|
if (child && !child.includes(info.name)) {
|
|
@@ -3146,16 +3389,18 @@ const formatLoggerFilter = Winston.format((info, opts) => {
|
|
|
3146
3389
|
return false;
|
|
3147
3390
|
});
|
|
3148
3391
|
const formatLoggerConsole = clientInfo => {
|
|
3149
|
-
return Winston.format.printf(
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3153
|
-
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3392
|
+
return Winston.format.combine(formatLoggerBoundary(), Winston.format.printf(info => {
|
|
3393
|
+
if (getRequestBoundary()) return '';
|
|
3394
|
+
const {
|
|
3395
|
+
timestamp,
|
|
3396
|
+
level,
|
|
3397
|
+
stack,
|
|
3398
|
+
message,
|
|
3399
|
+
name,
|
|
3400
|
+
beanFullName,
|
|
3401
|
+
durationMs,
|
|
3402
|
+
...meta
|
|
3403
|
+
} = info;
|
|
3159
3404
|
const textClientName = clientInfo.clientName === 'default' ? '' : ` ${chalk.cyan(`[${clientInfo.clientName}]`)}`;
|
|
3160
3405
|
const textName = name ? ` ${chalk.cyan(`[${name}]`)}` : '';
|
|
3161
3406
|
const textBeanFullName = beanFullName ? ` ${chalk.gray(`[${beanFullName}]`)}` : '';
|
|
@@ -3164,9 +3409,10 @@ const formatLoggerConsole = clientInfo => {
|
|
|
3164
3409
|
const textDurationMs = durationMs !== undefined ? ` ${chalk.cyan(`+${durationMs}ms`)}` : '';
|
|
3165
3410
|
const textStack = stack ? `\n${stack}` : '';
|
|
3166
3411
|
return `${timestamp} ${level}${textClientName}${textName}${textBeanFullName}${textMeta}${textMessage}${textDurationMs}${textStack}`;
|
|
3167
|
-
});
|
|
3412
|
+
}));
|
|
3168
3413
|
};
|
|
3169
3414
|
function __formatLoggerFilterCheckInfo(info) {
|
|
3415
|
+
if (getRequestBoundary()) return false;
|
|
3170
3416
|
if (typeof info.message === 'function') {
|
|
3171
3417
|
if (info.message[SymbolLoggerMessage] === undefined) {
|
|
3172
3418
|
info.message[SymbolLoggerMessage] = info.message();
|
|
@@ -3176,8 +3422,307 @@ function __formatLoggerFilterCheckInfo(info) {
|
|
|
3176
3422
|
return info;
|
|
3177
3423
|
}
|
|
3178
3424
|
|
|
3425
|
+
// Both source and bundled dist consumers use the checked-in, published src assets.
|
|
3426
|
+
function getWindowsTestClaimFilesystemHelper(directory = import.meta.dirname ?? path.dirname(fileURLToPath(import.meta.url))) {
|
|
3427
|
+
let current = directory;
|
|
3428
|
+
while (true) {
|
|
3429
|
+
const manifest = path.join(current, 'package.json');
|
|
3430
|
+
if (existsSync(manifest)) {
|
|
3431
|
+
const value = JSON.parse(readFileSync(manifest, 'utf8'));
|
|
3432
|
+
if (value.name === 'vona-core') {
|
|
3433
|
+
const helper = path.join(current, 'src/lib/core/testClaimFilesystemWindows.ps1');
|
|
3434
|
+
if (!existsSync(helper) || !existsSync(helper.replace(/\.ps1$/, '.cs'))) throw new Error('Windows test claim filesystem helper assets are missing');
|
|
3435
|
+
return helper;
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
const parent = path.dirname(current);
|
|
3439
|
+
if (parent === current) throw new Error('Cannot locate the vona-core Windows filesystem helper');
|
|
3440
|
+
current = parent;
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
function invocation() {
|
|
3444
|
+
if (process.platform !== 'win32') throw new Error('Windows claim filesystem requires Windows');
|
|
3445
|
+
return ['-NoLogo', '-NoProfile', '-NonInteractive', '-File', getWindowsTestClaimFilesystemHelper()];
|
|
3446
|
+
}
|
|
3447
|
+
function decode(source) {
|
|
3448
|
+
let value;
|
|
3449
|
+
try {
|
|
3450
|
+
value = JSON.parse(source);
|
|
3451
|
+
} catch {
|
|
3452
|
+
throw new Error('Windows test claim filesystem helper returned an invalid response');
|
|
3453
|
+
}
|
|
3454
|
+
if (!value || value.ok !== true) {
|
|
3455
|
+
const error = new Error(value?.code === 'EEXIST' ? 'Test claim file already exists' : `Unsafe Windows test claim filesystem operation (${value?.code ?? 'unknown'})`);
|
|
3456
|
+
Object.assign(error, {
|
|
3457
|
+
code: value?.code ?? 'EUNSAFE'
|
|
3458
|
+
});
|
|
3459
|
+
throw error;
|
|
3460
|
+
}
|
|
3461
|
+
return value;
|
|
3462
|
+
}
|
|
3463
|
+
function windowsTestClaimFilesystemSync(request) {
|
|
3464
|
+
try {
|
|
3465
|
+
return decode(execFileSync('pwsh', invocation(), {
|
|
3466
|
+
input: JSON.stringify(request),
|
|
3467
|
+
encoding: 'utf8',
|
|
3468
|
+
windowsHide: true,
|
|
3469
|
+
timeout: 30000,
|
|
3470
|
+
maxBuffer: 8 * 1024 * 1024
|
|
3471
|
+
}));
|
|
3472
|
+
} catch (error) {
|
|
3473
|
+
if (error.code === 'ENOENT') throw new Error('Native Windows test claims require PowerShell 7 (pwsh) on PATH');
|
|
3474
|
+
throw error;
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
let bridge;
|
|
3478
|
+
function windowsTestClaimFilesystem(request) {
|
|
3479
|
+
if (!bridge) {
|
|
3480
|
+
// Explicit executable + argv, never a shell. A burst shares Add-Type startup;
|
|
3481
|
+
// the bridge owns no persistent native handles and exits when idle.
|
|
3482
|
+
const child = execFile('pwsh', invocation(), {
|
|
3483
|
+
windowsHide: true,
|
|
3484
|
+
maxBuffer: Infinity
|
|
3485
|
+
});
|
|
3486
|
+
// execFile's collection listeners are for its callback API. This bounded
|
|
3487
|
+
// line protocol consumes the streams itself, without retaining burst history.
|
|
3488
|
+
child.stdout?.removeAllListeners('data');
|
|
3489
|
+
child.stderr?.removeAllListeners('data');
|
|
3490
|
+
const state = {
|
|
3491
|
+
child,
|
|
3492
|
+
pending: [],
|
|
3493
|
+
output: '',
|
|
3494
|
+
idle: undefined
|
|
3495
|
+
};
|
|
3496
|
+
bridge = state;
|
|
3497
|
+
const fail = error => {
|
|
3498
|
+
if (bridge === state) bridge = undefined;
|
|
3499
|
+
clearTimeout(state.idle);
|
|
3500
|
+
for (const operation of state.pending.splice(0)) {
|
|
3501
|
+
clearTimeout(operation.timer);
|
|
3502
|
+
operation.reject(error);
|
|
3503
|
+
}
|
|
3504
|
+
child.stdin?.destroy();
|
|
3505
|
+
child.kill();
|
|
3506
|
+
};
|
|
3507
|
+
child.once('error', error => fail(new Error(error.code === 'ENOENT' ? 'Native Windows test claims require PowerShell 7 (pwsh) on PATH' : 'Windows test claim filesystem helper failed; operation not acknowledged')));
|
|
3508
|
+
child.once('exit', () => fail(new Error('Windows test claim filesystem helper exited; operation not acknowledged')));
|
|
3509
|
+
child.stdin?.on('error', () => fail(new Error('Windows test claim filesystem control stream failed')));
|
|
3510
|
+
child.stdout?.setEncoding('utf8');
|
|
3511
|
+
child.stderr?.resume();
|
|
3512
|
+
child.stdout?.on('data', chunk => {
|
|
3513
|
+
state.output += chunk;
|
|
3514
|
+
if (Buffer.byteLength(state.output) > 8 * 1024 * 1024) {
|
|
3515
|
+
fail(new Error('Windows test claim filesystem response exceeded limit'));
|
|
3516
|
+
return;
|
|
3517
|
+
}
|
|
3518
|
+
while (state.output.includes('\n')) {
|
|
3519
|
+
const end = state.output.indexOf('\n');
|
|
3520
|
+
const line = state.output.slice(0, end);
|
|
3521
|
+
state.output = state.output.slice(end + 1);
|
|
3522
|
+
const operation = state.pending.shift();
|
|
3523
|
+
if (!operation) {
|
|
3524
|
+
fail(new Error('Unexpected Windows test claim filesystem response'));
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
clearTimeout(operation.timer);
|
|
3528
|
+
try {
|
|
3529
|
+
operation.resolve(decode(line));
|
|
3530
|
+
} catch (error) {
|
|
3531
|
+
operation.reject(error);
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
if (!state.pending.length) {
|
|
3535
|
+
state.idle = setTimeout(() => {
|
|
3536
|
+
if (bridge === state) bridge = undefined;
|
|
3537
|
+
child.stdin?.end();
|
|
3538
|
+
}, 1000);
|
|
3539
|
+
}
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
const state = bridge;
|
|
3543
|
+
clearTimeout(state.idle);
|
|
3544
|
+
return new Promise((resolve, reject) => {
|
|
3545
|
+
const timer = setTimeout(() => {
|
|
3546
|
+
if (bridge === state) bridge = undefined;
|
|
3547
|
+
for (const operation of state.pending.splice(0)) {
|
|
3548
|
+
clearTimeout(operation.timer);
|
|
3549
|
+
operation.reject(new Error('Windows test claim filesystem operation timed out; not acknowledged'));
|
|
3550
|
+
}
|
|
3551
|
+
state.child.kill();
|
|
3552
|
+
}, 30000);
|
|
3553
|
+
state.pending.push({
|
|
3554
|
+
resolve,
|
|
3555
|
+
reject,
|
|
3556
|
+
timer
|
|
3557
|
+
});
|
|
3558
|
+
state.child.stdin?.write(`${JSON.stringify(request)}\n`);
|
|
3559
|
+
});
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
const TEST_CLAIM_FILE_LIMIT = 4 * 1024 * 1024;
|
|
3563
|
+
function assertTestClaimFilesystemSafePath(file, allowMissing = true) {
|
|
3564
|
+
if (process.platform === 'win32') {
|
|
3565
|
+
windowsTestClaimFilesystemSync({
|
|
3566
|
+
command: 'safe-path',
|
|
3567
|
+
path: path.resolve(file),
|
|
3568
|
+
allowMissing
|
|
3569
|
+
});
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
let current = path.resolve(file);
|
|
3573
|
+
while (true) {
|
|
3574
|
+
try {
|
|
3575
|
+
const stat = lstatSync(current);
|
|
3576
|
+
if (stat.isSymbolicLink() || current !== path.resolve(file) && !stat.isDirectory()) throw new Error('Unsafe test claim path');
|
|
3577
|
+
} catch (error) {
|
|
3578
|
+
if (!allowMissing || error.code !== 'ENOENT') throw error;
|
|
3579
|
+
}
|
|
3580
|
+
const parent = path.dirname(current);
|
|
3581
|
+
if (parent === current) break;
|
|
3582
|
+
current = parent;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
async function assertTestClaimPrivateDirectory(directory, missingAllowed = true) {
|
|
3586
|
+
if (process.platform === 'win32') {
|
|
3587
|
+
const value = await windowsTestClaimFilesystem({
|
|
3588
|
+
command: 'directory',
|
|
3589
|
+
path: path.resolve(directory),
|
|
3590
|
+
allowMissing: missingAllowed
|
|
3591
|
+
});
|
|
3592
|
+
return value.exists === true;
|
|
3593
|
+
}
|
|
3594
|
+
assertTestClaimFilesystemSafePath(directory, missingAllowed);
|
|
3595
|
+
let stat;
|
|
3596
|
+
try {
|
|
3597
|
+
stat = await lstat(directory);
|
|
3598
|
+
} catch (error) {
|
|
3599
|
+
if (missingAllowed && error.code === 'ENOENT') return false;
|
|
3600
|
+
throw error;
|
|
3601
|
+
}
|
|
3602
|
+
if (!stat.isDirectory() || (stat.mode & 0o077) !== 0 || process.getuid && stat.uid !== process.getuid()) {
|
|
3603
|
+
throw new Error('Unsafe shared test claim directory');
|
|
3604
|
+
}
|
|
3605
|
+
return true;
|
|
3606
|
+
}
|
|
3607
|
+
async function ensureTestClaimPrivateDirectory(directory) {
|
|
3608
|
+
if (process.platform === 'win32') {
|
|
3609
|
+
await windowsTestClaimFilesystem({
|
|
3610
|
+
command: 'ensure-directory',
|
|
3611
|
+
path: path.resolve(directory)
|
|
3612
|
+
});
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3615
|
+
assertTestClaimFilesystemSafePath(directory);
|
|
3616
|
+
await mkdir(directory, {
|
|
3617
|
+
mode: 0o700
|
|
3618
|
+
}).catch(error => {
|
|
3619
|
+
if (error.code !== 'EEXIST') throw error;
|
|
3620
|
+
});
|
|
3621
|
+
await assertTestClaimPrivateDirectory(directory, false);
|
|
3622
|
+
}
|
|
3623
|
+
function assertPrivateFile(file) {
|
|
3624
|
+
assertTestClaimFilesystemSafePath(file, false);
|
|
3625
|
+
const stat = lstatSync(file);
|
|
3626
|
+
if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0 || process.getuid && stat.uid !== process.getuid()) {
|
|
3627
|
+
throw new Error('Unsafe test claim file');
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
async function syncDirectory(directory) {
|
|
3631
|
+
const handle = await open(directory, constants.O_RDONLY);
|
|
3632
|
+
try {
|
|
3633
|
+
await handle.sync();
|
|
3634
|
+
} finally {
|
|
3635
|
+
await handle.close();
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
function checkContent(content) {
|
|
3639
|
+
if (Buffer.byteLength(content) > TEST_CLAIM_FILE_LIMIT) throw new Error('Invalid test claim file size');
|
|
3640
|
+
}
|
|
3641
|
+
async function readTestClaimFile(file) {
|
|
3642
|
+
if (process.platform === 'win32') {
|
|
3643
|
+
const value = await windowsTestClaimFilesystem({
|
|
3644
|
+
command: 'read',
|
|
3645
|
+
path: path.resolve(file)
|
|
3646
|
+
});
|
|
3647
|
+
if (typeof value.content !== 'string') throw new Error('Invalid test claim file response');
|
|
3648
|
+
return value.content;
|
|
3649
|
+
}
|
|
3650
|
+
assertPrivateFile(file);
|
|
3651
|
+
const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
3652
|
+
try {
|
|
3653
|
+
const stat = await handle.stat();
|
|
3654
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > TEST_CLAIM_FILE_LIMIT || (stat.mode & 0o077) !== 0 || process.getuid && stat.uid !== process.getuid()) {
|
|
3655
|
+
throw new Error('Invalid test claim file');
|
|
3656
|
+
}
|
|
3657
|
+
// Bound the allocation and bytes read even if the file grows after fstat.
|
|
3658
|
+
const buffer = Buffer.alloc(TEST_CLAIM_FILE_LIMIT + 1);
|
|
3659
|
+
let offset = 0;
|
|
3660
|
+
while (offset < buffer.length) {
|
|
3661
|
+
const {
|
|
3662
|
+
bytesRead
|
|
3663
|
+
} = await handle.read(buffer, offset, buffer.length - offset, null);
|
|
3664
|
+
if (!bytesRead) break;
|
|
3665
|
+
offset += bytesRead;
|
|
3666
|
+
}
|
|
3667
|
+
if (offset > TEST_CLAIM_FILE_LIMIT) throw new Error('Invalid test claim file size');
|
|
3668
|
+
return buffer.subarray(0, offset).toString('utf8');
|
|
3669
|
+
} finally {
|
|
3670
|
+
await handle.close();
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
async function createTestClaimFile(file, content) {
|
|
3674
|
+
checkContent(content);
|
|
3675
|
+
if (process.platform === 'win32') {
|
|
3676
|
+
await windowsTestClaimFilesystem({
|
|
3677
|
+
command: 'create',
|
|
3678
|
+
path: path.resolve(file),
|
|
3679
|
+
content
|
|
3680
|
+
});
|
|
3681
|
+
return;
|
|
3682
|
+
}
|
|
3683
|
+
assertTestClaimFilesystemSafePath(file);
|
|
3684
|
+
const handle = await open(file, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
3685
|
+
try {
|
|
3686
|
+
await handle.writeFile(content);
|
|
3687
|
+
await handle.sync();
|
|
3688
|
+
} finally {
|
|
3689
|
+
await handle.close();
|
|
3690
|
+
}
|
|
3691
|
+
await syncDirectory(path.dirname(file));
|
|
3692
|
+
}
|
|
3693
|
+
async function replaceTestClaimFile(file, content) {
|
|
3694
|
+
checkContent(content);
|
|
3695
|
+
if (process.platform === 'win32') {
|
|
3696
|
+
await windowsTestClaimFilesystem({
|
|
3697
|
+
command: 'replace',
|
|
3698
|
+
path: path.resolve(file),
|
|
3699
|
+
content
|
|
3700
|
+
});
|
|
3701
|
+
return;
|
|
3702
|
+
}
|
|
3703
|
+
const temp = `${file}.${randomBytes(12).toString('hex')}.tmp`;
|
|
3704
|
+
const handle = await open(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
3705
|
+
try {
|
|
3706
|
+
try {
|
|
3707
|
+
await handle.writeFile(content);
|
|
3708
|
+
await handle.sync();
|
|
3709
|
+
} finally {
|
|
3710
|
+
await handle.close();
|
|
3711
|
+
}
|
|
3712
|
+
assertPrivateFile(file);
|
|
3713
|
+
await rename(temp, file);
|
|
3714
|
+
await syncDirectory(path.dirname(file));
|
|
3715
|
+
} finally {
|
|
3716
|
+
await unlink(temp).catch(error => {
|
|
3717
|
+
if (error.code !== 'ENOENT') throw error;
|
|
3718
|
+
});
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3179
3722
|
const TEST_CLAIM_URL_ENV = 'VONA_TEST_CLAIM_URL';
|
|
3180
3723
|
const TEST_CLAIM_TOKEN_ENV = 'VONA_TEST_CLAIM_TOKEN';
|
|
3724
|
+
/** A locator only. Containment still requires the registered guardian's live native verifier. */
|
|
3725
|
+
const TEST_CLAIM_WINDOWS_GUARDIAN_ENV = 'VONA_TEST_WINDOWS_GUARDIAN_ID';
|
|
3181
3726
|
const TEST_CLAIM_DIRECTORY = '.vona-test-claim';
|
|
3182
3727
|
function getTestClaimPaths(projectPath, runId) {
|
|
3183
3728
|
resolveTestRunId('test', runId);
|
|
@@ -3196,20 +3741,7 @@ function getTestClaimCreatedAt(runId) {
|
|
|
3196
3741
|
return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}T${s.slice(8, 10)}:${s.slice(10, 12)}:${s.slice(12, 14)}.000Z`;
|
|
3197
3742
|
}
|
|
3198
3743
|
function assertTestClaimSafePath(file, allowMissing = true) {
|
|
3199
|
-
|
|
3200
|
-
while (true) {
|
|
3201
|
-
try {
|
|
3202
|
-
const stat = lstatSync(current);
|
|
3203
|
-
if (stat.isSymbolicLink() || current !== path.resolve(file) && !stat.isDirectory()) {
|
|
3204
|
-
throw new Error('Unsafe test claim path');
|
|
3205
|
-
}
|
|
3206
|
-
} catch (error) {
|
|
3207
|
-
if (!allowMissing || error.code !== 'ENOENT') throw error;
|
|
3208
|
-
}
|
|
3209
|
-
const parent = path.dirname(current);
|
|
3210
|
-
if (parent === current) break;
|
|
3211
|
-
current = parent;
|
|
3212
|
-
}
|
|
3744
|
+
assertTestClaimFilesystemSafePath(file, allowMissing);
|
|
3213
3745
|
}
|
|
3214
3746
|
function record(value) {
|
|
3215
3747
|
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('Invalid test claim object');
|
|
@@ -3334,16 +3866,28 @@ function validateTestClaim(value, claimPath) {
|
|
|
3334
3866
|
array(m.processes);
|
|
3335
3867
|
unique(m.processes.map(item => item.id));
|
|
3336
3868
|
for (const proc of m.processes) {
|
|
3337
|
-
keys(proc, ['id', 'role', 'stage', 'pid', 'pgid', 'startTime', 'host', 'profilePath']);
|
|
3869
|
+
keys(proc, ['id', 'role', 'stage', 'pid', 'pgid', 'containment', 'jobId', 'startTime', 'host', 'profilePath']);
|
|
3338
3870
|
label(proc.id);
|
|
3339
3871
|
oneOf(proc.role, ['guardian', 'browser', 'cleanup']);
|
|
3340
3872
|
if (proc.host !== undefined) text(proc.host);
|
|
3341
3873
|
oneOf(proc.stage, ['pending', 'registered', 'drained']);
|
|
3874
|
+
if (proc.containment !== undefined) oneOf(proc.containment, ['windows-job']);
|
|
3875
|
+
if (proc.containment === 'windows-job') {
|
|
3876
|
+
if (proc.pgid !== undefined || proc.role !== 'guardian' || proc.profilePath !== undefined) throw new Error('Invalid Windows test claim containment');
|
|
3877
|
+
text(proc.jobId);
|
|
3878
|
+
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(proc.jobId)) throw new Error('Invalid Windows test claim job identity');
|
|
3879
|
+
} else if (proc.jobId !== undefined) {
|
|
3880
|
+
throw new Error('Windows test claim job needs explicit containment');
|
|
3881
|
+
}
|
|
3342
3882
|
const hasIdentity = proc.pid !== undefined || proc.pgid !== undefined || proc.startTime !== undefined;
|
|
3343
3883
|
if (hasIdentity || proc.stage !== 'pending') {
|
|
3344
3884
|
integer(proc.pid, 1);
|
|
3345
|
-
integer(proc.pgid, 1);
|
|
3885
|
+
if (proc.containment !== 'windows-job') integer(proc.pgid, 1);
|
|
3346
3886
|
text(proc.startTime);
|
|
3887
|
+
if (proc.containment === 'windows-job') {
|
|
3888
|
+
if (!/^[1-9]\d{0,19}$/.test(proc.startTime)) throw new Error('Invalid Windows test claim process creation time');
|
|
3889
|
+
text(proc.host);
|
|
3890
|
+
}
|
|
3347
3891
|
}
|
|
3348
3892
|
if (proc.profilePath !== undefined) {
|
|
3349
3893
|
text(proc.profilePath);
|
|
@@ -3356,23 +3900,8 @@ function validateTestClaim(value, claimPath) {
|
|
|
3356
3900
|
for (const key of ['processes', 'databases', 'redis', 'locals']) bool(m.cleanup[key]);
|
|
3357
3901
|
return m;
|
|
3358
3902
|
}
|
|
3359
|
-
function assertPrivateFile(file) {
|
|
3360
|
-
assertTestClaimSafePath(file, false);
|
|
3361
|
-
const stat = lstatSync(file);
|
|
3362
|
-
if (!stat.isFile() || stat.nlink !== 1 || (stat.mode & 0o077) !== 0 || process.getuid && stat.uid !== process.getuid()) {
|
|
3363
|
-
throw new Error('Unsafe test claim file');
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
3903
|
async function loadTestClaim(claimPath) {
|
|
3367
|
-
|
|
3368
|
-
const handle = await open(claimPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
3369
|
-
try {
|
|
3370
|
-
const stat = await handle.stat();
|
|
3371
|
-
if (!stat.isFile() || stat.size > 4 * 1024 * 1024) throw new Error('Invalid test claim file');
|
|
3372
|
-
return validateTestClaim(parseTestClaimJson(await handle.readFile('utf8')), claimPath);
|
|
3373
|
-
} finally {
|
|
3374
|
-
await handle.close();
|
|
3375
|
-
}
|
|
3904
|
+
return validateTestClaim(parseTestClaimJson(await readTestClaimFile(claimPath)), claimPath);
|
|
3376
3905
|
}
|
|
3377
3906
|
function parseTestClaimJson(source) {
|
|
3378
3907
|
// Claim identities stay literal even when @cabloy/json5 revives ISO strings.
|
|
@@ -3380,31 +3909,13 @@ function parseTestClaimJson(source) {
|
|
|
3380
3909
|
return this[key];
|
|
3381
3910
|
});
|
|
3382
3911
|
}
|
|
3383
|
-
async function syncDirectory(directory) {
|
|
3384
|
-
const handle = await open(directory, constants.O_RDONLY);
|
|
3385
|
-
try {
|
|
3386
|
-
await handle.sync();
|
|
3387
|
-
} finally {
|
|
3388
|
-
await handle.close();
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3391
3912
|
async function createTestClaim(projectPath, runId, kind, owner) {
|
|
3392
3913
|
const p = getTestClaimPaths(projectPath, runId);
|
|
3393
|
-
|
|
3394
|
-
await mkdir(p.claimDirectory, {
|
|
3395
|
-
mode: 0o700
|
|
3396
|
-
}).catch(error => {
|
|
3397
|
-
if (error.code !== 'EEXIST') throw error;
|
|
3398
|
-
});
|
|
3914
|
+
await ensureTestClaimPrivateDirectory(p.claimDirectory);
|
|
3399
3915
|
// The directory is shared across runs; an existing directory is valid, never an existing claim.
|
|
3400
3916
|
return createClaimFile(p, runId, kind, owner);
|
|
3401
3917
|
}
|
|
3402
3918
|
async function createClaimFile(p, runId, kind, owner) {
|
|
3403
|
-
assertTestClaimSafePath(p.claimDirectory, false);
|
|
3404
|
-
const directoryStat = lstatSync(p.claimDirectory);
|
|
3405
|
-
if (!directoryStat.isDirectory() || (directoryStat.mode & 0o077) !== 0 || process.getuid && directoryStat.uid !== process.getuid()) {
|
|
3406
|
-
throw new Error('Unsafe shared test claim directory');
|
|
3407
|
-
}
|
|
3408
3919
|
const manifest = {
|
|
3409
3920
|
version: 1,
|
|
3410
3921
|
runId,
|
|
@@ -3433,14 +3944,7 @@ async function createClaimFile(p, runId, kind, owner) {
|
|
|
3433
3944
|
}
|
|
3434
3945
|
};
|
|
3435
3946
|
validateTestClaim(manifest, p.claimPath);
|
|
3436
|
-
|
|
3437
|
-
try {
|
|
3438
|
-
await handle.writeFile(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
3439
|
-
await handle.sync();
|
|
3440
|
-
} finally {
|
|
3441
|
-
await handle.close();
|
|
3442
|
-
}
|
|
3443
|
-
await syncDirectory(p.claimDirectory);
|
|
3947
|
+
await createTestClaimFile(p.claimPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
3444
3948
|
return {
|
|
3445
3949
|
claimPath: p.claimPath,
|
|
3446
3950
|
manifest
|
|
@@ -3454,23 +3958,7 @@ async function writeTestClaim(claimPath, manifest) {
|
|
|
3454
3958
|
for (const key of ['version', 'runId', 'createdAt', 'projectPath', 'kind', 'owner']) {
|
|
3455
3959
|
if (!isDeepStrictEqual(previous[key], manifest[key])) throw new Error('Test claim identity is immutable');
|
|
3456
3960
|
}
|
|
3457
|
-
|
|
3458
|
-
const handle = await open(temp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
3459
|
-
try {
|
|
3460
|
-
try {
|
|
3461
|
-
await handle.writeFile(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
3462
|
-
await handle.sync();
|
|
3463
|
-
} finally {
|
|
3464
|
-
await handle.close();
|
|
3465
|
-
}
|
|
3466
|
-
assertPrivateFile(claimPath);
|
|
3467
|
-
await rename(temp, claimPath);
|
|
3468
|
-
await syncDirectory(path.dirname(claimPath));
|
|
3469
|
-
} finally {
|
|
3470
|
-
await unlink(temp).catch(error => {
|
|
3471
|
-
if (error.code !== 'ENOENT') throw error;
|
|
3472
|
-
});
|
|
3473
|
-
}
|
|
3961
|
+
await replaceTestClaimFile(claimPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
3474
3962
|
}
|
|
3475
3963
|
function applyEvent(m, event) {
|
|
3476
3964
|
record(event);
|
|
@@ -3497,7 +3985,9 @@ function applyEvent(m, event) {
|
|
|
3497
3985
|
if (event.process.stage !== 'pending') throw new Error('Process needs a write-ahead intent');
|
|
3498
3986
|
m.processes.push(event.process);
|
|
3499
3987
|
} else {
|
|
3500
|
-
if (old.role !== event.process.role
|
|
3988
|
+
if (old.role !== event.process.role || old.containment !== event.process.containment || old.jobId !== event.process.jobId) {
|
|
3989
|
+
throw new Error('Test claim process identity changed');
|
|
3990
|
+
}
|
|
3501
3991
|
if (event.process.profilePath !== undefined && old.profilePath !== event.process.profilePath) {
|
|
3502
3992
|
if (old.stage !== 'pending' || event.process.stage !== 'registered' || !old.profilePath || !event.process.profilePath.startsWith(`${old.profilePath}${path.sep}`)) {
|
|
3503
3993
|
throw new Error('Test claim process profile changed');
|
|
@@ -3578,11 +4068,79 @@ function applyEvent(m, event) {
|
|
|
3578
4068
|
}
|
|
3579
4069
|
validateTestClaim(m);
|
|
3580
4070
|
}
|
|
4071
|
+
// Native verifiers are controller-local capabilities, never HTTP registrations or journal data.
|
|
4072
|
+
const liveWindowsBrokers = new Map();
|
|
4073
|
+
function validateWindowsJobQuery(value) {
|
|
4074
|
+
keys(value, ['runId', 'guardianId', 'process']);
|
|
4075
|
+
const query = value;
|
|
4076
|
+
resolveTestRunId('test', query.runId);
|
|
4077
|
+
label(query.guardianId);
|
|
4078
|
+
keys(query.process, ['pid', 'startTime', 'host']);
|
|
4079
|
+
integer(query.process.pid, 1, 0xffffffff);
|
|
4080
|
+
text(query.process.startTime);
|
|
4081
|
+
if (!/^[1-9]\d{0,19}$/.test(query.process.startTime)) throw new Error('Invalid Windows containment creation time');
|
|
4082
|
+
text(query.process.host);
|
|
4083
|
+
}
|
|
4084
|
+
|
|
4085
|
+
/** Only the controller hosting this authenticated broker can attach its sole native Job owner. */
|
|
4086
|
+
async function registerTestClaimWindowsJobVerifier(env, guardian, verify) {
|
|
4087
|
+
if (!hasTestClaimTransport(env)) throw new Error('Windows containment requires a claim broker');
|
|
4088
|
+
if (!resolveTestRunId(env.META_MODE, env.VONA_TEST_RUN_ID)) throw new Error('Windows containment requires test identity');
|
|
4089
|
+
const broker = liveWindowsBrokers.get(env[TEST_CLAIM_TOKEN_ENV]);
|
|
4090
|
+
if (!broker || broker.url !== env[TEST_CLAIM_URL_ENV]) throw new Error('Windows containment verifier must belong to the local claim broker');
|
|
4091
|
+
return await broker.register(guardian, verify);
|
|
4092
|
+
}
|
|
3581
4093
|
async function startTestClaimBroker(claimPath) {
|
|
3582
4094
|
await loadTestClaim(claimPath);
|
|
3583
4095
|
const token = randomBytes(32).toString('hex');
|
|
3584
4096
|
let queue = Promise.resolve();
|
|
3585
4097
|
let closing = false;
|
|
4098
|
+
const verifiers = new Map();
|
|
4099
|
+
let queries = 0;
|
|
4100
|
+
const containment = async query => {
|
|
4101
|
+
validateWindowsJobQuery(query);
|
|
4102
|
+
if (closing || queries >= 32) throw new Error('Windows containment verifier unavailable');
|
|
4103
|
+
queries++;
|
|
4104
|
+
try {
|
|
4105
|
+
await queue;
|
|
4106
|
+
const manifest = await loadTestClaim(claimPath);
|
|
4107
|
+
const live = verifiers.get(query.guardianId);
|
|
4108
|
+
const guardian = manifest.processes.find(item => item.id === query.guardianId);
|
|
4109
|
+
if (manifest.kind !== 'e2e' || manifest.runId !== query.runId || guardian?.stage !== 'registered' || guardian.role !== 'guardian' || guardian.containment !== 'windows-job' || guardian.host !== query.process.host || !live || !isDeepStrictEqual(live.guardian, guardian)) {
|
|
4110
|
+
throw new Error('Windows containment requires a registered live E2E guardian');
|
|
4111
|
+
}
|
|
4112
|
+
let timer;
|
|
4113
|
+
try {
|
|
4114
|
+
const verified = await Promise.race([live.verify(query.process), new Promise((_, reject) => {
|
|
4115
|
+
timer = setTimeout(() => reject(new Error('Windows containment query timed out')), 5000);
|
|
4116
|
+
})]);
|
|
4117
|
+
await queue;
|
|
4118
|
+
const current = await loadTestClaim(claimPath);
|
|
4119
|
+
if (!verified || closing || verifiers.get(query.guardianId) !== live || !isDeepStrictEqual(current.processes.find(item => item.id === query.guardianId), guardian)) {
|
|
4120
|
+
throw new Error('Windows process is not in the designated live Job');
|
|
4121
|
+
}
|
|
4122
|
+
} finally {
|
|
4123
|
+
clearTimeout(timer);
|
|
4124
|
+
}
|
|
4125
|
+
} finally {
|
|
4126
|
+
queries--;
|
|
4127
|
+
}
|
|
4128
|
+
};
|
|
4129
|
+
const register = async (guardian, verify) => {
|
|
4130
|
+
await queue;
|
|
4131
|
+
const manifest = await loadTestClaim(claimPath);
|
|
4132
|
+
if (closing || guardian.role !== 'guardian' || guardian.stage !== 'registered' || guardian.containment !== 'windows-job' || typeof verify !== 'function' || verifiers.has(guardian.id) || !isDeepStrictEqual(manifest.processes.find(item => item.id === guardian.id), guardian)) {
|
|
4133
|
+
throw new Error('Windows containment verifier needs a durable registered guardian');
|
|
4134
|
+
}
|
|
4135
|
+
const live = {
|
|
4136
|
+
guardian: structuredClone(guardian),
|
|
4137
|
+
verify
|
|
4138
|
+
};
|
|
4139
|
+
verifiers.set(guardian.id, live);
|
|
4140
|
+
return () => {
|
|
4141
|
+
if (verifiers.get(guardian.id) === live) verifiers.delete(guardian.id);
|
|
4142
|
+
};
|
|
4143
|
+
};
|
|
3586
4144
|
const update = event => {
|
|
3587
4145
|
if (closing) return Promise.reject(new Error('Test claim broker is closed'));
|
|
3588
4146
|
const result = queue.then(async () => {
|
|
@@ -3596,7 +4154,7 @@ async function startTestClaimBroker(claimPath) {
|
|
|
3596
4154
|
const server = createServer(async (req, res) => {
|
|
3597
4155
|
const auth = req.headers.authorization;
|
|
3598
4156
|
const expected = `Bearer ${token}`;
|
|
3599
|
-
if (req.method !== 'POST' ||
|
|
4157
|
+
if (req.method !== 'POST' || !['/event', '/containment'].includes(req.url) || typeof auth !== 'string' || Buffer.byteLength(auth) !== Buffer.byteLength(expected) || !timingSafeEqual(Buffer.from(auth), Buffer.from(expected))) {
|
|
3600
4158
|
res.writeHead(403).end();
|
|
3601
4159
|
req.resume();
|
|
3602
4160
|
return;
|
|
@@ -3607,7 +4165,7 @@ async function startTestClaimBroker(claimPath) {
|
|
|
3607
4165
|
body += chunk.toString('utf8');
|
|
3608
4166
|
if (Buffer.byteLength(body) > 1024 * 1024) throw new Error('Test claim event too large');
|
|
3609
4167
|
}
|
|
3610
|
-
await update(parseTestClaimJson(body));
|
|
4168
|
+
if (req.url === '/containment') await containment(parseTestClaimJson(body));else await update(parseTestClaimJson(body));
|
|
3611
4169
|
res.writeHead(204).end();
|
|
3612
4170
|
} catch {
|
|
3613
4171
|
res.writeHead(409).end('Test claim registration rejected');
|
|
@@ -3621,14 +4179,21 @@ async function startTestClaimBroker(claimPath) {
|
|
|
3621
4179
|
});
|
|
3622
4180
|
const address = server.address();
|
|
3623
4181
|
if (!address || typeof address === 'string') throw new Error('Cannot bind test claim broker');
|
|
4182
|
+
const url = `http://127.0.0.1:${address.port}/event`;
|
|
4183
|
+
liveWindowsBrokers.set(token, {
|
|
4184
|
+
url,
|
|
4185
|
+
register
|
|
4186
|
+
});
|
|
3624
4187
|
return {
|
|
3625
4188
|
env: {
|
|
3626
|
-
[TEST_CLAIM_URL_ENV]:
|
|
4189
|
+
[TEST_CLAIM_URL_ENV]: url,
|
|
3627
4190
|
[TEST_CLAIM_TOKEN_ENV]: token
|
|
3628
4191
|
},
|
|
3629
4192
|
update,
|
|
3630
4193
|
async close() {
|
|
3631
4194
|
closing = true;
|
|
4195
|
+
liveWindowsBrokers.delete(token);
|
|
4196
|
+
verifiers.clear();
|
|
3632
4197
|
await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
|
|
3633
4198
|
await queue;
|
|
3634
4199
|
}
|
|
@@ -3669,6 +4234,36 @@ async function sendTestClaimEvent(env, event) {
|
|
|
3669
4234
|
throw new Error('Managed test allocation registration failed');
|
|
3670
4235
|
}
|
|
3671
4236
|
}
|
|
4237
|
+
|
|
4238
|
+
/** A transient authenticated gate: success is live exact membership, never a durable process event. */
|
|
4239
|
+
async function assertTestClaimWindowsJob(env, identity) {
|
|
4240
|
+
try {
|
|
4241
|
+
if (!hasTestClaimTransport(env)) throw new Error('Missing test claim transport');
|
|
4242
|
+
const query = {
|
|
4243
|
+
runId: env.VONA_TEST_RUN_ID,
|
|
4244
|
+
guardianId: env[TEST_CLAIM_WINDOWS_GUARDIAN_ENV],
|
|
4245
|
+
process: {
|
|
4246
|
+
pid: identity.pid,
|
|
4247
|
+
startTime: identity.startTime,
|
|
4248
|
+
host: identity.host
|
|
4249
|
+
}
|
|
4250
|
+
};
|
|
4251
|
+
validateWindowsJobQuery(query);
|
|
4252
|
+
const response = await fetch(new URL('/containment', env[TEST_CLAIM_URL_ENV]), {
|
|
4253
|
+
method: 'POST',
|
|
4254
|
+
headers: {
|
|
4255
|
+
'authorization': `Bearer ${env[TEST_CLAIM_TOKEN_ENV]}`,
|
|
4256
|
+
'content-type': 'application/json'
|
|
4257
|
+
},
|
|
4258
|
+
body: JSON.stringify(query),
|
|
4259
|
+
signal: AbortSignal.timeout(7000),
|
|
4260
|
+
redirect: 'error'
|
|
4261
|
+
});
|
|
4262
|
+
if (response.status !== 204) throw new Error('Containment rejected');
|
|
4263
|
+
} catch {
|
|
4264
|
+
throw new Error('Managed Windows browser containment verification failed');
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
3672
4267
|
function getTestClaimRedisPrefix(runId, clientName) {
|
|
3673
4268
|
const appName = getTestAppName(runId);
|
|
3674
4269
|
if (clientName === 'broadcast') return undefined;
|
|
@@ -3887,6 +4482,164 @@ function assertTestClaimRedisClient(app, clientName, options) {
|
|
|
3887
4482
|
if (plan.kind === 'client' ? options.keyPrefix !== plan.prefix : options.keyPrefix !== undefined) throw new Error('Unregistered managed Redis key prefix');
|
|
3888
4483
|
}
|
|
3889
4484
|
|
|
4485
|
+
const TEST_REQUIRE_FRESH_RESOURCES_ENV = 'VONA_TEST_REQUIRE_FRESH_RESOURCES';
|
|
4486
|
+
const freshApps = new WeakSet();
|
|
4487
|
+
function requiresFreshTestResources() {
|
|
4488
|
+
const value = process.env[TEST_REQUIRE_FRESH_RESOURCES_ENV];
|
|
4489
|
+
if (value !== undefined && value !== 'true') {
|
|
4490
|
+
throw new Error('Invalid strict test resource freshness flag');
|
|
4491
|
+
}
|
|
4492
|
+
return value === 'true';
|
|
4493
|
+
}
|
|
4494
|
+
function assertFreshTestResourcesReady(app) {
|
|
4495
|
+
if (!requiresFreshTestResources()) return;
|
|
4496
|
+
if (!freshApps.has(app)) throw new Error('Strict test resource freshness was not established');
|
|
4497
|
+
}
|
|
4498
|
+
async function assertEmptyDirectory(directory) {
|
|
4499
|
+
assertIsolatedTestPath(directory, directory);
|
|
4500
|
+
try {
|
|
4501
|
+
if (!(await lstat(directory)).isDirectory() || (await readdir(directory)).length !== 0) {
|
|
4502
|
+
throw new Error('Strict test local resource is not fresh');
|
|
4503
|
+
}
|
|
4504
|
+
} catch (error) {
|
|
4505
|
+
if (error.code !== 'ENOENT') throw error;
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
// Runs after configuration, before module loading (including worker-alive Redis writes).
|
|
4510
|
+
// This is a freshness snapshot, not a lease against independent external writers.
|
|
4511
|
+
async function assertFreshTestResources(app, dependencies) {
|
|
4512
|
+
if (!requiresFreshTestResources()) return;
|
|
4513
|
+
if (!app.meta.env.VONA_TEST_RUN_ID || app.meta.appReady || freshApps.has(app)) {
|
|
4514
|
+
throw new Error('Strict test freshness requires a new isolated startup');
|
|
4515
|
+
}
|
|
4516
|
+
assertIsolatedTestConfig(app);
|
|
4517
|
+
const config = app.config;
|
|
4518
|
+
const paths = resolveVonaLocalPaths(app.projectPath, 'test', app.meta.env.VONA_TEST_RUN_ID);
|
|
4519
|
+
for (const directory of [paths.publicRoot, paths.runtimeRoot, paths.sqliteRoot]) {
|
|
4520
|
+
await assertEmptyDirectory(directory);
|
|
4521
|
+
}
|
|
4522
|
+
const inventory = buildTestClaimResourceInventory(app);
|
|
4523
|
+
const readers = dependencies ?? createFreshnessDependencies(app.projectPath);
|
|
4524
|
+
try {
|
|
4525
|
+
for (const plan of inventory.databases) {
|
|
4526
|
+
if (plan.endpoint.dialect === 'better-sqlite3') continue;
|
|
4527
|
+
const options = mergeTestClaimDatabaseOptions(config.database.base, undefined, config.database.clients[plan.clientName]);
|
|
4528
|
+
if (await readers.databaseExists(plan, options)) {
|
|
4529
|
+
throw new Error('Preexisting strict test database namespace');
|
|
4530
|
+
}
|
|
4531
|
+
}
|
|
4532
|
+
for (const resource of inventory.redis) {
|
|
4533
|
+
const options = mergeTestClaimRedisOptions(config.redis.base, config.redis.clients[resource.clientName]);
|
|
4534
|
+
if (await readers.redisExists(resource, options)) {
|
|
4535
|
+
throw new Error('Preexisting strict test Redis namespace');
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
} catch {
|
|
4539
|
+
// Driver messages may embed credentials, database names or Redis keys.
|
|
4540
|
+
throw new Error('Strict test resource freshness check failed');
|
|
4541
|
+
}
|
|
4542
|
+
freshApps.add(app);
|
|
4543
|
+
}
|
|
4544
|
+
async function hasFreshnessDatabase(connection, plan) {
|
|
4545
|
+
try {
|
|
4546
|
+
const pg = plan.endpoint.dialect === 'pg';
|
|
4547
|
+
if (!pg && !['mysql', 'mysql2'].includes(plan.endpoint.dialect)) {
|
|
4548
|
+
throw new Error('Unsupported freshness database dialect');
|
|
4549
|
+
}
|
|
4550
|
+
const result = await connection.raw(pg ? 'select datname from pg_database where left(datname, ?) = ? limit 1' : 'select schema_name from information_schema.schemata where left(schema_name, ?) = ? limit 1', [plan.prefix.length, plan.prefix]);
|
|
4551
|
+
const rows = pg ? result?.rows : result?.[0];
|
|
4552
|
+
if (!Array.isArray(rows)) throw new Error('Invalid freshness database response');
|
|
4553
|
+
return rows.length !== 0;
|
|
4554
|
+
} finally {
|
|
4555
|
+
await connection.destroy();
|
|
4556
|
+
}
|
|
4557
|
+
}
|
|
4558
|
+
async function hasFreshnessRedis(connection, prefix, maxScans = 10000) {
|
|
4559
|
+
if (!prefix || !Number.isSafeInteger(maxScans) || maxScans < 1) {
|
|
4560
|
+
throw new Error('Invalid Redis freshness bounds');
|
|
4561
|
+
}
|
|
4562
|
+
const pattern = `${prefix.replace(/[\\*?[\]]/g, '\\$&')}*`;
|
|
4563
|
+
let cursor = '0';
|
|
4564
|
+
for (let scans = 0; scans < maxScans; scans++) {
|
|
4565
|
+
const result = await connection.scan(cursor, 'MATCH', pattern, 'COUNT', 128);
|
|
4566
|
+
if (!Array.isArray(result) || result.length !== 2 || typeof result[0] !== 'string' || !/^\d+$/.test(result[0]) || !Array.isArray(result[1]) || result[1].some(key => typeof key !== 'string' || !key.startsWith(prefix))) {
|
|
4567
|
+
throw new Error('Invalid Redis freshness response');
|
|
4568
|
+
}
|
|
4569
|
+
if (result[1].length !== 0) return true;
|
|
4570
|
+
cursor = result[0];
|
|
4571
|
+
if (cursor === '0') return false;
|
|
4572
|
+
}
|
|
4573
|
+
throw new Error('Redis freshness scan limit exceeded');
|
|
4574
|
+
}
|
|
4575
|
+
function createFreshnessDependencies(projectPath, load = createRequire(path.join(projectPath, 'package.json'))) {
|
|
4576
|
+
return {
|
|
4577
|
+
async databaseExists(plan, options) {
|
|
4578
|
+
const endpoint = plan.endpoint;
|
|
4579
|
+
const source = options.connection;
|
|
4580
|
+
const target = {
|
|
4581
|
+
user: source.user,
|
|
4582
|
+
password: source.password,
|
|
4583
|
+
ssl: source.ssl,
|
|
4584
|
+
database: endpoint.adminDatabase,
|
|
4585
|
+
connectionTimeoutMillis: 10000,
|
|
4586
|
+
connectTimeout: 10000
|
|
4587
|
+
};
|
|
4588
|
+
if (endpoint.transport === 'tcp') {
|
|
4589
|
+
target.host = endpoint.host;
|
|
4590
|
+
target.port = endpoint.port;
|
|
4591
|
+
} else if (endpoint.dialect === 'pg') {
|
|
4592
|
+
target.host = endpoint.socketPath;
|
|
4593
|
+
target.port = endpoint.port;
|
|
4594
|
+
} else {
|
|
4595
|
+
target.socketPath = endpoint.socketPath;
|
|
4596
|
+
}
|
|
4597
|
+
const knex = load('knex');
|
|
4598
|
+
return await hasFreshnessDatabase(knex({
|
|
4599
|
+
client: endpoint.dialect,
|
|
4600
|
+
connection: target,
|
|
4601
|
+
pool: {
|
|
4602
|
+
min: 0,
|
|
4603
|
+
max: 1
|
|
4604
|
+
},
|
|
4605
|
+
acquireConnectionTimeout: 10000
|
|
4606
|
+
}), plan);
|
|
4607
|
+
},
|
|
4608
|
+
async redisExists(resource, options) {
|
|
4609
|
+
const endpoint = resource.endpoint;
|
|
4610
|
+
const target = endpoint.transport === 'socket' ? {
|
|
4611
|
+
path: endpoint.socketPath
|
|
4612
|
+
} : {
|
|
4613
|
+
host: endpoint.host,
|
|
4614
|
+
port: endpoint.port
|
|
4615
|
+
};
|
|
4616
|
+
const Redis = load('ioredis').Redis;
|
|
4617
|
+
const connection = new Redis({
|
|
4618
|
+
...target,
|
|
4619
|
+
username: options.username,
|
|
4620
|
+
password: options.password,
|
|
4621
|
+
tls: options.tls,
|
|
4622
|
+
db: endpoint.db,
|
|
4623
|
+
keyPrefix: undefined,
|
|
4624
|
+
lazyConnect: true,
|
|
4625
|
+
connectTimeout: 10000,
|
|
4626
|
+
commandTimeout: 10000,
|
|
4627
|
+
retryStrategy: () => null,
|
|
4628
|
+
maxRetriesPerRequest: 0,
|
|
4629
|
+
enableOfflineQueue: false
|
|
4630
|
+
});
|
|
4631
|
+
connection.on('error', () => {});
|
|
4632
|
+
try {
|
|
4633
|
+
await connection.connect();
|
|
4634
|
+
const prefix = resource.kind === 'queue' ? resource.prefix.slice(0, -'queue:'.length) : resource.prefix;
|
|
4635
|
+
return await hasFreshnessRedis(connection, prefix);
|
|
4636
|
+
} finally {
|
|
4637
|
+
connection.disconnect();
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
};
|
|
4641
|
+
}
|
|
4642
|
+
|
|
3890
4643
|
function jsxEnhance() {
|
|
3891
4644
|
const createElementOriginal = React.createElement;
|
|
3892
4645
|
React.createElement = function (...args) {
|
|
@@ -4106,6 +4859,7 @@ class Start {
|
|
|
4106
4859
|
const app = this.app;
|
|
4107
4860
|
try {
|
|
4108
4861
|
await this._start_appConfig();
|
|
4862
|
+
await assertFreshTestResources(app);
|
|
4109
4863
|
await this._start_appLoad();
|
|
4110
4864
|
await this._start_appStart();
|
|
4111
4865
|
await this._start_appReady();
|
|
@@ -4325,13 +5079,16 @@ async function _closeInner() {
|
|
|
4325
5079
|
clearTimeout(timeout);
|
|
4326
5080
|
process.exit(err ? 1 : 0);
|
|
4327
5081
|
}
|
|
5082
|
+
|
|
5083
|
+
// The authenticated test health endpoint uses the same guard and cleanup as signals.
|
|
5084
|
+
async function closeProcessWorker() {
|
|
5085
|
+
if (__closing) return;
|
|
5086
|
+
__closing = true;
|
|
5087
|
+
await _closeInner();
|
|
5088
|
+
}
|
|
4328
5089
|
function handleProcessWork() {
|
|
4329
5090
|
['SIGINT', 'SIGTERM', 'SIGUSR2'].forEach(signal => {
|
|
4330
|
-
process.on(signal,
|
|
4331
|
-
if (__closing) return;
|
|
4332
|
-
__closing = true;
|
|
4333
|
-
await _closeInner();
|
|
4334
|
-
});
|
|
5091
|
+
process.on(signal, closeProcessWorker);
|
|
4335
5092
|
});
|
|
4336
5093
|
process.on('uncaughtException', async err => {
|
|
4337
5094
|
const app = useApp();
|
|
@@ -4492,5 +5249,5 @@ async function retry(options, fn) {
|
|
|
4492
5249
|
|
|
4493
5250
|
zodExtendOpenApi();
|
|
4494
5251
|
|
|
4495
|
-
export { $Class, $localeScope, $makeLocaleMagic, $protocolKey, AppHmr, AppHmrDeps, AppLocale, AppLogger, AppMeta, AppMetadata, AppResource, AppUtil, BeanAopBase, BeanAopMethodBase, BeanBase, BeanBaseSimple, BeanContainer, BeanInfo, BeanScopeBase, BeanScopeContainer, BeanScopeError, BeanScopeErrorImpl, BeanScopeLocale, BeanScopeScene, BeanScopeUtil, BeanSimple, EnumAppEvent, ErrorClass, Global, LocaleModuleNameSeparator, PickClassInner, ProxyDisable, SymbolBeanContainerInstances, SymbolBeanFullName, SymbolBeanInstanceKey, SymbolBeanInstancePropsLazy, SymbolCacheAopChains, SymbolCacheAopChainsKey, SymbolDecoratorBeanFullName, SymbolDecoratorBeanInfo, SymbolDecoratorGlobal, SymbolDecoratorProxyDisable, SymbolDecoratorUse, SymbolDecoratorVirtual, SymbolHmrStateLoad, SymbolHmrStateSave, SymbolMappedClassMetadataKeys, SymbolModuleBelong, SymbolModuleName, TEST_CLAIM_DIRECTORY, TEST_CLAIM_TOKEN_ENV, TEST_CLAIM_URL_ENV, TEST_RUN_ID_ENV, TEST_RUN_OWNER_FILE, Use, Virtual, VonaApplication, __prepareInjectSelectorInfo, appHmrDeps, appMetadata, appResource, assertDisposableDatabaseName, assertIsolatedTestConfig, assertIsolatedTestDatabaseClient, assertIsolatedTestPath, assertTestAppIdentity, assertTestClaimDatabaseClient, assertTestClaimRedisClient, assertTestClaimSafePath, assertTestRunOwner, beanFullNameFromOnionName, bootstrap, buildTestClaimResourceInventory, cast, closeApp, combineConfigDefault, combineFilePathSafe, compose, copyAddon, copyMetadataOfClasses, copyProperties, copyPropertiesOfClasses, createApp, createAppMaster, createBeanDecorator, createGeneralApp, createHash, createTestClaim, deepExtend, disposeInstance, errorsInternal, filterHeaders, formatLoggerAxiosError, formatLoggerConsole, formatLoggerCtx, formatLoggerDummy, formatLoggerErrors, formatLoggerFilter, functionNoop, getDatabaseTestPrefix, getHomeVonaAppDir, getLoggerPathPhysicalRoot, getMappedClassMetadataKeys, getPlatformArch, getPublicPathPhysicalRoot, getRuntimePathPhysicalRoot, getSqlite3DatabaseNameDefault, getTestAppName, getTestClaimCreatedAt, getTestClaimPaths, getTestClaimRedisPrefix, getTestRunIdFromEnv, hasTestClaimTransport, instanceDesp, isLocaleMagic, loadJSONFile, loadTestClaim, mergeTestClaimDatabaseOptions, mergeTestClaimRedisOptions, normalizeTestClaimDatabaseEndpoint, normalizeTestClaimRedisEndpoint, onionNameFromBeanFullName, pathToHref, polyfillDispose, prepareEnv, registerMappedClassMetadataKey, registerTestClaimDatabaseCreation, registerTestClaimResourceInventory, requireAddon, requireDynamic, resolveAddon, resolveTestRunId, resolveVonaLocalPaths, resolverSharp, resolverSharpLibvips, resolverSqlite3, retry, saveJSONFile, sendTestClaimEvent, setMappedClassMetadataKeys, startTestClaimBroker, text$1 as text, useApp, usePrepareArg, usePrepareArgs, uuidv4, validateTestClaim, withIsolatedTestDatabaseDiscovery, writeTestClaim };
|
|
5252
|
+
export { $Class, $localeScope, $makeLocaleMagic, $protocolKey, AppHmr, AppHmrDeps, AppLocale, AppLogger, AppMeta, AppMetadata, AppResource, AppUtil, BeanAopBase, BeanAopMethodBase, BeanBase, BeanBaseSimple, BeanContainer, BeanInfo, BeanScopeBase, BeanScopeContainer, BeanScopeError, BeanScopeErrorImpl, BeanScopeLocale, BeanScopeScene, BeanScopeUtil, BeanSimple, EnumAppEvent, ErrorClass, Global, LocaleModuleNameSeparator, PickClassInner, ProxyDisable, SymbolBeanContainerInstances, SymbolBeanFullName, SymbolBeanInstanceKey, SymbolBeanInstancePropsLazy, SymbolCacheAopChains, SymbolCacheAopChainsKey, SymbolDecoratorBeanFullName, SymbolDecoratorBeanInfo, SymbolDecoratorGlobal, SymbolDecoratorProxyDisable, SymbolDecoratorUse, SymbolDecoratorVirtual, SymbolHmrStateLoad, SymbolHmrStateSave, SymbolMappedClassMetadataKeys, SymbolModuleBelong, SymbolModuleName, TEST_CLAIM_DIRECTORY, TEST_CLAIM_TOKEN_ENV, TEST_CLAIM_URL_ENV, TEST_CLAIM_WINDOWS_GUARDIAN_ENV, TEST_REQUIRE_FRESH_RESOURCES_ENV, TEST_RUN_ID_ENV, TEST_RUN_OWNER_FILE, Use, Virtual, VonaApplication, __prepareInjectSelectorInfo, appHmrDeps, appMetadata, appResource, assertDisposableDatabaseName, assertFreshTestResources, assertFreshTestResourcesReady, assertIsolatedTestConfig, assertIsolatedTestDatabaseClient, assertIsolatedTestPath, assertTestAppIdentity, assertTestClaimDatabaseClient, assertTestClaimRedisClient, assertTestClaimSafePath, assertTestClaimWindowsJob, assertTestRunOwner, beanFullNameFromOnionName, bootstrap, buildTestClaimResourceInventory, captureRequestBoundary, cast, closeApp, closeProcessWorker, combineConfigDefault, combineFilePathSafe, compose, copyAddon, copyMetadataOfClasses, copyProperties, copyPropertiesOfClasses, createApp, createAppMaster, createBeanDecorator, createDiagnosticLogger, createFreshnessDependencies, createGeneralApp, createHash, createRequestBoundary, createTestClaim, deepExtend, disposeInstance, errorsInternal, filterHeaders, formatLoggerAxiosError, formatLoggerBoundary, formatLoggerConsole, formatLoggerCtx, formatLoggerDummy, formatLoggerErrors, formatLoggerFilter, functionNoop, getDatabaseTestPrefix, getHomeVonaAppDir, getLoggerPathPhysicalRoot, getMappedClassMetadataKeys, getPlatformArch, getPublicPathPhysicalRoot, getRequestBoundary, getRuntimePathPhysicalRoot, getSqlite3DatabaseNameDefault, getSuppressedDiagnosticLogger, getTestAppName, getTestClaimCreatedAt, getTestClaimPaths, getTestClaimRedisPrefix, getTestRunIdFromEnv, hasFreshnessDatabase, hasFreshnessRedis, hasTestClaimTransport, instanceDesp, isLocaleMagic, loadJSONFile, loadTestClaim, mergeTestClaimDatabaseOptions, mergeTestClaimRedisOptions, normalizeTestClaimDatabaseEndpoint, normalizeTestClaimRedisEndpoint, onionNameFromBeanFullName, pathToHref, polyfillDispose, prepareEnv, registerMappedClassMetadataKey, registerTestClaimDatabaseCreation, registerTestClaimResourceInventory, registerTestClaimWindowsJobVerifier, requireAddon, requireDynamic, requiresFreshTestResources, resolveAddon, resolveTestRunId, resolveVonaLocalPaths, resolverSharp, resolverSharpLibvips, resolverSqlite3, retry, runWithRequestBoundary, saveJSONFile, sendTestClaimEvent, setMappedClassMetadataKeys, startTestClaimBroker, text$1 as text, useApp, usePrepareArg, usePrepareArgs, uuidv4, validateTestClaim, withIsolatedTestDatabaseDiscovery, writeTestClaim };
|
|
4496
5253
|
//# sourceMappingURL=index.js.map
|