underpost 3.2.8 → 3.2.10
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/.github/workflows/npmpkg.ci.yml +1 -0
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -0
- package/.vscode/settings.json +10 -5
- package/CHANGELOG.md +223 -2
- package/CLI-HELP.md +36 -7
- package/README.md +38 -9
- package/bin/build.js +27 -11
- package/bin/deploy.js +20 -21
- package/bin/file.js +32 -13
- package/bin/index.js +2 -1
- package/bin/vs.js +1 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -4
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +4 -2
- package/manifests/kind-config-dev.yaml +8 -0
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/package.json +40 -25
- package/scripts/k3s-node-setup.sh +30 -11
- package/scripts/nat-iptables.sh +103 -18
- package/src/api/core/core.router.js +19 -14
- package/src/api/core/core.service.js +5 -5
- package/src/api/default/default.router.js +22 -18
- package/src/api/default/default.service.js +5 -5
- package/src/api/document/document.router.js +28 -23
- package/src/api/document/document.service.js +100 -23
- package/src/api/file/file.router.js +19 -13
- package/src/api/file/file.service.js +9 -7
- package/src/api/test/test.router.js +17 -12
- package/src/api/types.js +24 -0
- package/src/api/user/guest.service.js +5 -4
- package/src/api/user/user.router.js +297 -288
- package/src/api/user/user.service.js +100 -35
- package/src/cli/baremetal.js +20 -11
- package/src/cli/cluster.js +243 -55
- package/src/cli/db.js +106 -62
- package/src/cli/deploy.js +297 -154
- package/src/cli/fs.js +19 -3
- package/src/cli/index.js +37 -9
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +217 -135
- package/src/cli/release.js +289 -131
- package/src/cli/repository.js +91 -34
- package/src/cli/run.js +297 -56
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +19 -5
- package/src/client/components/core/Docs.js +6 -34
- package/src/client/components/core/FileExplorer.js +6 -6
- package/src/client/components/core/Modal.js +65 -2
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Recover.js +4 -4
- package/src/client/components/core/Worker.js +170 -350
- package/src/client/services/default/default.management.js +20 -25
- package/src/client/services/user/guest.service.js +10 -3
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +120 -20
- package/src/db/mongo/MongoBootstrap.js +587 -0
- package/src/db/mongo/MongooseDB.js +126 -22
- package/src/index.js +1 -1
- package/src/runtime/express/Express.js +2 -2
- package/src/runtime/wp/Wp.js +8 -5
- package/src/server/auth.js +2 -2
- package/src/server/client-build-docs.js +1 -1
- package/src/server/client-build.js +94 -129
- package/src/server/conf.js +20 -65
- package/src/server/data-query.js +32 -20
- package/src/server/dns.js +22 -0
- package/src/server/process.js +180 -19
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +26 -7
- package/src/server/valkey.js +9 -2
- package/src/ws/IoInterface.js +16 -16
- package/src/ws/core/channels/core.ws.chat.js +11 -11
- package/src/ws/core/channels/core.ws.mailer.js +29 -29
- package/src/ws/core/channels/core.ws.stream.js +19 -19
- package/src/ws/core/core.ws.connection.js +8 -8
- package/src/ws/core/core.ws.server.js +6 -5
- package/src/ws/default/channels/default.ws.main.js +10 -10
- package/src/ws/default/default.ws.connection.js +4 -4
- package/src/ws/default/default.ws.server.js +4 -3
- package/typedoc.json +10 -1
- package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
- package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
- /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
- /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
- /package/src/client/ssr/{pages → views}/Test.js +0 -0
package/src/server/conf.js
CHANGED
|
@@ -41,6 +41,20 @@ const logger = loggerFactory(import.meta);
|
|
|
41
41
|
*/
|
|
42
42
|
const ENV_REF_PREFIX = 'env:';
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Resolves a standardized context key from host/path descriptors.
|
|
46
|
+
* The key is used across DB, WS, mailer, and cache registries.
|
|
47
|
+
*
|
|
48
|
+
* @method resolveHostKeyContext
|
|
49
|
+
* @param {{host?: string, path?: string}|string} [context={ host: '', path: '' }] - Context object or prebuilt key.
|
|
50
|
+
* @returns {string} Host key context string.
|
|
51
|
+
* @memberof ServerConfBuilder
|
|
52
|
+
*/
|
|
53
|
+
const resolveHostKeyContext = (context = { host: '', path: '' }) => {
|
|
54
|
+
if (typeof context === 'string') return context;
|
|
55
|
+
return `${context.host || ''}${context.path || ''}`;
|
|
56
|
+
};
|
|
57
|
+
|
|
44
58
|
/**
|
|
45
59
|
* Recursively walks a configuration object and replaces every string value that
|
|
46
60
|
* starts with {@link ENV_REF_PREFIX} (`"env:"`) with the corresponding
|
|
@@ -1208,7 +1222,7 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1208
1222
|
const confSsr = DefaultConf.ssr[ssr];
|
|
1209
1223
|
const clients = DefaultConf.client.default.services;
|
|
1210
1224
|
|
|
1211
|
-
if (absolutePath.match('src/api') && !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))) {
|
|
1225
|
+
if (absolutePath.match('src/api') && !absolutePath.match('src/api/types.js') && !confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))) {
|
|
1212
1226
|
return false;
|
|
1213
1227
|
}
|
|
1214
1228
|
if (absolutePath.match('conf.dd-') && absolutePath.match('.js')) return false;
|
|
@@ -1250,14 +1264,8 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1250
1264
|
return false;
|
|
1251
1265
|
}
|
|
1252
1266
|
if (
|
|
1253
|
-
absolutePath.match('src/client/ssr/
|
|
1254
|
-
!confSsr.
|
|
1255
|
-
) {
|
|
1256
|
-
return false;
|
|
1257
|
-
}
|
|
1258
|
-
if (
|
|
1259
|
-
absolutePath.match('src/client/ssr/pages') &&
|
|
1260
|
-
!confSsr.pages.find((p) => absolutePath.match(`src/client/ssr/pages/${p.client}.js`))
|
|
1267
|
+
absolutePath.match('src/client/ssr/views') &&
|
|
1268
|
+
!(confSsr.views || []).find((p) => absolutePath.match(`src/client/ssr/views/${p.client}.js`))
|
|
1261
1269
|
) {
|
|
1262
1270
|
return false;
|
|
1263
1271
|
}
|
|
@@ -1287,6 +1295,7 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1287
1295
|
const awaitDeployMonitor = async (init = false, deltaMs = 1000) => {
|
|
1288
1296
|
if (init) Underpost.env.set('await-deploy', new Date().toISOString());
|
|
1289
1297
|
await timer(deltaMs);
|
|
1298
|
+
if (Underpost.env.get('container-status') === 'error') throw new Error('Container status error');
|
|
1290
1299
|
if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor();
|
|
1291
1300
|
};
|
|
1292
1301
|
|
|
@@ -1312,59 +1321,6 @@ const mergeFile = async (parts = [], outputFilePath) => {
|
|
|
1312
1321
|
});
|
|
1313
1322
|
};
|
|
1314
1323
|
|
|
1315
|
-
/**
|
|
1316
|
-
* @method rebuildConfFactory
|
|
1317
|
-
* @description Rebuilds the conf factory.
|
|
1318
|
-
* @param {object} options - The options.
|
|
1319
|
-
* @param {string} options.deployId - The deploy ID.
|
|
1320
|
-
* @param {string} options.valkey - The valkey.
|
|
1321
|
-
* @param {boolean} [options.mongo=false] - The mongo.
|
|
1322
|
-
* @returns {object} - The rebuild conf factory.
|
|
1323
|
-
* @memberof ServerConfBuilder
|
|
1324
|
-
*/
|
|
1325
|
-
const rebuildConfFactory = ({ deployId, valkey, mongo }) => {
|
|
1326
|
-
const confServer = loadReplicas(deployId, loadConfServerJson(`./engine-private/conf/${deployId}/conf.server.json`));
|
|
1327
|
-
const hosts = {};
|
|
1328
|
-
for (const host of Object.keys(confServer)) {
|
|
1329
|
-
hosts[host] = {};
|
|
1330
|
-
for (const path of Object.keys(confServer[host])) {
|
|
1331
|
-
if (!confServer[host][path].db) continue;
|
|
1332
|
-
const { singleReplica, replicas, db } = confServer[host][path];
|
|
1333
|
-
const { provider } = db;
|
|
1334
|
-
if (singleReplica) {
|
|
1335
|
-
for (const replica of replicas) {
|
|
1336
|
-
const deployIdReplica = buildReplicaId({ replica, deployId });
|
|
1337
|
-
const confServerReplica = loadConfServerJson(`./engine-private/replica/${deployIdReplica}/conf.server.json`);
|
|
1338
|
-
for (const _host of Object.keys(confServerReplica)) {
|
|
1339
|
-
for (const _path of Object.keys(confServerReplica[_host])) {
|
|
1340
|
-
hosts[host][_path] = { replica: { host, path } };
|
|
1341
|
-
confServerReplica[_host][_path].valkey = valkey;
|
|
1342
|
-
switch (provider) {
|
|
1343
|
-
case 'mongoose':
|
|
1344
|
-
confServerReplica[_host][_path].db.host = mongo.host;
|
|
1345
|
-
break;
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
fs.writeFileSync(
|
|
1350
|
-
`./engine-private/replica/${deployIdReplica}/conf.server.json`,
|
|
1351
|
-
JSON.stringify(confServerReplica, null, 4),
|
|
1352
|
-
'utf8',
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
} else hosts[host][path] = {};
|
|
1356
|
-
confServer[host][path].valkey = valkey;
|
|
1357
|
-
switch (provider) {
|
|
1358
|
-
case 'mongoose':
|
|
1359
|
-
confServer[host][path].db.host = mongo.host;
|
|
1360
|
-
break;
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
fs.writeFileSync(`./engine-private/conf/${deployId}/conf.server.json`, JSON.stringify(confServer, null, 4), 'utf8');
|
|
1365
|
-
return { hosts };
|
|
1366
|
-
};
|
|
1367
|
-
|
|
1368
1324
|
/**
|
|
1369
1325
|
* @method getPathsSSR
|
|
1370
1326
|
* @description Gets the paths SSR.
|
|
@@ -1377,8 +1333,7 @@ const getPathsSSR = (conf) => {
|
|
|
1377
1333
|
for (const o of conf.head) paths.push(`src/client/ssr/head/${o}.js`);
|
|
1378
1334
|
for (const o of conf.body) paths.push(`src/client/ssr/body/${o}.js`);
|
|
1379
1335
|
for (const o of Object.keys(conf.mailer)) paths.push(`src/client/ssr/mailer/${conf.mailer[o]}.js`);
|
|
1380
|
-
for (const o of conf.
|
|
1381
|
-
for (const o of conf.pages) paths.push(`src/client/ssr/pages/${o.client}.js`);
|
|
1336
|
+
for (const o of conf.views || []) paths.push(`src/client/ssr/views/${o.client}.js`);
|
|
1382
1337
|
return paths;
|
|
1383
1338
|
};
|
|
1384
1339
|
|
|
@@ -1774,7 +1729,6 @@ export {
|
|
|
1774
1729
|
pathPortAssignmentFactory,
|
|
1775
1730
|
deployRangePortFactory,
|
|
1776
1731
|
awaitDeployMonitor,
|
|
1777
|
-
rebuildConfFactory,
|
|
1778
1732
|
buildCliDoc,
|
|
1779
1733
|
getInstanceContext,
|
|
1780
1734
|
buildApiConf,
|
|
@@ -1784,6 +1738,7 @@ export {
|
|
|
1784
1738
|
devProxyHostFactory,
|
|
1785
1739
|
isTlsDevProxy,
|
|
1786
1740
|
getTlsHosts,
|
|
1741
|
+
resolveHostKeyContext,
|
|
1787
1742
|
resolveConfSecrets,
|
|
1788
1743
|
loadConfServerJson,
|
|
1789
1744
|
getConfFolder,
|
package/src/server/data-query.js
CHANGED
|
@@ -5,7 +5,16 @@
|
|
|
5
5
|
* @namespace DataQuery
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @class DataQuery
|
|
10
|
+
* @description Utility class for parsing request query parameters into Mongoose query options,
|
|
11
|
+
* including support for AG Grid filterModel and sortModel. Provides a static `parse`
|
|
12
|
+
* method that takes in request query parameters and returns an object containing the MongoDB
|
|
13
|
+
* query, sort options, pagination skip/limit, and page number. Designed to be used in API
|
|
14
|
+
* controllers to handle complex querying needs from the frontend.
|
|
15
|
+
* @memberof DataQuery
|
|
16
|
+
*/
|
|
17
|
+
class DataQuery {
|
|
9
18
|
/**
|
|
10
19
|
* Parse request query parameters into Mongoose query options
|
|
11
20
|
* @param {Object} params - The request query parameters (req.query)
|
|
@@ -20,7 +29,7 @@ export const DataQuery = {
|
|
|
20
29
|
* @memberof DataQuery
|
|
21
30
|
* @returns {Object} { query, sort, skip, limit, page }
|
|
22
31
|
*/
|
|
23
|
-
parse
|
|
32
|
+
static parse(params = {}) {
|
|
24
33
|
let { filterModel, sortModel, page, limit, sort: sortParam, asc, order, query: defaultQuery } = params;
|
|
25
34
|
|
|
26
35
|
// === 1. Pagination ===
|
|
@@ -35,7 +44,7 @@ export const DataQuery = {
|
|
|
35
44
|
const query = DataQuery._parseFilter(filterModel, defaultQuery);
|
|
36
45
|
|
|
37
46
|
return { query, sort, skip, limit, page };
|
|
38
|
-
}
|
|
47
|
+
}
|
|
39
48
|
|
|
40
49
|
/**
|
|
41
50
|
* Parse sort parameters from AG Grid sortModel or simple sort params
|
|
@@ -47,7 +56,7 @@ export const DataQuery = {
|
|
|
47
56
|
* @return {Object} sort object for Mongoose
|
|
48
57
|
* @memberof DataQuery
|
|
49
58
|
*/
|
|
50
|
-
_parseSort
|
|
59
|
+
static _parseSort(sortModel, sortParam, asc, order) {
|
|
51
60
|
const sort = {};
|
|
52
61
|
|
|
53
62
|
// Parse sortModel from string if needed
|
|
@@ -90,7 +99,7 @@ export const DataQuery = {
|
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
return sort;
|
|
93
|
-
}
|
|
102
|
+
}
|
|
94
103
|
|
|
95
104
|
/**
|
|
96
105
|
* Parse filter parameters from AG Grid filterModel
|
|
@@ -100,7 +109,7 @@ export const DataQuery = {
|
|
|
100
109
|
* @return {Object} query object for Mongoose
|
|
101
110
|
* @memberof DataQuery
|
|
102
111
|
*/
|
|
103
|
-
_parseFilter
|
|
112
|
+
static _parseFilter(filterModel, defaultQuery) {
|
|
104
113
|
let query = defaultQuery ? { ...defaultQuery } : {};
|
|
105
114
|
|
|
106
115
|
// Parse filterModel from string if needed
|
|
@@ -127,7 +136,7 @@ export const DataQuery = {
|
|
|
127
136
|
});
|
|
128
137
|
|
|
129
138
|
return query;
|
|
130
|
-
}
|
|
139
|
+
}
|
|
131
140
|
|
|
132
141
|
/**
|
|
133
142
|
* Parse a single field filter
|
|
@@ -137,7 +146,7 @@ export const DataQuery = {
|
|
|
137
146
|
* @return {Object|null} query condition for the field or null if invalid
|
|
138
147
|
* @memberof DataQuery
|
|
139
148
|
*/
|
|
140
|
-
_parseFieldFilter
|
|
149
|
+
static _parseFieldFilter(field, filter) {
|
|
141
150
|
if (!filter || !filter.filterType) {
|
|
142
151
|
return null;
|
|
143
152
|
}
|
|
@@ -158,7 +167,7 @@ export const DataQuery = {
|
|
|
158
167
|
default:
|
|
159
168
|
return null;
|
|
160
169
|
}
|
|
161
|
-
}
|
|
170
|
+
}
|
|
162
171
|
|
|
163
172
|
/**
|
|
164
173
|
* Parse text filter
|
|
@@ -168,7 +177,7 @@ export const DataQuery = {
|
|
|
168
177
|
* @return {Object|null} query condition for the text field or null if invalid
|
|
169
178
|
* @memberof DataQuery
|
|
170
179
|
*/
|
|
171
|
-
_parseTextFilter
|
|
180
|
+
static _parseTextFilter(field, filter) {
|
|
172
181
|
const { type, filter: filterValue } = filter;
|
|
173
182
|
|
|
174
183
|
if (filterValue === null || filterValue === undefined || filterValue === '') {
|
|
@@ -219,7 +228,7 @@ export const DataQuery = {
|
|
|
219
228
|
}
|
|
220
229
|
|
|
221
230
|
return query;
|
|
222
|
-
}
|
|
231
|
+
}
|
|
223
232
|
|
|
224
233
|
/**
|
|
225
234
|
* Parse number filter
|
|
@@ -229,7 +238,7 @@ export const DataQuery = {
|
|
|
229
238
|
* @return {Object|null} query condition for the number field or null if invalid
|
|
230
239
|
* @memberof DataQuery
|
|
231
240
|
*/
|
|
232
|
-
_parseNumberFilter
|
|
241
|
+
static _parseNumberFilter(field, filter) {
|
|
233
242
|
const { type, filter: filterValue, filterTo } = filter;
|
|
234
243
|
|
|
235
244
|
if (filterValue === null || filterValue === undefined) {
|
|
@@ -281,7 +290,7 @@ export const DataQuery = {
|
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
return query;
|
|
284
|
-
}
|
|
293
|
+
}
|
|
285
294
|
|
|
286
295
|
/**
|
|
287
296
|
* Parse date filter
|
|
@@ -291,7 +300,7 @@ export const DataQuery = {
|
|
|
291
300
|
* @return {Object|null} query condition for the date field or null if invalid
|
|
292
301
|
* @memberof DataQuery
|
|
293
302
|
*/
|
|
294
|
-
_parseDateFilter
|
|
303
|
+
static _parseDateFilter(field, filter) {
|
|
295
304
|
const { type, dateFrom, dateTo } = filter;
|
|
296
305
|
|
|
297
306
|
// Handle blank/notBlank without dates
|
|
@@ -391,7 +400,7 @@ export const DataQuery = {
|
|
|
391
400
|
}
|
|
392
401
|
|
|
393
402
|
return query;
|
|
394
|
-
}
|
|
403
|
+
}
|
|
395
404
|
|
|
396
405
|
/**
|
|
397
406
|
* Parse set filter
|
|
@@ -401,7 +410,7 @@ export const DataQuery = {
|
|
|
401
410
|
* @return {Object|null} query condition for the set field or null if invalid
|
|
402
411
|
* @memberof DataQuery
|
|
403
412
|
*/
|
|
404
|
-
_parseSetFilter
|
|
413
|
+
static _parseSetFilter(field, filter) {
|
|
405
414
|
const { values } = filter;
|
|
406
415
|
|
|
407
416
|
if (!Array.isArray(values) || values.length === 0) {
|
|
@@ -409,7 +418,7 @@ export const DataQuery = {
|
|
|
409
418
|
}
|
|
410
419
|
|
|
411
420
|
return { [field]: { $in: values } };
|
|
412
|
-
}
|
|
421
|
+
}
|
|
413
422
|
|
|
414
423
|
/**
|
|
415
424
|
* Parse multi filter (combines multiple filters with AND/OR)
|
|
@@ -419,7 +428,7 @@ export const DataQuery = {
|
|
|
419
428
|
* @return {Object|null} query condition for the multi filter or null if invalid
|
|
420
429
|
* @memberof DataQuery
|
|
421
430
|
*/
|
|
422
|
-
_parseMultiFilter
|
|
431
|
+
static _parseMultiFilter(field, filter) {
|
|
423
432
|
const { filterModels, operator } = filter;
|
|
424
433
|
|
|
425
434
|
if (!Array.isArray(filterModels) || filterModels.length === 0) {
|
|
@@ -445,5 +454,8 @@ export const DataQuery = {
|
|
|
445
454
|
// AND operator (default)
|
|
446
455
|
return { $and: conditions };
|
|
447
456
|
}
|
|
448
|
-
}
|
|
449
|
-
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export { DataQuery };
|
|
461
|
+
export default DataQuery;
|
package/src/server/dns.js
CHANGED
|
@@ -101,6 +101,22 @@ class Dns {
|
|
|
101
101
|
return ipv4.address;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Gets the MAC address of the main (default route) network interface.
|
|
106
|
+
* @static
|
|
107
|
+
* @memberof UnderpostDns
|
|
108
|
+
* @returns {string|null} The MAC address, or null if not found.
|
|
109
|
+
*/
|
|
110
|
+
static getMainInterfaceMac() {
|
|
111
|
+
const interfaceName = Dns.getDefaultNetworkInterface();
|
|
112
|
+
const networkInfo = os.networkInterfaces()[interfaceName];
|
|
113
|
+
if (!networkInfo || networkInfo.length === 0) {
|
|
114
|
+
logger.error(`Could not find network interface: ${interfaceName}`);
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return networkInfo[0].mac;
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
/**
|
|
105
121
|
* Setup nftables tables and chains if they don't exist.
|
|
106
122
|
* @static
|
|
@@ -491,6 +507,12 @@ class Dns {
|
|
|
491
507
|
});
|
|
492
508
|
}
|
|
493
509
|
|
|
510
|
+
if (options.mac) {
|
|
511
|
+
const mac = Dns.getMainInterfaceMac();
|
|
512
|
+
console.log(mac);
|
|
513
|
+
return mac;
|
|
514
|
+
}
|
|
515
|
+
|
|
494
516
|
let ip;
|
|
495
517
|
if (options.dhcp) ip = Dns.getLocalIPv4Address();
|
|
496
518
|
else ip = await Dns.getPublicIp();
|
package/src/server/process.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Module for process and shell command management.
|
|
3
|
-
* Provides utilities for executing shell commands, managing signals, and
|
|
3
|
+
* Provides utilities for executing shell commands, managing signals, and
|
|
4
|
+
* handling environment details.
|
|
5
|
+
*
|
|
6
|
+
* Execution semantics:
|
|
7
|
+
* - `shellExec(cmd)` throws `ShellExecError` on non-zero exit (fail-fast
|
|
8
|
+
* is the default). CI/CD chains observe the failure end-to-end.
|
|
9
|
+
* - `shellExec(cmd, { silentOnError: true })` opts out — returns the
|
|
10
|
+
* `ShellString` result with `.code/.stdout/.stderr` so callers can
|
|
11
|
+
* branch on the exit code themselves. Use for existence checks
|
|
12
|
+
* (`test -x …`, `command -v …`, `kubectl get` when "missing" is a
|
|
13
|
+
* normal answer).
|
|
14
|
+
* - `shellExec(cmd, { cwd: "..." })` runs hermetically in `cwd` without
|
|
15
|
+
* touching shelljs's global state.
|
|
16
|
+
* - All children spawned by `shellExec` register in
|
|
17
|
+
* `ProcessController.children` so SIGINT/SIGTERM forwarding can reach
|
|
18
|
+
* them before the parent exits.
|
|
19
|
+
*
|
|
4
20
|
* @module src/server/process.js
|
|
5
21
|
* @namespace Process
|
|
6
22
|
*/
|
|
@@ -19,6 +35,12 @@ const logger = loggerFactory(import.meta);
|
|
|
19
35
|
const getRootDirectory = () => process.cwd().replace(/\\/g, '/');
|
|
20
36
|
/**
|
|
21
37
|
* Controls and manages process-level events and signals.
|
|
38
|
+
*
|
|
39
|
+
* Subprocess registry: any child process tracked here will receive
|
|
40
|
+
* SIGTERM (followed by SIGKILL after a short grace period) when the
|
|
41
|
+
* parent receives SIGINT or SIGTERM. This prevents orphaned children
|
|
42
|
+
* during Ctrl+C in dev and during pod-termination in K8S.
|
|
43
|
+
*
|
|
22
44
|
* @namespace ProcessController
|
|
23
45
|
*/
|
|
24
46
|
class ProcessController {
|
|
@@ -41,9 +63,41 @@ class ProcessController {
|
|
|
41
63
|
'SIGSEGV',
|
|
42
64
|
'SIGILL',
|
|
43
65
|
];
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Registry of currently running tracked child processes.
|
|
69
|
+
* Populated when callers spawn via the streaming Node-native path
|
|
70
|
+
* (future expansion). The sets are exposed so signal handlers and
|
|
71
|
+
* test harnesses can introspect / clean up the registry.
|
|
72
|
+
*/
|
|
73
|
+
static children = new Set();
|
|
74
|
+
|
|
75
|
+
/** Internal: forward terminating signals to all tracked children. */
|
|
76
|
+
static _forwardToChildren(sig) {
|
|
77
|
+
if (ProcessController.children.size === 0) return;
|
|
78
|
+
for (const child of [...ProcessController.children]) {
|
|
79
|
+
try {
|
|
80
|
+
if (!child.killed) child.kill(sig);
|
|
81
|
+
} catch (_) {
|
|
82
|
+
// child may already have exited; ignore.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Hard SIGKILL after 5s grace if any child is still alive.
|
|
86
|
+
setTimeout(() => {
|
|
87
|
+
for (const child of [...ProcessController.children]) {
|
|
88
|
+
try {
|
|
89
|
+
if (!child.killed) child.kill('SIGKILL');
|
|
90
|
+
} catch (_) {
|
|
91
|
+
/* noop */
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}, 5000).unref();
|
|
95
|
+
}
|
|
96
|
+
|
|
44
97
|
/**
|
|
45
98
|
* Sets up listeners for various process signals defined in {@link ProcessController.SIG}.
|
|
46
|
-
* Handles graceful exit on 'SIGINT' (Ctrl+C)
|
|
99
|
+
* Handles graceful exit on 'SIGINT' (Ctrl+C) — but first forwards the
|
|
100
|
+
* signal to every tracked child so they get a chance to clean up.
|
|
47
101
|
* @memberof ProcessController
|
|
48
102
|
* @returns {Array<process.Process>} An array of process listener handles.
|
|
49
103
|
*/
|
|
@@ -53,7 +107,14 @@ class ProcessController {
|
|
|
53
107
|
ProcessController.logger.info(`process on ${sig}`, args);
|
|
54
108
|
switch (sig) {
|
|
55
109
|
case 'SIGINT':
|
|
56
|
-
|
|
110
|
+
case 'SIGTERM':
|
|
111
|
+
case 'SIGHUP':
|
|
112
|
+
ProcessController._forwardToChildren('SIGTERM');
|
|
113
|
+
// Give children a moment to exit cleanly before our own exit.
|
|
114
|
+
if (sig === 'SIGINT') {
|
|
115
|
+
setTimeout(() => process.exit(130), 200).unref();
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
57
118
|
default:
|
|
58
119
|
break;
|
|
59
120
|
}
|
|
@@ -71,33 +132,111 @@ class ProcessController {
|
|
|
71
132
|
ProcessController.logger = logger;
|
|
72
133
|
process.on('exit', (...args) => {
|
|
73
134
|
ProcessController.logger.info(`process on exit`, args);
|
|
135
|
+
// Last-chance reap: any tracked child still alive at exit time
|
|
136
|
+
// gets a hard kill so the parent does not leak orphans into the
|
|
137
|
+
// pod / shell session.
|
|
138
|
+
ProcessController._forwardToChildren('SIGKILL');
|
|
74
139
|
});
|
|
75
140
|
ProcessController.onSigListen();
|
|
76
|
-
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `ShellExecError` — thrown by `shellExec` when the underlying command
|
|
145
|
+
* exits with a non-zero code (the default fail-fast behaviour). Carries
|
|
146
|
+
* the exit code, stdout, and stderr for inspection by callers / CI
|
|
147
|
+
* pipelines that need structured failure data.
|
|
148
|
+
*/
|
|
149
|
+
class ShellExecError extends Error {
|
|
150
|
+
constructor(cmd, code, stdout, stderr) {
|
|
151
|
+
super(`shellExec failed (exit=${code}): ${cmd}`);
|
|
152
|
+
this.name = 'ShellExecError';
|
|
153
|
+
this.cmd = cmd;
|
|
154
|
+
this.code = code;
|
|
155
|
+
this.stdout = stdout;
|
|
156
|
+
this.stderr = stderr;
|
|
77
157
|
}
|
|
78
158
|
}
|
|
79
159
|
/**
|
|
80
160
|
* Executes a shell command using shelljs.
|
|
161
|
+
*
|
|
162
|
+
* **Default behaviour is fail-fast**: a non-zero exit code throws
|
|
163
|
+
* `ShellExecError`. Callers that need to branch on the exit code
|
|
164
|
+
* (existence checks, optional commands) must pass `silentOnError: true`
|
|
165
|
+
* to opt out of throwing.
|
|
166
|
+
*
|
|
167
|
+
* The async-callback path is exempt from the throw — shelljs delivers
|
|
168
|
+
* `(code, stdout, stderr)` to the callback, which owns its own error
|
|
169
|
+
* handling.
|
|
170
|
+
*
|
|
81
171
|
* @memberof Process
|
|
82
172
|
* @param {string} cmd - The command string to execute.
|
|
83
173
|
* @param {Object} [options] - Options for execution.
|
|
84
|
-
* @param {boolean} [options.silent=false] - Suppress
|
|
85
|
-
* @param {boolean} [options.async=false] - Run command asynchronously.
|
|
86
|
-
* @param {boolean} [options.stdout=false] - Return stdout
|
|
87
|
-
* @param {boolean} [options.disableLog=false] -
|
|
88
|
-
* @param {Function} [options.callback=null] -
|
|
89
|
-
* @
|
|
174
|
+
* @param {boolean} [options.silent=false] - Suppress child stdout/stderr to the parent terminal.
|
|
175
|
+
* @param {boolean} [options.async=false] - Run the command asynchronously (use with `callback`).
|
|
176
|
+
* @param {boolean} [options.stdout=false] - Return stdout string instead of the `ShellString` result object.
|
|
177
|
+
* @param {boolean} [options.disableLog=false] - Skip the `[process] cmd …` info log line.
|
|
178
|
+
* @param {Function} [options.callback=null] - Async callback `(code, stdout, stderr) => void` when `async: true`.
|
|
179
|
+
* @param {boolean} [options.silentOnError=false] - When `true`, swallow non-zero exits and return the `ShellString` instead of throwing. Inverse of the previous `throwOnError` flag.
|
|
180
|
+
* @param {string} [options.cwd] - Hermetic working directory (snapshotted + restored — does NOT leak).
|
|
181
|
+
* @returns {string|shelljs.ShellString} `ShellString` by default; the stdout string when `stdout: true`.
|
|
182
|
+
* @throws {ShellExecError} On non-zero exit when `silentOnError` is not set.
|
|
90
183
|
*/
|
|
91
|
-
const shellExec = (
|
|
92
|
-
cmd,
|
|
93
|
-
options = { silent: false, async: false, stdout: false, disableLog: false, callback: null },
|
|
94
|
-
) => {
|
|
184
|
+
const shellExec = (cmd, options = {}) => {
|
|
95
185
|
if (!options.disableLog) logger.info(`cmd`, cmd);
|
|
96
|
-
|
|
97
|
-
|
|
186
|
+
|
|
187
|
+
// Whitelist exactly the keys `shelljs.exec` understands. Passing our own
|
|
188
|
+
// bookkeeping keys through (or a literal `cwd: undefined`) makes shelljs
|
|
189
|
+
// call `path.resolve(undefined)` and crash with ERR_INVALID_ARG_TYPE.
|
|
190
|
+
const shellOpts = {};
|
|
191
|
+
if (options.silent !== undefined) shellOpts.silent = options.silent;
|
|
192
|
+
if (options.async !== undefined) shellOpts.async = options.async;
|
|
193
|
+
|
|
194
|
+
// Hermetic cwd. shelljs.cd mutates a process-wide global; instead we
|
|
195
|
+
// snapshot the current cwd here, switch for the duration of this call,
|
|
196
|
+
// and restore in `finally`. We deliberately do NOT forward `cwd` to
|
|
197
|
+
// shelljs — leaving its `cwd` unset means it inherits our just-changed
|
|
198
|
+
// `process.cwd()`, and we keep full control of restore semantics.
|
|
199
|
+
const previousCwd = options.cwd ? process.cwd() : null;
|
|
200
|
+
if (options.cwd) {
|
|
201
|
+
try {
|
|
202
|
+
process.chdir(options.cwd);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (Underpost.env.isInsideContainer()) Underpost.env.set('container-status', 'error')
|
|
205
|
+
throw new ShellExecError(cmd, -1, '', `chdir(${options.cwd}) failed: ${err.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
if (options.callback) {
|
|
210
|
+
// Async path. shelljs invokes the callback with (code, stdout, stderr).
|
|
211
|
+
// The callback owns its own error handling; the throw default does
|
|
212
|
+
// not apply here.
|
|
213
|
+
return shell.exec(cmd, shellOpts, options.callback);
|
|
214
|
+
}
|
|
215
|
+
const result = shell.exec(cmd, shellOpts);
|
|
216
|
+
|
|
217
|
+
if (!options.silentOnError && result && typeof result.code === 'number' && result.code !== 0) {
|
|
218
|
+
if (Underpost.env.isInsideContainer()) Underpost.env.set('container-status', 'error')
|
|
219
|
+
throw new ShellExecError(cmd, result.code, result.stdout || '', result.stderr || '');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return options.stdout ? result.stdout : result;
|
|
223
|
+
} finally {
|
|
224
|
+
if (previousCwd) {
|
|
225
|
+
try {
|
|
226
|
+
process.chdir(previousCwd);
|
|
227
|
+
} catch (_) {
|
|
228
|
+
/* best-effort restore */
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
98
232
|
};
|
|
99
233
|
/**
|
|
100
234
|
* Changes the current working directory using shelljs.
|
|
235
|
+
*
|
|
236
|
+
* Note: `shellCd` mutates global state. Prefer `shellExec(cmd, { cwd })`
|
|
237
|
+
* for one-shot directory-scoped commands; use `shellCd` only for the
|
|
238
|
+
* outermost shell where the cwd should persist across many calls.
|
|
239
|
+
*
|
|
101
240
|
* @memberof Process
|
|
102
241
|
* @param {string} cd - The path to change the directory to.
|
|
103
242
|
* @param {Object} [options] - Options for the CD operation.
|
|
@@ -110,6 +249,11 @@ const shellCd = (cd, options = { disableLog: false }) => {
|
|
|
110
249
|
};
|
|
111
250
|
/**
|
|
112
251
|
* Wraps a command to run it as a daemon process in a shell (keeping the process alive/terminal open).
|
|
252
|
+
*
|
|
253
|
+
* NB: callers must ensure `cmd` does not contain unescaped single quotes —
|
|
254
|
+
* the wrapper uses `bash -c '<cmd>; …'`. For arbitrary user input prefer
|
|
255
|
+
* a heredoc or a temporary script file.
|
|
256
|
+
*
|
|
113
257
|
* @memberof Process
|
|
114
258
|
* @param {string} cmd - The command to daemonize.
|
|
115
259
|
* @returns {string} The shell command string for the daemon process.
|
|
@@ -119,11 +263,19 @@ const daemonProcess = (cmd) => `exec bash -c '${cmd}; exec tail -f /dev/null'`;
|
|
|
119
263
|
* Retrieves the process ID (PID) of the most recently created gnome-terminal instance.
|
|
120
264
|
* Note: This function is environment-specific (GNOME/Linux) and uses `pgrep -n`.
|
|
121
265
|
* @memberof Process
|
|
122
|
-
* @returns {number} The PID of the last gnome-terminal process.
|
|
266
|
+
* @returns {number|null} The PID of the last gnome-terminal process, or null if none running.
|
|
123
267
|
*/
|
|
124
268
|
// list all terminals: pgrep gnome-terminal
|
|
125
269
|
// list last terminal: pgrep -n gnome-terminal
|
|
126
|
-
const getTerminalPid = () =>
|
|
270
|
+
const getTerminalPid = () => {
|
|
271
|
+
const raw = shellExec(`pgrep -n gnome-terminal`, { stdout: true, silent: true, silentOnError: true });
|
|
272
|
+
if (!raw || !raw.trim()) return null;
|
|
273
|
+
try {
|
|
274
|
+
return JSON.parse(raw);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
127
279
|
/**
|
|
128
280
|
* Copies text content to the system clipboard using clipboardy.
|
|
129
281
|
* Logs the copied content for confirmation.
|
|
@@ -135,4 +287,13 @@ function pbcopy(data) {
|
|
|
135
287
|
clipboard.writeSync(data || '🦄');
|
|
136
288
|
logger.info(`copied to clipboard`, clipboard.readSync());
|
|
137
289
|
}
|
|
138
|
-
export {
|
|
290
|
+
export {
|
|
291
|
+
ProcessController,
|
|
292
|
+
ShellExecError,
|
|
293
|
+
getRootDirectory,
|
|
294
|
+
shellExec,
|
|
295
|
+
shellCd,
|
|
296
|
+
pbcopy,
|
|
297
|
+
getTerminalPid,
|
|
298
|
+
daemonProcess,
|
|
299
|
+
};
|