underpost 3.2.9 → 3.2.11
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/extensions.json +9 -9
- package/.vscode/settings.json +20 -4
- package/CHANGELOG.md +195 -1
- package/CLI-HELP.md +92 -23
- package/README.md +38 -9
- package/bin/build.js +27 -7
- package/bin/build.template.js +187 -0
- package/bin/deploy.js +12 -2
- package/bin/index.js +2 -1
- package/bump.config.js +26 -0
- package/conf.js +20 -7
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- 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/lxd/lxd-admin-profile.yaml +12 -3
- package/manifests/mongodb/pv-pvc.yaml +44 -8
- package/manifests/mongodb/statefulset.yaml +55 -68
- package/manifests/mongodb-4.4/headless-service.yaml +10 -0
- package/manifests/mongodb-4.4/kustomization.yaml +3 -1
- package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
- package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
- package/manifests/mongodb-4.4/statefulset.yaml +79 -0
- package/manifests/mongodb-4.4/storage-class.yaml +9 -0
- package/manifests/valkey/statefulset.yaml +1 -1
- package/manifests/valkey/valkey-nodeport.yaml +17 -0
- package/package.json +27 -12
- package/scripts/ipxe-setup.sh +52 -49
- package/scripts/k3s-node-setup.sh +81 -46
- package/scripts/lxd-vm-setup.sh +193 -8
- package/scripts/maas-nat-firewalld.sh +145 -0
- 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 +132 -101
- package/src/cli/cluster.js +700 -232
- package/src/cli/db.js +59 -60
- package/src/cli/deploy.js +216 -137
- package/src/cli/fs.js +13 -3
- package/src/cli/index.js +80 -15
- package/src/cli/ipfs.js +4 -6
- package/src/cli/kubectl.js +4 -1
- package/src/cli/lxd.js +1099 -223
- package/src/cli/monitor.js +9 -3
- package/src/cli/release.js +334 -140
- package/src/cli/repository.js +68 -23
- package/src/cli/run.js +191 -47
- package/src/cli/secrets.js +11 -2
- package/src/cli/test.js +9 -3
- package/src/client/Default.index.js +9 -3
- package/src/client/components/core/Auth.js +5 -0
- package/src/client/components/core/ClientEvents.js +76 -0
- package/src/client/components/core/EventBus.js +4 -0
- package/src/client/components/core/Modal.js +82 -41
- package/src/client/components/core/PanelForm.js +56 -52
- package/src/client/components/core/Worker.js +162 -363
- package/src/client/sw/core.sw.js +174 -112
- package/src/db/DataBaseProvider.js +115 -15
- package/src/db/mariadb/MariaDB.js +2 -1
- package/src/db/mongo/MongoBootstrap.js +657 -0
- package/src/db/mongo/MongooseDB.js +129 -21
- 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 +81 -79
- package/src/server/process.js +180 -19
- package/src/server/proxy.js +9 -2
- package/src/server/runtime.js +1 -1
- package/src/server/start.js +16 -4
- package/src/server/valkey.js +2 -0
- 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/bin/file.js +0 -202
- package/bin/vs.js +0 -74
- package/bin/zed.js +0 -84
- 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
|
|
@@ -320,25 +334,29 @@ const Config = {
|
|
|
320
334
|
|
|
321
335
|
if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true });
|
|
322
336
|
|
|
323
|
-
const
|
|
337
|
+
const sharedEnvTemplate = fs.existsSync('./.env.example')
|
|
324
338
|
? fs.readFileSync('./.env.example', 'utf8')
|
|
325
339
|
: fs.existsSync('./.env.production')
|
|
326
340
|
? fs.readFileSync('./.env.production', 'utf8')
|
|
327
341
|
: '';
|
|
328
342
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
fs.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
343
|
+
const envTemplates = {
|
|
344
|
+
production: fs.existsSync('./.env.production') ? fs.readFileSync('./.env.production', 'utf8') : sharedEnvTemplate,
|
|
345
|
+
development: fs.existsSync('./.env.development')
|
|
346
|
+
? fs.readFileSync('./.env.development', 'utf8')
|
|
347
|
+
: sharedEnvTemplate
|
|
348
|
+
? sharedEnvTemplate.replace('NODE_ENV=production', 'NODE_ENV=development').replace('PORT=3000', 'PORT=4000')
|
|
349
|
+
: '',
|
|
350
|
+
test: fs.existsSync('./.env.test')
|
|
351
|
+
? fs.readFileSync('./.env.test', 'utf8')
|
|
352
|
+
: sharedEnvTemplate
|
|
353
|
+
? sharedEnvTemplate.replace('NODE_ENV=production', 'NODE_ENV=test').replace('PORT=3000', 'PORT=5000')
|
|
354
|
+
: '',
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
for (const [envName, envTemplate] of Object.entries(envTemplates)) {
|
|
358
|
+
if (!envTemplate) continue;
|
|
359
|
+
fs.writeFileSync(`${folder}/.env.${envName}`, envTemplate.replaceAll('dd-default', deployId), 'utf8');
|
|
342
360
|
}
|
|
343
361
|
|
|
344
362
|
fs.writeFileSync(
|
|
@@ -1208,7 +1226,11 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1208
1226
|
const confSsr = DefaultConf.ssr[ssr];
|
|
1209
1227
|
const clients = DefaultConf.client.default.services;
|
|
1210
1228
|
|
|
1211
|
-
if (
|
|
1229
|
+
if (
|
|
1230
|
+
absolutePath.match('src/api') &&
|
|
1231
|
+
!absolutePath.match('src/api/types.js') &&
|
|
1232
|
+
!confServer.apis.find((p) => absolutePath.match(`src/api/${p}/`))
|
|
1233
|
+
) {
|
|
1212
1234
|
return false;
|
|
1213
1235
|
}
|
|
1214
1236
|
if (absolutePath.match('conf.dd-') && absolutePath.match('.js')) return false;
|
|
@@ -1250,14 +1272,8 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1250
1272
|
return false;
|
|
1251
1273
|
}
|
|
1252
1274
|
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`))
|
|
1275
|
+
absolutePath.match('src/client/ssr/views') &&
|
|
1276
|
+
!(confSsr.views || []).find((p) => absolutePath.match(`src/client/ssr/views/${p.client}.js`))
|
|
1261
1277
|
) {
|
|
1262
1278
|
return false;
|
|
1263
1279
|
}
|
|
@@ -1287,6 +1303,7 @@ const validateTemplatePath = (absolutePath = '') => {
|
|
|
1287
1303
|
const awaitDeployMonitor = async (init = false, deltaMs = 1000) => {
|
|
1288
1304
|
if (init) Underpost.env.set('await-deploy', new Date().toISOString());
|
|
1289
1305
|
await timer(deltaMs);
|
|
1306
|
+
if (Underpost.env.get('container-status') === 'error') throw new Error('Container status error');
|
|
1290
1307
|
if (Underpost.env.get('await-deploy')) return await awaitDeployMonitor();
|
|
1291
1308
|
};
|
|
1292
1309
|
|
|
@@ -1312,59 +1329,6 @@ const mergeFile = async (parts = [], outputFilePath) => {
|
|
|
1312
1329
|
});
|
|
1313
1330
|
};
|
|
1314
1331
|
|
|
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
1332
|
/**
|
|
1369
1333
|
* @method getPathsSSR
|
|
1370
1334
|
* @description Gets the paths SSR.
|
|
@@ -1377,8 +1341,7 @@ const getPathsSSR = (conf) => {
|
|
|
1377
1341
|
for (const o of conf.head) paths.push(`src/client/ssr/head/${o}.js`);
|
|
1378
1342
|
for (const o of conf.body) paths.push(`src/client/ssr/body/${o}.js`);
|
|
1379
1343
|
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`);
|
|
1344
|
+
for (const o of conf.views || []) paths.push(`src/client/ssr/views/${o.client}.js`);
|
|
1382
1345
|
return paths;
|
|
1383
1346
|
};
|
|
1384
1347
|
|
|
@@ -1746,6 +1709,44 @@ const loadConfServerJson = (jsonPath, options) => {
|
|
|
1746
1709
|
return options && options.resolve === true ? resolveConfSecrets(raw) : raw;
|
|
1747
1710
|
};
|
|
1748
1711
|
|
|
1712
|
+
/**
|
|
1713
|
+
* Creates and writes the /etc/hosts file for a deployment.
|
|
1714
|
+
* @method etcHostFactory
|
|
1715
|
+
* @param {Array<string>} hosts - List of hosts to be added to the hosts file.
|
|
1716
|
+
* @param {object} options - Options for the hosts file creation.
|
|
1717
|
+
* @param {boolean} options.append - Whether to append to the existing hosts file.
|
|
1718
|
+
* @returns {object} - Object containing the rendered hosts file.
|
|
1719
|
+
* @memberof ServerConfBuilder
|
|
1720
|
+
*/
|
|
1721
|
+
const etcHostFactory = (hosts = [], options = { append: false }) => {
|
|
1722
|
+
hosts = hosts.map((host) => {
|
|
1723
|
+
try {
|
|
1724
|
+
if (!host.startsWith('http')) host = `http://${host}`;
|
|
1725
|
+
const hostname = new URL(host).hostname;
|
|
1726
|
+
logger.info('Hostname extract valid', { host, hostname });
|
|
1727
|
+
return hostname;
|
|
1728
|
+
} catch (e) {
|
|
1729
|
+
logger.warn('No hostname extract valid', host);
|
|
1730
|
+
return host;
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
const renderHosts = `127.0.0.1 ${hosts.join(
|
|
1734
|
+
' ',
|
|
1735
|
+
)} localhost localhost.localdomain localhost4 localhost4.localdomain4
|
|
1736
|
+
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6`;
|
|
1737
|
+
|
|
1738
|
+
if (options && options.append && fs.existsSync(`/etc/hosts`)) {
|
|
1739
|
+
fs.writeFileSync(
|
|
1740
|
+
`/etc/hosts`,
|
|
1741
|
+
fs.readFileSync(`/etc/hosts`, 'utf8') +
|
|
1742
|
+
`
|
|
1743
|
+
${renderHosts}`,
|
|
1744
|
+
'utf8',
|
|
1745
|
+
);
|
|
1746
|
+
} else fs.writeFileSync(`/etc/hosts`, renderHosts, 'utf8');
|
|
1747
|
+
return { renderHosts };
|
|
1748
|
+
};
|
|
1749
|
+
|
|
1749
1750
|
export {
|
|
1750
1751
|
Config,
|
|
1751
1752
|
loadConf,
|
|
@@ -1774,7 +1775,6 @@ export {
|
|
|
1774
1775
|
pathPortAssignmentFactory,
|
|
1775
1776
|
deployRangePortFactory,
|
|
1776
1777
|
awaitDeployMonitor,
|
|
1777
|
-
rebuildConfFactory,
|
|
1778
1778
|
buildCliDoc,
|
|
1779
1779
|
getInstanceContext,
|
|
1780
1780
|
buildApiConf,
|
|
@@ -1784,6 +1784,7 @@ export {
|
|
|
1784
1784
|
devProxyHostFactory,
|
|
1785
1785
|
isTlsDevProxy,
|
|
1786
1786
|
getTlsHosts,
|
|
1787
|
+
resolveHostKeyContext,
|
|
1787
1788
|
resolveConfSecrets,
|
|
1788
1789
|
loadConfServerJson,
|
|
1789
1790
|
getConfFolder,
|
|
@@ -1792,4 +1793,5 @@ export {
|
|
|
1792
1793
|
DEFAULT_DEPLOY_ID,
|
|
1793
1794
|
loadCronDeployEnv,
|
|
1794
1795
|
cronDeployIdResolve,
|
|
1796
|
+
etcHostFactory,
|
|
1795
1797
|
};
|
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
|
+
};
|
package/src/server/proxy.js
CHANGED
|
@@ -9,7 +9,14 @@
|
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
|
11
11
|
import { loggerFactory, loggerMiddleware } from './logger.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
buildPortProxyRouter,
|
|
14
|
+
buildProxyRouter,
|
|
15
|
+
etcHostFactory,
|
|
16
|
+
getTlsHosts,
|
|
17
|
+
isDevProxyContext,
|
|
18
|
+
isTlsDevProxy,
|
|
19
|
+
} from './conf.js';
|
|
13
20
|
|
|
14
21
|
import { shellExec } from './process.js';
|
|
15
22
|
import fs from 'fs-extra';
|
|
@@ -114,7 +121,7 @@ class ProxyService {
|
|
|
114
121
|
logger.info('Proxy running', { port, router: options.router });
|
|
115
122
|
if (process.env.NODE_ENV === 'development')
|
|
116
123
|
logger.info(
|
|
117
|
-
|
|
124
|
+
etcHostFactory(Object.keys(options.router), {
|
|
118
125
|
append: true,
|
|
119
126
|
}).renderHosts,
|
|
120
127
|
);
|
package/src/server/runtime.js
CHANGED
package/src/server/start.js
CHANGED
|
@@ -198,20 +198,32 @@ class UnderpostStartUp {
|
|
|
198
198
|
*/
|
|
199
199
|
async run(deployId = 'dd-default', env = 'development', options = {}) {
|
|
200
200
|
const runCmd = env === 'production' ? 'run prod:container' : 'run dev:container';
|
|
201
|
+
const makeDeployCallback = (cmd) => (code, out, msg) => {
|
|
202
|
+
if (code !== 0) {
|
|
203
|
+
logger.error(`Deployment process exited with code ${code}`, { cmd, msg });
|
|
204
|
+
Underpost.env.set('container-status', 'error');
|
|
205
|
+
}
|
|
206
|
+
};
|
|
201
207
|
if (fs.existsSync(`./engine-private/replica`)) {
|
|
202
208
|
const replicas = await fs.readdir(`./engine-private/replica`);
|
|
203
209
|
for (const replica of replicas) {
|
|
204
210
|
if (!replica.match(deployId)) continue;
|
|
205
211
|
shellExec(`node bin env ${replica} ${env}`);
|
|
206
|
-
|
|
212
|
+
const replicaCmd = `npm ${runCmd} ${replica}`;
|
|
213
|
+
shellExec(replicaCmd, { async: true, callback: makeDeployCallback(replicaCmd) });
|
|
207
214
|
await awaitDeployMonitor(true);
|
|
208
215
|
}
|
|
209
216
|
}
|
|
210
217
|
shellExec(`node bin env ${deployId} ${env}`);
|
|
211
|
-
|
|
218
|
+
const deployCmd = `npm ${runCmd} ${deployId}`;
|
|
219
|
+
shellExec(deployCmd, { async: true, callback: makeDeployCallback(deployCmd) });
|
|
212
220
|
await awaitDeployMonitor(true);
|
|
213
|
-
if (
|
|
214
|
-
|
|
221
|
+
if (Underpost.env.get('container-status') !== 'error') {
|
|
222
|
+
if (env === 'production' && Underpost.env.isInsideContainer()) Underpost.secret.globalSecretClean();
|
|
223
|
+
Underpost.env.set('container-status', `${deployId}-${env}-running-deployment`);
|
|
224
|
+
}
|
|
225
|
+
Underpost.env.set('container-status', 'error');
|
|
226
|
+
throw new Error('Deployment process exited unexpectedly');
|
|
215
227
|
},
|
|
216
228
|
};
|
|
217
229
|
}
|
package/src/server/valkey.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import Valkey from 'iovalkey';
|
|
16
16
|
import { loggerFactory } from './logger.js';
|
|
17
|
+
import Underpost from '../index.js';
|
|
17
18
|
|
|
18
19
|
const logger = loggerFactory(import.meta);
|
|
19
20
|
|
|
@@ -70,6 +71,7 @@ const createValkeyConnection = async (instance = {}, connectionOptions = {}) =>
|
|
|
70
71
|
client.on('error', (err) => {
|
|
71
72
|
ValkeyStatus[key] = 'error';
|
|
72
73
|
logger.error('Valkey error', { err: err?.message, instance });
|
|
74
|
+
if (Underpost.env.isInsideContainer()) Underpost.env.set('container-status', 'error');
|
|
73
75
|
});
|
|
74
76
|
client.on('reconnecting', () => {
|
|
75
77
|
ValkeyStatus[key] = 'reconnecting';
|