@kne/fastify-app-manager 0.1.1 → 0.1.2
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/README.md +80 -19
- package/index.js +29 -1
- package/libs/controllers/app.js +32 -4
- package/libs/controllers/dbops.js +268 -0
- package/libs/services/app.js +247 -48
- package/libs/services/bootstrap.js +10 -20
- package/libs/services/dbops.js +817 -0
- package/libs/utils/dbIdentity.js +118 -0
- package/libs/utils/env.js +110 -10
- package/libs/utils/logFiles.js +19 -5
- package/libs/utils/migrate.js +10 -5
- package/libs/utils/sqlQueryGuard.js +72 -0
- package/package.json +2 -2
package/libs/services/app.js
CHANGED
|
@@ -2,7 +2,7 @@ const fp = require('fastify-plugin');
|
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const portPool = require('../utils/portPool');
|
|
5
|
-
const { mergeEnv, maskEnvForResponse, applyEnvPatch, mergePm2Config } = require('../utils/env');
|
|
5
|
+
const { mergeEnv, maskEnvForResponse, applyEnvPatch, mergePm2Config, normalizeSecretKeys, collectSecretKeys } = require('../utils/env');
|
|
6
6
|
const { prepareVersionArtifact } = require('../utils/version');
|
|
7
7
|
const { injectEntryHtml } = require('../utils/entryInject');
|
|
8
8
|
const { ensureLogFiles, readLogTail, readLastLines, appendLog } = require('../utils/logFiles');
|
|
@@ -29,13 +29,18 @@ module.exports = fp(async (fastify, options) => {
|
|
|
29
29
|
const appRoot = name => path.join(options.appsRoot, name);
|
|
30
30
|
const mountPrefix = name => `${options.pathPrefix.replace(/\/$/, '')}/${name}`;
|
|
31
31
|
|
|
32
|
+
const resolveSecretEnvKeys = appLike => normalizeSecretKeys(appLike?.options?.secretEnvKeys);
|
|
33
|
+
|
|
32
34
|
const toPublicApp = app => {
|
|
33
35
|
if (!app) {
|
|
34
36
|
return null;
|
|
35
37
|
}
|
|
36
38
|
const json = typeof app.toJSON === 'function' ? app.toJSON() : { ...app };
|
|
37
|
-
|
|
39
|
+
const explicitSecrets = resolveSecretEnvKeys(json);
|
|
40
|
+
const envOpts = Object.assign({}, options, { secretEnvKeys: explicitSecrets });
|
|
41
|
+
json.env = maskEnvForResponse(json.env || {}, envOpts);
|
|
38
42
|
json.passthroughEnvKeys = options.passthroughEnvKeys || [];
|
|
43
|
+
json.secretEnvKeys = collectSecretKeys(json.env || {}, envOpts);
|
|
39
44
|
json.pathUrl = `${mountPrefix(json.name)}/`;
|
|
40
45
|
return json;
|
|
41
46
|
};
|
|
@@ -79,6 +84,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
79
84
|
portMax: options.portMax
|
|
80
85
|
});
|
|
81
86
|
|
|
87
|
+
const normalizedOptions = Object.assign({}, appOptions || {});
|
|
88
|
+
if (normalizedOptions.secretEnvKeys !== undefined) {
|
|
89
|
+
normalizedOptions.secretEnvKeys = normalizeSecretKeys(normalizedOptions.secretEnvKeys);
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
const app = await models.app.create({
|
|
83
93
|
name,
|
|
84
94
|
label,
|
|
@@ -87,7 +97,7 @@ module.exports = fp(async (fastify, options) => {
|
|
|
87
97
|
description: description || null,
|
|
88
98
|
env: env || {},
|
|
89
99
|
pm2Config: pm2Config || {},
|
|
90
|
-
options:
|
|
100
|
+
options: normalizedOptions,
|
|
91
101
|
port,
|
|
92
102
|
status: 'idle',
|
|
93
103
|
rootPath,
|
|
@@ -114,16 +124,29 @@ module.exports = fp(async (fastify, options) => {
|
|
|
114
124
|
}
|
|
115
125
|
}
|
|
116
126
|
if (patch.env) {
|
|
117
|
-
|
|
127
|
+
const secretEnvKeys = resolveSecretEnvKeys(app);
|
|
128
|
+
patch.env = applyEnvPatch(app.env || {}, patch.env, Object.assign({}, options, { secretEnvKeys }));
|
|
129
|
+
}
|
|
130
|
+
if (patch.options && patch.options.secretEnvKeys !== undefined) {
|
|
131
|
+
patch.options = Object.assign({}, patch.options, {
|
|
132
|
+
secretEnvKeys: normalizeSecretKeys(patch.options.secretEnvKeys)
|
|
133
|
+
});
|
|
118
134
|
}
|
|
119
135
|
await app.update(patch);
|
|
120
136
|
return toPublicApp(app);
|
|
121
137
|
};
|
|
122
138
|
|
|
123
|
-
const saveEnv = async ({ name, env }) => {
|
|
139
|
+
const saveEnv = async ({ name, env, secretEnvKeys: nextSecretKeys } = {}) => {
|
|
124
140
|
const app = await getByName(name);
|
|
125
|
-
const
|
|
126
|
-
|
|
141
|
+
const currentSecrets = resolveSecretEnvKeys(app);
|
|
142
|
+
const secretEnvKeys = nextSecretKeys !== undefined ? normalizeSecretKeys(nextSecretKeys) : currentSecrets;
|
|
143
|
+
const nextEnv = applyEnvPatch(app.env || {}, env || {}, Object.assign({}, options, { secretEnvKeys }));
|
|
144
|
+
const cleanedSecrets = secretEnvKeys.filter(key => Object.prototype.hasOwnProperty.call(nextEnv, key));
|
|
145
|
+
await app.update({
|
|
146
|
+
env: nextEnv,
|
|
147
|
+
options: Object.assign({}, app.options || {}, { secretEnvKeys: cleanedSecrets })
|
|
148
|
+
});
|
|
149
|
+
await app.reload();
|
|
127
150
|
return toPublicApp(app);
|
|
128
151
|
};
|
|
129
152
|
|
|
@@ -235,6 +258,139 @@ module.exports = fp(async (fastify, options) => {
|
|
|
235
258
|
return false;
|
|
236
259
|
};
|
|
237
260
|
|
|
261
|
+
const getPm2ProcessStatus = async pm2Name => {
|
|
262
|
+
try {
|
|
263
|
+
const list = await pm2Util.describe(pm2Name);
|
|
264
|
+
const proc = list?.[0];
|
|
265
|
+
return proc?.pm2_env?.status || null;
|
|
266
|
+
} catch (e) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
/** 同一 app 并发同步合并为一次,避免 restart/reconcile 竞态 */
|
|
272
|
+
const statusSyncInflight = new Map();
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 根据 PM2 真实进程状态 + HTTP 健康检查,异步写回 DB status。
|
|
276
|
+
* @param {string|number} appId
|
|
277
|
+
* @param {{ recoverIfMissing?: boolean }} [opts] recoverIfMissing:DB 期望在跑但进程不在时尝试 startProcess
|
|
278
|
+
*/
|
|
279
|
+
const syncRealStatus = async (appId, { recoverIfMissing = false, beforeTables } = {}) => {
|
|
280
|
+
const key = String(appId);
|
|
281
|
+
if (statusSyncInflight.has(key)) {
|
|
282
|
+
return statusSyncInflight.get(key);
|
|
283
|
+
}
|
|
284
|
+
const run = (async () => {
|
|
285
|
+
const app = await models.app.findByPk(appId);
|
|
286
|
+
if (!app) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
let pm2Status = await getPm2ProcessStatus(app.pm2Name);
|
|
291
|
+
|
|
292
|
+
if ((!pm2Status || pm2Status === 'stopped' || pm2Status === 'stopping') && recoverIfMissing && app.currentVersionId) {
|
|
293
|
+
try {
|
|
294
|
+
await startProcess(app);
|
|
295
|
+
pm2Status = (await getPm2ProcessStatus(app.pm2Name)) || 'online';
|
|
296
|
+
} catch (e) {
|
|
297
|
+
await app.update({ status: 'error', message: e.message });
|
|
298
|
+
return toPublicApp(app);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!pm2Status || pm2Status === 'stopped' || pm2Status === 'stopping') {
|
|
303
|
+
if (app.status !== 'stopped' && app.status !== 'idle') {
|
|
304
|
+
await app.update({ status: 'stopped', message: null });
|
|
305
|
+
}
|
|
306
|
+
return toPublicApp(await models.app.findByPk(appId));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (pm2Status === 'errored') {
|
|
310
|
+
await app.update({ status: 'error', message: 'pm2 process errored' });
|
|
311
|
+
return toPublicApp(await models.app.findByPk(appId));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// online / launching / waiting 等:探测 HTTP 就绪
|
|
315
|
+
const ok = await healthCheck(app.port, {
|
|
316
|
+
timeoutMs: options.healthCheckTimeoutMs,
|
|
317
|
+
intervalMs: options.healthCheckIntervalMs,
|
|
318
|
+
healthPath: options.healthCheckPath
|
|
319
|
+
});
|
|
320
|
+
if (ok) {
|
|
321
|
+
await app.update({ status: 'running', message: null });
|
|
322
|
+
try {
|
|
323
|
+
const fresh = await models.app.findByPk(appId);
|
|
324
|
+
const dbops = fastify[options.name].services.dbops;
|
|
325
|
+
await dbops.claimOwnedTablesByPrefix(fresh);
|
|
326
|
+
if (Array.isArray(beforeTables)) {
|
|
327
|
+
const afterTables = await dbops.withSequelize(fresh, sequelize => dbops.listAllTables(sequelize));
|
|
328
|
+
await fresh.reload();
|
|
329
|
+
await dbops.mergeOwnedTablesFromDiff(fresh, beforeTables, afterTables);
|
|
330
|
+
}
|
|
331
|
+
} catch (e) {
|
|
332
|
+
fastify.log.warn({ err: e, appId }, 'claim owned tables after ready failed');
|
|
333
|
+
}
|
|
334
|
+
} else {
|
|
335
|
+
const again = await getPm2ProcessStatus(app.pm2Name);
|
|
336
|
+
if (again === 'errored') {
|
|
337
|
+
await app.update({ status: 'error', message: 'pm2 process errored' });
|
|
338
|
+
} else if (again === 'online' || again === 'launching') {
|
|
339
|
+
await app.update({ status: 'error', message: 'health check timeout' });
|
|
340
|
+
} else {
|
|
341
|
+
await app.update({ status: 'stopped', message: 'process not online after health check' });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return toPublicApp(await models.app.findByPk(appId));
|
|
345
|
+
} catch (e) {
|
|
346
|
+
await app.update({ status: 'error', message: e.message });
|
|
347
|
+
return toPublicApp(await models.app.findByPk(appId));
|
|
348
|
+
}
|
|
349
|
+
})().finally(() => {
|
|
350
|
+
statusSyncInflight.delete(key);
|
|
351
|
+
});
|
|
352
|
+
statusSyncInflight.set(key, run);
|
|
353
|
+
return run;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const snapshotTablesSafe = async app => {
|
|
357
|
+
try {
|
|
358
|
+
const dbops = fastify[options.name].services.dbops;
|
|
359
|
+
return await dbops.withSequelize(app, sequelize => dbops.listAllTables(sequelize));
|
|
360
|
+
} catch (e) {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const finishDeployAsync = async appId => syncRealStatus(appId, { recoverIfMissing: false });
|
|
366
|
+
|
|
367
|
+
const scheduleStatusSync = (appId, opts) => {
|
|
368
|
+
setImmediate(() => {
|
|
369
|
+
syncRealStatus(appId, opts).catch(err => {
|
|
370
|
+
fastify.log.warn({ err, appId }, 'syncRealStatus failed');
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const syncStatus = async ({ name, recoverIfMissing = false } = {}) => {
|
|
376
|
+
const app = await getByName(name);
|
|
377
|
+
return syncRealStatus(app.id, { recoverIfMissing });
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const syncAllStatuses = async ({ recoverIfMissing = true } = {}) => {
|
|
381
|
+
const apps = await models.app.findAll({
|
|
382
|
+
where: {
|
|
383
|
+
status: ['running', 'deploying', 'stopped', 'error']
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
const results = [];
|
|
387
|
+
for (const app of apps) {
|
|
388
|
+
const shouldRecover = recoverIfMissing && (app.status === 'running' || app.status === 'deploying');
|
|
389
|
+
results.push(await syncRealStatus(app.id, { recoverIfMissing: shouldRecover }));
|
|
390
|
+
}
|
|
391
|
+
return results.filter(Boolean);
|
|
392
|
+
};
|
|
393
|
+
|
|
238
394
|
const startProcess = async app => {
|
|
239
395
|
const version = await models.appVersion.findByPk(app.currentVersionId);
|
|
240
396
|
if (!version) {
|
|
@@ -245,9 +401,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
245
401
|
const env = mergeEnv({
|
|
246
402
|
passthroughEnvKeys: options.passthroughEnvKeys,
|
|
247
403
|
appEnv: app.env,
|
|
248
|
-
port: app.port
|
|
404
|
+
port: app.port,
|
|
405
|
+
defaultAppDb: options.defaultAppDb,
|
|
406
|
+
appName: app.name
|
|
249
407
|
});
|
|
250
408
|
|
|
409
|
+
// Persist injected shared-DB table prefix so claim/UI see the same value
|
|
410
|
+
if (env.DB_TABLE_PREFIX && app.env?.DB_TABLE_PREFIX !== env.DB_TABLE_PREFIX) {
|
|
411
|
+
await app.update({
|
|
412
|
+
env: Object.assign({}, app.env || {}, { DB_TABLE_PREFIX: env.DB_TABLE_PREFIX })
|
|
413
|
+
});
|
|
414
|
+
await app.reload();
|
|
415
|
+
}
|
|
416
|
+
|
|
251
417
|
const pathBase = mountPrefix(app.name);
|
|
252
418
|
// Domain-first: inject `/` so custom Host SPA works; path mode then relies on gateway rewrite fallback.
|
|
253
419
|
// Path-only: inject `/app/{name}` so runtime* matches strip-prefix gateway.
|
|
@@ -270,27 +436,6 @@ module.exports = fp(async (fastify, options) => {
|
|
|
270
436
|
});
|
|
271
437
|
};
|
|
272
438
|
|
|
273
|
-
const finishDeployAsync = async appId => {
|
|
274
|
-
const app = await models.app.findByPk(appId);
|
|
275
|
-
if (!app) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
try {
|
|
279
|
-
const ok = await healthCheck(app.port, {
|
|
280
|
-
timeoutMs: options.healthCheckTimeoutMs,
|
|
281
|
-
intervalMs: options.healthCheckIntervalMs,
|
|
282
|
-
healthPath: options.healthCheckPath
|
|
283
|
-
});
|
|
284
|
-
if (ok) {
|
|
285
|
-
await app.update({ status: 'running', message: null });
|
|
286
|
-
} else {
|
|
287
|
-
await app.update({ status: 'error', message: 'health check timeout' });
|
|
288
|
-
}
|
|
289
|
-
} catch (e) {
|
|
290
|
-
await app.update({ status: 'error', message: e.message });
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
|
|
294
439
|
const deploy = async ({ name, versionId, version, runMigration = true }) => {
|
|
295
440
|
const app = await getByName(name);
|
|
296
441
|
let ver = null;
|
|
@@ -321,13 +466,30 @@ module.exports = fp(async (fastify, options) => {
|
|
|
321
466
|
const env = mergeEnv({
|
|
322
467
|
passthroughEnvKeys: options.passthroughEnvKeys,
|
|
323
468
|
appEnv: app.env,
|
|
324
|
-
port: app.port
|
|
469
|
+
port: app.port,
|
|
470
|
+
defaultAppDb: options.defaultAppDb,
|
|
471
|
+
appName: app.name
|
|
325
472
|
});
|
|
473
|
+
const dbops = fastify[options.name].services.dbops;
|
|
474
|
+
let beforeTables = [];
|
|
475
|
+
try {
|
|
476
|
+
beforeTables = await dbops.withSequelize(app, async sequelize => dbops.listAllTables(sequelize));
|
|
477
|
+
} catch (e) {
|
|
478
|
+
beforeTables = [];
|
|
479
|
+
}
|
|
326
480
|
await runSqlMigrations({
|
|
327
481
|
serverDir: path.join(ver.artifactPath, 'server'),
|
|
328
482
|
sqlPath: ver.migrationPath || options.sqlPath,
|
|
329
|
-
env
|
|
483
|
+
env,
|
|
484
|
+
Sequelize: fastify.sequelize.Sequelize
|
|
330
485
|
});
|
|
486
|
+
try {
|
|
487
|
+
const afterTables = await dbops.withSequelize(app, async sequelize => dbops.listAllTables(sequelize));
|
|
488
|
+
await app.reload();
|
|
489
|
+
await dbops.mergeOwnedTablesFromDiff(app, beforeTables, afterTables);
|
|
490
|
+
} catch (e) {
|
|
491
|
+
// ignore ownership merge failures
|
|
492
|
+
}
|
|
331
493
|
}
|
|
332
494
|
|
|
333
495
|
if (runMigration === false) {
|
|
@@ -335,8 +497,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
335
497
|
app.env = Object.assign({}, app.env, { RUN_SQL_ON_SYNC: 'false' });
|
|
336
498
|
}
|
|
337
499
|
|
|
500
|
+
const beforeForReady = await snapshotTablesSafe(app);
|
|
338
501
|
await startProcess(app);
|
|
339
|
-
|
|
502
|
+
scheduleStatusSync(app.id, { beforeTables: beforeForReady });
|
|
340
503
|
return {
|
|
341
504
|
name: app.name,
|
|
342
505
|
status: 'deploying',
|
|
@@ -369,8 +532,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
369
532
|
}
|
|
370
533
|
await app.update({ status: 'deploying', message: null });
|
|
371
534
|
try {
|
|
535
|
+
const beforeTables = await snapshotTablesSafe(app);
|
|
372
536
|
await startProcess(app);
|
|
373
|
-
|
|
537
|
+
scheduleStatusSync(app.id, { beforeTables });
|
|
374
538
|
return toPublicApp(await getByName(name));
|
|
375
539
|
} catch (e) {
|
|
376
540
|
await app.update({ status: 'error', message: e.message });
|
|
@@ -382,38 +546,64 @@ module.exports = fp(async (fastify, options) => {
|
|
|
382
546
|
const app = await getByName(name);
|
|
383
547
|
await app.update({ status: 'deploying', message: null });
|
|
384
548
|
try {
|
|
385
|
-
await
|
|
386
|
-
|
|
387
|
-
return toPublicApp(await getByName(name));
|
|
388
|
-
} catch (e) {
|
|
549
|
+
const beforeTables = await snapshotTablesSafe(app);
|
|
550
|
+
// Always startProcess so env (incl. DB_TABLE_PREFIX) is refreshed; pm2.restart keeps stale env
|
|
389
551
|
try {
|
|
390
|
-
await
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
} catch (e2) {
|
|
394
|
-
await app.update({ status: 'error', message: e2.message });
|
|
395
|
-
httpError(400, e2.message);
|
|
552
|
+
await pm2Util.stopApp(app.pm2Name);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
// ignore if not running
|
|
396
555
|
}
|
|
556
|
+
await startProcess(app);
|
|
557
|
+
scheduleStatusSync(app.id, { beforeTables });
|
|
558
|
+
return toPublicApp(await getByName(name));
|
|
559
|
+
} catch (e) {
|
|
560
|
+
await app.update({ status: 'error', message: e.message });
|
|
561
|
+
httpError(400, e.message);
|
|
397
562
|
}
|
|
398
563
|
};
|
|
399
564
|
|
|
400
|
-
const remove = async ({ name }) => {
|
|
565
|
+
const remove = async ({ name, exportBeforeRemove = true, cleanupData = true, allowRemoveIfCleanupIncomplete = false, force = false, removeSqliteFile = false } = {}) => {
|
|
401
566
|
const app = await getByName(name);
|
|
402
567
|
try {
|
|
403
568
|
await pm2Util.deleteProcess(app.pm2Name);
|
|
404
569
|
} catch (e) {
|
|
405
570
|
// ignore
|
|
406
571
|
}
|
|
572
|
+
|
|
573
|
+
const dbops = fastify[options.name].services.dbops;
|
|
574
|
+
let exportResult = null;
|
|
575
|
+
let cleanupResult = null;
|
|
576
|
+
|
|
577
|
+
if (exportBeforeRemove && !force) {
|
|
578
|
+
exportResult = await dbops.exportData({ name });
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (cleanupData) {
|
|
582
|
+
cleanupResult = await dbops.cleanup({ name, removeSqliteFile });
|
|
583
|
+
if (cleanupResult.manualRequired && !allowRemoveIfCleanupIncomplete) {
|
|
584
|
+
return {
|
|
585
|
+
removed: false,
|
|
586
|
+
reason: 'cleanup incomplete; execute returned sql or pass allowRemoveIfCleanupIncomplete=true',
|
|
587
|
+
export: exportResult,
|
|
588
|
+
cleanup: cleanupResult
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
407
593
|
await models.appVersion.destroy({ where: { appName: name } });
|
|
408
594
|
await fs.remove(app.rootPath).catch(() => {});
|
|
409
595
|
await app.destroy();
|
|
410
|
-
return {
|
|
596
|
+
return {
|
|
597
|
+
removed: true,
|
|
598
|
+
export: exportResult,
|
|
599
|
+
cleanup: cleanupResult
|
|
600
|
+
};
|
|
411
601
|
};
|
|
412
602
|
|
|
413
|
-
const logs = async ({ name, stream = 'out', perPage = 100, currentPage = 1 }) => {
|
|
603
|
+
const logs = async ({ name, stream = 'out', perPage = 100, currentPage = 1, beforeLine } = {}) => {
|
|
414
604
|
const app = await getByName(name);
|
|
415
605
|
const file = path.join(app.rootPath, 'logs', stream === 'err' ? 'err.log' : 'out.log');
|
|
416
|
-
return readLogTail(file, { perPage, currentPage });
|
|
606
|
+
return readLogTail(file, { perPage, currentPage, beforeLine });
|
|
417
607
|
};
|
|
418
608
|
|
|
419
609
|
const findByHost = async host => {
|
|
@@ -441,6 +631,8 @@ module.exports = fp(async (fastify, options) => {
|
|
|
441
631
|
start,
|
|
442
632
|
stop,
|
|
443
633
|
restart,
|
|
634
|
+
syncStatus,
|
|
635
|
+
syncAllStatuses,
|
|
444
636
|
remove,
|
|
445
637
|
logs,
|
|
446
638
|
toPublicApp,
|
|
@@ -462,7 +654,14 @@ module.exports = fp(async (fastify, options) => {
|
|
|
462
654
|
await appendLog(file, content);
|
|
463
655
|
const hub = getLogHub();
|
|
464
656
|
if (hub) {
|
|
465
|
-
|
|
657
|
+
const { totalCount } = await readLogTail(file, { perPage: 1, currentPage: 1 });
|
|
658
|
+
hub.emit(`log:${appName}`, {
|
|
659
|
+
appName,
|
|
660
|
+
stream,
|
|
661
|
+
content,
|
|
662
|
+
line: totalCount,
|
|
663
|
+
loggedAt: new Date().toISOString()
|
|
664
|
+
});
|
|
466
665
|
}
|
|
467
666
|
}
|
|
468
667
|
}
|
|
@@ -52,7 +52,12 @@ module.exports = fp(async (fastify, options) => {
|
|
|
52
52
|
fastify.log.warn({ err: e }, 'pm2 launchBus failed');
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// 父应用重启后不阻塞 ready:后台按 PM2 真实状态异步对齐 DB
|
|
56
|
+
setImmediate(() => {
|
|
57
|
+
reconcile().catch(err => {
|
|
58
|
+
fastify.log.warn({ err }, 'async reconcile/sync statuses failed');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
56
61
|
};
|
|
57
62
|
|
|
58
63
|
const fsEnsureAppsRoot = async () => {
|
|
@@ -61,26 +66,11 @@ module.exports = fp(async (fastify, options) => {
|
|
|
61
66
|
};
|
|
62
67
|
|
|
63
68
|
const reconcile = async () => {
|
|
64
|
-
const {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
let list = [];
|
|
69
|
-
try {
|
|
70
|
-
list = await pm2Util.list();
|
|
71
|
-
} catch (e) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const runningNames = new Set(list.filter(p => p.pm2_env?.status === 'online').map(p => p.name));
|
|
75
|
-
for (const app of apps) {
|
|
76
|
-
if (!runningNames.has(app.pm2Name)) {
|
|
77
|
-
try {
|
|
78
|
-
await services.app.start({ name: app.name });
|
|
79
|
-
} catch (e) {
|
|
80
|
-
await app.update({ status: 'error', message: `reconcile failed: ${e.message}` });
|
|
81
|
-
}
|
|
82
|
-
}
|
|
69
|
+
const { services } = fastify[options.name];
|
|
70
|
+
if (typeof services.app.syncAllStatuses === 'function') {
|
|
71
|
+
return services.app.syncAllStatuses({ recoverIfMissing: true });
|
|
83
72
|
}
|
|
73
|
+
return [];
|
|
84
74
|
};
|
|
85
75
|
|
|
86
76
|
const onClose = async () => {
|