lupine.api 1.1.50 → 1.1.52
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/admin/admin-release.tsx +37 -2
- package/dev/cp-index-html.js +5 -5
- package/package.json +1 -1
- package/src/admin-api/admin-api.ts +3 -0
- package/src/admin-api/admin-auth.ts +20 -14
- package/src/admin-api/admin-release.ts +59 -1
- package/src/admin-api/web-config-api.ts +19 -0
- package/src/api/server-render.ts +7 -8
- package/src/app/app-message.ts +15 -7
- package/src/app/app-restart.ts +54 -0
- package/src/app/app-shared-storage.ts +14 -6
- package/src/app/app-start.ts +27 -7
- package/src/app/cleanup-exit.ts +1 -1
- package/src/app/process-dev-requests.ts +21 -8
- package/src/app/web-server.ts +4 -2
- package/src/lib/db/db.ts +24 -3
- package/src/models/app-shared-storage-props.ts +1 -1
package/admin/admin-release.tsx
CHANGED
|
@@ -284,7 +284,7 @@ export const AdminReleasePage = () => {
|
|
|
284
284
|
isLocal,
|
|
285
285
|
});
|
|
286
286
|
const dataResponse = await response.json;
|
|
287
|
-
console.log('
|
|
287
|
+
console.log('refresh-cache', dataResponse);
|
|
288
288
|
if (!dataResponse || dataResponse.status !== 'ok') {
|
|
289
289
|
NotificationMessage.sendMessage(dataResponse.message || 'Failed to refresh cache', NotificationColor.Error);
|
|
290
290
|
return;
|
|
@@ -293,6 +293,35 @@ export const AdminReleasePage = () => {
|
|
|
293
293
|
NotificationMessage.sendMessage('Cache refreshed successfully', NotificationColor.Success);
|
|
294
294
|
};
|
|
295
295
|
|
|
296
|
+
const onRestartAppLocal = async () => {
|
|
297
|
+
return onRestartApp(true);
|
|
298
|
+
};
|
|
299
|
+
const onRestartAppRemote = async () => {
|
|
300
|
+
return onRestartApp(false);
|
|
301
|
+
};
|
|
302
|
+
const onRestartApp = async (isLocal?: boolean) => {
|
|
303
|
+
const data = getDomData();
|
|
304
|
+
if (!isLocal) {
|
|
305
|
+
if (!data.targetUrl || !data.accessToken) {
|
|
306
|
+
NotificationMessage.sendMessage('Please fill in all fields', NotificationColor.Error);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const response = await getRenderPageProps().renderPageFunctions.fetchData('/api/admin/release/restart-app', {
|
|
312
|
+
...data,
|
|
313
|
+
isLocal,
|
|
314
|
+
});
|
|
315
|
+
const dataResponse = await response.json;
|
|
316
|
+
console.log('restart-app', dataResponse);
|
|
317
|
+
if (!dataResponse || dataResponse.status !== 'ok') {
|
|
318
|
+
NotificationMessage.sendMessage(dataResponse.message || 'Failed to Restart App', NotificationColor.Error);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
domLog.value = <pre>{JSON.stringify(dataResponse, null, 2)}</pre>;
|
|
322
|
+
NotificationMessage.sendMessage('Restart App successfully', NotificationColor.Success);
|
|
323
|
+
};
|
|
324
|
+
|
|
296
325
|
const ref: RefProps = {
|
|
297
326
|
onLoad: async () => {
|
|
298
327
|
const data = JSON.parse(localStorage.getItem('admin-release') || '{}');
|
|
@@ -321,9 +350,15 @@ export const AdminReleasePage = () => {
|
|
|
321
350
|
<button onClick={onRefreshCacheRemote} class='button-base mr-m'>
|
|
322
351
|
Refresh Cache (Remote)
|
|
323
352
|
</button>
|
|
324
|
-
<button onClick={
|
|
353
|
+
<button onClick={onRestartAppRemote} class='button-base mr-m color-red'>
|
|
354
|
+
Restart App (Remote)
|
|
355
|
+
</button>
|
|
356
|
+
<button onClick={onRefreshCacheLocal} class='button-base mr-m'>
|
|
325
357
|
Refresh Cache (Local)
|
|
326
358
|
</button>
|
|
359
|
+
<button onClick={onRestartAppLocal} class='button-base color-red'>
|
|
360
|
+
Restart App (Local)
|
|
361
|
+
</button>
|
|
327
362
|
</div>
|
|
328
363
|
{domUpdate.node}
|
|
329
364
|
{domLog.node}
|
package/dev/cp-index-html.js
CHANGED
|
@@ -37,7 +37,7 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
37
37
|
// if isMobile=true, then the last number is 1, or if isMobile=false, the last is 2
|
|
38
38
|
const chgTime = Math.trunc(f1.mtime.getTime() / 10) * 10 + (isMobile ? 1 : 2);
|
|
39
39
|
|
|
40
|
-
// when it's isMobile, need to update env and configs
|
|
40
|
+
// when it's isMobile, need to update env and configs => no configs as mobile app fetches it from api
|
|
41
41
|
if (!f2 || f2.mtime.getTime() !== chgTime || isMobile) {
|
|
42
42
|
const inHtml = await fs.readFile(htmlFile, 'utf-8');
|
|
43
43
|
let outStr = inHtml.replace(/{hash}/gi, new Date().getTime().toString(36));
|
|
@@ -48,7 +48,7 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
48
48
|
const metaIndexStart = inHtml.indexOf(metaTextStart);
|
|
49
49
|
const metaIndexEnd = inHtml.indexOf(metaTextEnd);
|
|
50
50
|
|
|
51
|
-
const webConfig = await readWebConfig(outdirData);
|
|
51
|
+
// const webConfig = await readWebConfig(outdirData);
|
|
52
52
|
const webEnvData = webEnv.getWebEnv(appName);
|
|
53
53
|
|
|
54
54
|
outStr =
|
|
@@ -56,9 +56,9 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
|
|
|
56
56
|
'<script id="web-env" type="application/json">' +
|
|
57
57
|
JSON.stringify(webEnvData) +
|
|
58
58
|
'</script>\r\n' +
|
|
59
|
-
'<script id="web-setting" type="application/json">' +
|
|
60
|
-
JSON.stringify(webConfig) +
|
|
61
|
-
'</script>' +
|
|
59
|
+
// '<script id="web-setting" type="application/json">' +
|
|
60
|
+
// JSON.stringify(webConfig) +
|
|
61
|
+
// '</script>' +
|
|
62
62
|
outStr.substring(metaIndexEnd + metaTextEnd.length);
|
|
63
63
|
// outStr = webEnv.replaceWebEnv(inHtml, appName, true);
|
|
64
64
|
}
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { AdminConfig } from './admin-config';
|
|
|
11
11
|
import { Logger } from '../lib';
|
|
12
12
|
import { IApiBase, ServerRequest } from '../models';
|
|
13
13
|
import { ApiRouter } from '../api';
|
|
14
|
+
import { readWebConfig } from './web-config-api';
|
|
14
15
|
|
|
15
16
|
const logger = new Logger('admin-api');
|
|
16
17
|
|
|
@@ -27,6 +28,8 @@ export class AdminApi implements IApiBase {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
protected mountDashboard() {
|
|
31
|
+
this.router.use('/web-config', readWebConfig);
|
|
32
|
+
|
|
30
33
|
const adminDb = new AdminDb();
|
|
31
34
|
this.router.use('/db', needDevAdminSession, adminDb.getRouter());
|
|
32
35
|
|
|
@@ -10,6 +10,11 @@ export const needDevAdminSession = async (req: ServerRequest, res: ServerRespons
|
|
|
10
10
|
const devAdminSession = await adminApiHelper.getDevAdminFromCookie(req, res, true);
|
|
11
11
|
if (!devAdminSession) {
|
|
12
12
|
// return true to skip the rest of the middleware
|
|
13
|
+
const response = {
|
|
14
|
+
status: 'error',
|
|
15
|
+
message: langHelper.getLang('shared:permission_denied'),
|
|
16
|
+
};
|
|
17
|
+
ApiHelper.sendJson(req, res, response);
|
|
13
18
|
return true;
|
|
14
19
|
}
|
|
15
20
|
return false;
|
|
@@ -64,14 +69,15 @@ export const devAdminAuth = async (req: ServerRequest, res: ServerResponse) => {
|
|
|
64
69
|
ApiHelper.sendJson(req, res, response);
|
|
65
70
|
return true;
|
|
66
71
|
}
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
// on set app cookie when login, not every time
|
|
73
|
+
// // if it's dev admin, then set app admin cookie as well
|
|
74
|
+
// let addLoginResponse = {};
|
|
75
|
+
// const appAdminHookSetCookie = adminApiHelper.getAppAdminHookSetCookie();
|
|
76
|
+
// if (appAdminHookSetCookie) {
|
|
77
|
+
// addLoginResponse = await appAdminHookSetCookie(req, res, devAdminSession.u);
|
|
78
|
+
// }
|
|
73
79
|
const response = {
|
|
74
|
-
...addLoginResponse,
|
|
80
|
+
// ...addLoginResponse,
|
|
75
81
|
status: 'ok',
|
|
76
82
|
message: langHelper.getLang('shared:login_success'),
|
|
77
83
|
devLogin: CryptoUtils.encrypt(JSON.stringify(devAdminSession), cryptoKey),
|
|
@@ -104,13 +110,13 @@ export const devAdminAuth = async (req: ServerRequest, res: ServerResponse) => {
|
|
|
104
110
|
message: langHelper.getLang('shared:login_success'),
|
|
105
111
|
devLogin: tokenCookie,
|
|
106
112
|
};
|
|
107
|
-
req.locals.setCookie('_token', tokenCookie, {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
113
|
+
// req.locals.setCookie('_token', tokenCookie, {
|
|
114
|
+
// expireDays: 360,
|
|
115
|
+
// path: '/',
|
|
116
|
+
// httpOnly: false,
|
|
117
|
+
// secure: true,
|
|
118
|
+
// sameSite: 'none',
|
|
119
|
+
// });
|
|
114
120
|
ApiHelper.sendJson(req, res, response);
|
|
115
121
|
return true;
|
|
116
122
|
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
adminApiHelper,
|
|
12
12
|
processRefreshCache,
|
|
13
13
|
apiStorage,
|
|
14
|
+
processRestartApp,
|
|
14
15
|
} from 'lupine.api';
|
|
15
16
|
import path from 'path';
|
|
16
17
|
import { needDevAdminSession } from './admin-auth';
|
|
@@ -32,15 +33,17 @@ export class AdminRelease implements IApiBase {
|
|
|
32
33
|
protected mountDashboard() {
|
|
33
34
|
// called by FE
|
|
34
35
|
this.router.use('/check', needDevAdminSession, this.check.bind(this));
|
|
35
|
-
this.router.use('/update', needDevAdminSession, this.
|
|
36
|
+
this.router.use('/update', needDevAdminSession, this.callUpdate.bind(this));
|
|
36
37
|
this.router.use('/view-log', needDevAdminSession, this.viewLog.bind(this));
|
|
37
38
|
// called online or by clients
|
|
38
39
|
this.router.use('/refresh-cache', needDevAdminSession, this.refreshCache.bind(this));
|
|
40
|
+
this.router.use('/restart-app', needDevAdminSession, this.restartApp.bind(this));
|
|
39
41
|
|
|
40
42
|
// ...ByClient will verify credentials from post, so it doesn't need AdminSession
|
|
41
43
|
this.router.use('/byClientCheck', this.byClientCheck.bind(this));
|
|
42
44
|
this.router.use('/byClientUpdate', this.byClientUpdate.bind(this));
|
|
43
45
|
this.router.use('/byClientRefreshCache', this.byClientRefreshCache.bind(this));
|
|
46
|
+
this.router.use('/byClientRestartApp', this.byClientRestartApp.bind(this));
|
|
44
47
|
this.router.use('/byClientViewLog', this.byClientViewLog.bind(this));
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -126,6 +129,47 @@ export class AdminRelease implements IApiBase {
|
|
|
126
129
|
return true;
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
async restartApp(req: ServerRequest, res: ServerResponse) {
|
|
133
|
+
// check whether it's from online admin
|
|
134
|
+
const json = await adminApiHelper.getDevAdminFromCookie(req, res, false);
|
|
135
|
+
const jsonData = req.locals.json();
|
|
136
|
+
if (json && jsonData && !Array.isArray(jsonData) && jsonData.isLocal) {
|
|
137
|
+
await processRestartApp(req);
|
|
138
|
+
const response = {
|
|
139
|
+
status: 'ok',
|
|
140
|
+
message: 'Restart app successfully.',
|
|
141
|
+
};
|
|
142
|
+
ApiHelper.sendJson(req, res, response);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data = await this.chkData(jsonData, req, res, true);
|
|
147
|
+
if (!data) return true;
|
|
148
|
+
|
|
149
|
+
let targetUrl = data.targetUrl as string;
|
|
150
|
+
if (targetUrl.endsWith('/')) {
|
|
151
|
+
targetUrl = targetUrl.slice(0, -1);
|
|
152
|
+
}
|
|
153
|
+
const remoteData = await fetch(targetUrl + '/api/admin/release/byClientRestartApp', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
body: JSON.stringify(data),
|
|
156
|
+
});
|
|
157
|
+
const resultText = await remoteData.text();
|
|
158
|
+
let remoteResult: any;
|
|
159
|
+
try {
|
|
160
|
+
remoteResult = JSON.parse(resultText);
|
|
161
|
+
} catch (e: any) {
|
|
162
|
+
remoteResult = { status: 'error', message: resultText };
|
|
163
|
+
}
|
|
164
|
+
const response = {
|
|
165
|
+
status: 'ok',
|
|
166
|
+
message: 'check.',
|
|
167
|
+
...remoteResult,
|
|
168
|
+
};
|
|
169
|
+
ApiHelper.sendJson(req, res, response);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
129
173
|
public async chkData(data: any, req: ServerRequest, res: ServerResponse, chkCredential: boolean) {
|
|
130
174
|
// add access token
|
|
131
175
|
if (!data || Array.isArray(data) || typeof data !== 'object' || !data.accessToken || !data.targetUrl) {
|
|
@@ -569,4 +613,18 @@ export class AdminRelease implements IApiBase {
|
|
|
569
613
|
ApiHelper.sendJson(req, res, response);
|
|
570
614
|
return true;
|
|
571
615
|
}
|
|
616
|
+
|
|
617
|
+
async byClientRestartApp(req: ServerRequest, res: ServerResponse) {
|
|
618
|
+
const jsonData = req.locals.json();
|
|
619
|
+
const data = await this.chkData(jsonData, req, res, true);
|
|
620
|
+
if (!data) return true;
|
|
621
|
+
|
|
622
|
+
await processRestartApp(req);
|
|
623
|
+
const response = {
|
|
624
|
+
status: 'ok',
|
|
625
|
+
message: 'Restart app successfully.',
|
|
626
|
+
};
|
|
627
|
+
ApiHelper.sendJson(req, res, response);
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
572
630
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ServerResponse } from 'http';
|
|
2
|
+
import { Logger } from '../lib';
|
|
3
|
+
import { ServerRequest } from '../models';
|
|
4
|
+
import { langHelper } from '../lang';
|
|
5
|
+
import { ApiHelper, apiStorage } from '../api';
|
|
6
|
+
|
|
7
|
+
// only used by mobile app. For web, it's injected in the html by server-side render
|
|
8
|
+
const logger = new Logger('web-cfg-api');
|
|
9
|
+
export const readWebConfig = async (req: ServerRequest, res: ServerResponse) => {
|
|
10
|
+
logger.info('readWebConfig');
|
|
11
|
+
|
|
12
|
+
const response = {
|
|
13
|
+
status: 'ok',
|
|
14
|
+
result: await apiStorage.getWebAll(),
|
|
15
|
+
message: langHelper.getLang('shared:operation_success'),
|
|
16
|
+
};
|
|
17
|
+
ApiHelper.sendJson(req, res, response);
|
|
18
|
+
return true;
|
|
19
|
+
};
|
package/src/api/server-render.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { IToClientDelivery } from '../models/to-client-delivery-props';
|
|
|
9
9
|
import { JsonObject } from '../models/json-object';
|
|
10
10
|
import { getTemplateCache } from './api-cache';
|
|
11
11
|
import { apiStorage } from './api-shared-storage';
|
|
12
|
-
import { SimpleStorageDataProps } from '../models';
|
|
13
12
|
import { RuntimeRequire } from '../lib/runtime-require';
|
|
14
13
|
|
|
15
14
|
const logger = new Logger('StaticServer');
|
|
@@ -159,13 +158,13 @@ export const serverSideRenderPage = async (
|
|
|
159
158
|
const _lupineJs = cachedHtml[nearRoot]._lupineJs;
|
|
160
159
|
const currentCache = cachedHtml[nearRoot] as CachedHtmlProps;
|
|
161
160
|
const webSetting = await apiStorage.getWebAll();
|
|
162
|
-
const webSettingShortKey: SimpleStorageDataProps = {};
|
|
163
|
-
for (let item of Object.keys(webSetting)) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
161
|
+
// const webSettingShortKey: SimpleStorageDataProps = {};
|
|
162
|
+
// for (let item of Object.keys(webSetting)) {
|
|
163
|
+
// const newItem = item.substring(4);
|
|
164
|
+
// webSettingShortKey[newItem] = webSetting[item];
|
|
165
|
+
// }
|
|
167
166
|
// const webSetting = AppConfig.get(AppConfig.WEB_SETTINGS_KEY) || {};
|
|
168
|
-
const clientDelivery = new ToClientDelivery(currentCache.webEnv,
|
|
167
|
+
const clientDelivery = new ToClientDelivery(currentCache.webEnv, webSetting, req.locals.cookies());
|
|
169
168
|
const page = await _lupineJs.generatePage(props, clientDelivery);
|
|
170
169
|
// console.log(`=========load lupin: `, content);
|
|
171
170
|
|
|
@@ -199,7 +198,7 @@ export const serverSideRenderPage = async (
|
|
|
199
198
|
res.write(page.metaData);
|
|
200
199
|
res.write(page.globalCss);
|
|
201
200
|
res.write('<script id="web-env" type="application/json">' + JSON.stringify(currentCache.webEnv) + '</script>');
|
|
202
|
-
res.write('<script id="web-setting" type="application/json">' + JSON.stringify(
|
|
201
|
+
res.write('<script id="web-setting" type="application/json">' + JSON.stringify(webSetting) + '</script>');
|
|
203
202
|
res.write(
|
|
204
203
|
currentCache.content.substring(
|
|
205
204
|
currentCache.metaIndexEnd + metaTextEnd.length,
|
package/src/app/app-message.ts
CHANGED
|
@@ -2,6 +2,7 @@ import cluster from 'cluster';
|
|
|
2
2
|
import { Logger, LogWriter, LogWriterMessageId } from '../lib';
|
|
3
3
|
import { processDebugMessage } from './process-dev-requests';
|
|
4
4
|
import { cleanupAndExit } from './cleanup-exit';
|
|
5
|
+
import { restartApp } from './app-restart';
|
|
5
6
|
|
|
6
7
|
export type AppMessageProps = {
|
|
7
8
|
id: string;
|
|
@@ -78,15 +79,22 @@ export const processMessageFromWorker = (msgObject: AppMessageProps) => {
|
|
|
78
79
|
logger.debug(
|
|
79
80
|
`Message from worker ${cluster.worker?.id}, message: ${msgObject.message}, appName: ${msgObject.appName}`
|
|
80
81
|
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
if (msgObject.message === 'restartApp') {
|
|
83
|
+
restartApp();
|
|
84
|
+
return;
|
|
85
|
+
} else if (msgObject.message === 'refresh') {
|
|
86
|
+
broadcast(msgObject);
|
|
87
|
+
} else if (msgObject.message === 'shutdown') {
|
|
88
|
+
broadcast(msgObject);
|
|
89
|
+
// if it's shutdown, the primary process will exit
|
|
90
|
+
setTimeout(async () => {
|
|
91
|
+
console.log(`[server primary] Received shutdown command.`, cluster.workers);
|
|
92
|
+
await cleanupAndExit();
|
|
87
93
|
}, 100);
|
|
94
|
+
} else {
|
|
95
|
+
logger.warn(`Unknown message: ${msgObject.id}`);
|
|
88
96
|
}
|
|
89
97
|
} else {
|
|
90
|
-
logger.warn(`Unknown message: ${msgObject.id}`);
|
|
98
|
+
logger.warn(`Unknown message id: ${msgObject.id}`);
|
|
91
99
|
}
|
|
92
100
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import cluster from 'cluster';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { appStorage } from './app-shared-storage';
|
|
4
|
+
import { AppMessageProps, processMessageFromWorker } from './app-message';
|
|
5
|
+
|
|
6
|
+
export const _restartApp = {
|
|
7
|
+
isRestarting: false,
|
|
8
|
+
};
|
|
9
|
+
export const restartApp = async () => {
|
|
10
|
+
if (!cluster.isPrimary) {
|
|
11
|
+
console.warn(`restartApp: shouldn't come here`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
await appStorage.save('', true);
|
|
16
|
+
console.log(`Old app ${process.pid} sends SIGTERM to all workors.`);
|
|
17
|
+
// const closeAll = () => {
|
|
18
|
+
// _restartApp.isRestarting = true;
|
|
19
|
+
|
|
20
|
+
// return Object.values(cluster.workers!).map(
|
|
21
|
+
// (w) =>
|
|
22
|
+
// new Promise((resolve) => {
|
|
23
|
+
// console.log(`Sending SIGTERM to workor ${w!.id}.`);
|
|
24
|
+
// w!.once('exit', resolve);
|
|
25
|
+
// w!.kill('SIGTERM');
|
|
26
|
+
// })
|
|
27
|
+
// );
|
|
28
|
+
// };
|
|
29
|
+
// await Promise.all(closeAll());
|
|
30
|
+
|
|
31
|
+
console.log(`Old app ${process.pid} starts new app ${process.execPath}`, process.argv);
|
|
32
|
+
// spawn(process.execPath, process.argv.slice(1), {
|
|
33
|
+
// stdio: 'inherit',
|
|
34
|
+
// env: { ...process.env, RESTARTING: '1' },
|
|
35
|
+
// });
|
|
36
|
+
|
|
37
|
+
console.log(`Old app ${process.pid} exists.`);
|
|
38
|
+
// setTimeout(process.exit, 3000);
|
|
39
|
+
if (!process.send) {
|
|
40
|
+
console.log(`The primary process is not focked from loader, so cannot restart.`);
|
|
41
|
+
} else {
|
|
42
|
+
process.send({ id: 'debug', message: 'restartApp' });
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// this is primary, and receive messages from loader
|
|
47
|
+
export const receiveMessageFromLoader = () => {
|
|
48
|
+
process.on('message', async (msg: AppMessageProps) => {
|
|
49
|
+
if (msg?.id === 'debug' && msg?.message === 'shutdown') {
|
|
50
|
+
console.log(`App ${process.pid}: received shutdown message from loader.`);
|
|
51
|
+
processMessageFromWorker(msg);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A persistent storage to store data in primary process and share to all workers
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* You should use apiStorage in api module
|
|
5
5
|
*/
|
|
6
6
|
import * as fs from 'fs/promises';
|
|
@@ -141,9 +141,9 @@ export class AppSharedStorage implements IAppSharedStorage {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// called from primary before exit, or from api to save changes
|
|
144
|
-
async save(appName?: string) {
|
|
144
|
+
async save(appName?: string, exit?: boolean) {
|
|
145
145
|
if (!cluster.isPrimary) {
|
|
146
|
-
AppSharedStorageWorker.save(appName);
|
|
146
|
+
await AppSharedStorageWorker.save(appName);
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -183,8 +183,16 @@ export class AppSharedStorage implements IAppSharedStorage {
|
|
|
183
183
|
getApi(appName: string, key: string): Promise<string> {
|
|
184
184
|
return this.get(appName, AppSharedStorageApiPrefix + key);
|
|
185
185
|
}
|
|
186
|
-
getWebAll(appName: string): Promise<SimpleStorageDataProps> {
|
|
187
|
-
|
|
186
|
+
async getWebAll(appName: string): Promise<SimpleStorageDataProps> {
|
|
187
|
+
const webAll = await this.getWithPrefix(appName, AppSharedStorageWebPrefix);
|
|
188
|
+
|
|
189
|
+
const webSettingShortKey: SimpleStorageDataProps = {};
|
|
190
|
+
for (let item of Object.keys(webAll)) {
|
|
191
|
+
const newItem = item.substring(AppSharedStorageWebPrefix.length);
|
|
192
|
+
webSettingShortKey[newItem] = webAll[item];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return webSettingShortKey;
|
|
188
196
|
}
|
|
189
197
|
getWithPrefix(appName: string, prefixKey: string): Promise<SimpleStorageDataProps> {
|
|
190
198
|
return new Promise((resolve, reject) => {
|
|
@@ -267,7 +275,7 @@ class AppSharedStorageWorker {
|
|
|
267
275
|
workerId: cluster.worker?.id || 0,
|
|
268
276
|
action: 'save',
|
|
269
277
|
appName: appName || '',
|
|
270
|
-
key: '',
|
|
278
|
+
key: 'save',
|
|
271
279
|
};
|
|
272
280
|
process.send!(obj);
|
|
273
281
|
}
|
package/src/app/app-start.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { AppStartProps, InitStartProps, AppCacheGlobal, AppCacheKeys } from '../
|
|
|
9
9
|
import { appStorage } from './app-shared-storage';
|
|
10
10
|
import { HostToPath } from './host-to-path';
|
|
11
11
|
import { cleanupAndExit } from './cleanup-exit';
|
|
12
|
+
import { _restartApp, receiveMessageFromLoader } from './app-restart';
|
|
12
13
|
|
|
13
14
|
// Don't use logger before set process message
|
|
14
15
|
class AppStart {
|
|
@@ -16,10 +17,16 @@ class AppStart {
|
|
|
16
17
|
webServer: WebServer | undefined;
|
|
17
18
|
|
|
18
19
|
getWorkerId() {
|
|
19
|
-
return cluster.worker ? cluster.worker.id :
|
|
20
|
+
return cluster.worker ? cluster.worker.id : -1;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async start(props: AppStartProps, webServer?: WebServer) {
|
|
24
|
+
// if it's started from spawn, wait for old master to clear ports
|
|
25
|
+
if (cluster.isPrimary && process.env.RESTARTING === '1') {
|
|
26
|
+
console.log(`New app ${process.pid} RESTARTING.`);
|
|
27
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
this.debug = props.debug;
|
|
24
31
|
this.bindProcess();
|
|
25
32
|
|
|
@@ -58,14 +65,20 @@ class AppStart {
|
|
|
58
65
|
const numCPUs = props.debug ? 1 : require('os').cpus().length;
|
|
59
66
|
console.log(`Master Process is trying to fork ${numCPUs} processes`);
|
|
60
67
|
|
|
68
|
+
receiveMessageFromLoader();
|
|
69
|
+
|
|
61
70
|
for (let i = 0; i < numCPUs; i++) {
|
|
62
71
|
let worker = cluster.fork();
|
|
63
72
|
worker.on('message', processMessageFromWorker);
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
cluster.on('death', (worker: any) => {
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
if (!_restartApp.isRestarting) {
|
|
77
|
+
console.log(`Worker ${worker.pid} died; starting a new one...`);
|
|
78
|
+
cluster.fork();
|
|
79
|
+
} else {
|
|
80
|
+
console.log(`Worker ${worker.pid} exited during restart`);
|
|
81
|
+
}
|
|
69
82
|
});
|
|
70
83
|
}
|
|
71
84
|
}
|
|
@@ -82,7 +95,7 @@ class AppStart {
|
|
|
82
95
|
|
|
83
96
|
// do something when app is closing
|
|
84
97
|
process.on('beforeExit', async () => {
|
|
85
|
-
cleanupAndExit();
|
|
98
|
+
await cleanupAndExit();
|
|
86
99
|
});
|
|
87
100
|
process.on('exit', (ret) => {
|
|
88
101
|
console.log(`${process.pid} - Process on exit, code: ${ret}`);
|
|
@@ -103,14 +116,21 @@ class AppStart {
|
|
|
103
116
|
const sslKeyPath = config.sslKeyPath || '';
|
|
104
117
|
const sslCrtPath = config.sslCrtPath || '';
|
|
105
118
|
|
|
106
|
-
console.log(
|
|
119
|
+
console.log(`${process.pid} - Starting Web Server, httpPort: ${httpPort}, httpsPort: ${httpsPort}`);
|
|
107
120
|
// for dev to refresh the FE or stop the server
|
|
108
121
|
if (this.debug) {
|
|
109
122
|
WebProcessor.enableDebug('/debug', processDevRequests);
|
|
110
123
|
}
|
|
111
124
|
|
|
112
|
-
httpPort && this.webServer!.startHttp(httpPort, bindIp);
|
|
113
|
-
httpsPort && this.webServer!.startHttps(httpsPort, bindIp, sslKeyPath, sslCrtPath);
|
|
125
|
+
const httpServer = httpPort && this.webServer!.startHttp(httpPort, bindIp);
|
|
126
|
+
const heepsServer = httpsPort && this.webServer!.startHttps(httpsPort, bindIp, sslKeyPath, sslCrtPath);
|
|
127
|
+
|
|
128
|
+
process.on("SIGTERM", () => {
|
|
129
|
+
console.log(`${process.pid} - Worker closing servers...`);
|
|
130
|
+
httpServer && httpServer.close();
|
|
131
|
+
heepsServer && heepsServer.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
114
134
|
}
|
|
115
135
|
}
|
|
116
136
|
|
package/src/app/cleanup-exit.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { appLoader } from './app-loader';
|
|
|
5
5
|
import { DebugService } from '../api/debug-service';
|
|
6
6
|
import { AppCacheGlobal, AppCacheKeys, getAppCache, ServerRequest } from '../models';
|
|
7
7
|
import { cleanupAndExit } from './cleanup-exit';
|
|
8
|
+
import { restartApp } from './app-restart';
|
|
8
9
|
const logger = new Logger('process-dev-requests');
|
|
9
10
|
|
|
10
11
|
function deleteRequireCache(moduleName: string) {
|
|
@@ -42,10 +43,10 @@ export const processDebugMessage = async (msgObject: any) => {
|
|
|
42
43
|
// this only works in debug mode (no clusters)
|
|
43
44
|
DebugService.broadcastRefresh();
|
|
44
45
|
}
|
|
45
|
-
if (msgObject.id === 'debug' && msgObject.message === '
|
|
46
|
-
// Only when it's debug mode, it can go here, otherwise
|
|
47
|
-
console.log(`[server] Received
|
|
48
|
-
cleanupAndExit();
|
|
46
|
+
if (msgObject.id === 'debug' && msgObject.message === 'shutdown') {
|
|
47
|
+
// Only when it's debug mode, it can go here, otherwise shutdown should be processed in processMessageFromWorker
|
|
48
|
+
console.log(`[server] Received shutdown command.`);
|
|
49
|
+
await cleanupAndExit();
|
|
49
50
|
}
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -63,6 +64,18 @@ export async function processRefreshCache(req: ServerRequest) {
|
|
|
63
64
|
}
|
|
64
65
|
}
|
|
65
66
|
|
|
67
|
+
export async function processRestartApp(req: ServerRequest) {
|
|
68
|
+
// if this is a child process, we need to notice parent process to broadcast to all clients to refresh
|
|
69
|
+
if (process.send) {
|
|
70
|
+
// send message to Primary to handle it
|
|
71
|
+
process.send({ id: 'debug', message: 'restartApp' });
|
|
72
|
+
}
|
|
73
|
+
// in case if it's only one process (primary process)
|
|
74
|
+
else {
|
|
75
|
+
await restartApp();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
// this is only for local development
|
|
67
80
|
export async function processDevRequests(req: ServerRequest, res: ServerResponse, rootUrl?: string) {
|
|
68
81
|
res.end();
|
|
@@ -71,15 +84,15 @@ export async function processDevRequests(req: ServerRequest, res: ServerResponse
|
|
|
71
84
|
console.log(`[server] Ignore request from: `, req.url, address.address);
|
|
72
85
|
return true;
|
|
73
86
|
}
|
|
74
|
-
if (req.url === '/debug/
|
|
75
|
-
console.log(`[server] Received
|
|
87
|
+
if (req.url === '/debug/shutdown') {
|
|
88
|
+
console.log(`[server] Received shutdown command.`);
|
|
76
89
|
if (process.send) {
|
|
77
90
|
// send to parent process to kill all
|
|
78
|
-
process.send({ id: 'debug', message: '
|
|
91
|
+
process.send({ id: 'debug', message: 'shutdown' });
|
|
79
92
|
}
|
|
80
93
|
// if it's debug mode (only one process)
|
|
81
94
|
else if (getAppCache().get(AppCacheGlobal, AppCacheKeys.APP_DEBUG) === true) {
|
|
82
|
-
await processDebugMessage({ id: 'debug', message: '
|
|
95
|
+
await processDebugMessage({ id: 'debug', message: 'shutdown' });
|
|
83
96
|
}
|
|
84
97
|
} else if (req.url === '/debug/refresh') {
|
|
85
98
|
await processRefreshCache(req);
|
package/src/app/web-server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { IncomingMessage, ServerResponse } from 'http';
|
|
|
8
8
|
import { Duplex } from 'stream';
|
|
9
9
|
import { WebProcessor } from './web-processor';
|
|
10
10
|
import { DebugService } from '../api/debug-service';
|
|
11
|
+
import cluster from 'cluster';
|
|
11
12
|
const logger = new Logger('web-server');
|
|
12
13
|
|
|
13
14
|
export class WebServer {
|
|
@@ -47,7 +48,7 @@ export class WebServer {
|
|
|
47
48
|
httpServer.on('upgrade', this.handleUpgrade.bind(this));
|
|
48
49
|
|
|
49
50
|
httpServer.listen(httpPort, bindIp, () => {
|
|
50
|
-
logger.info(`Http Server is started: http://localhost:${httpPort}`);
|
|
51
|
+
logger.info(`Http Server ${cluster.worker ? cluster.worker.id : -1} is started: http://localhost:${httpPort}`);
|
|
51
52
|
});
|
|
52
53
|
httpServer.on('error', (error: any) => {
|
|
53
54
|
logger.error('Error occurred on http server', error);
|
|
@@ -88,11 +89,12 @@ export class WebServer {
|
|
|
88
89
|
httpsServer.setTimeout(timeout);
|
|
89
90
|
}
|
|
90
91
|
httpsServer.listen(httpsPort, bindIp, () => {
|
|
91
|
-
logger.info(`Https Server is started: https://localhost:${httpsPort}`);
|
|
92
|
+
logger.info(`Https Server ${cluster.worker ? cluster.worker.id : -1} is started: https://localhost:${httpsPort}`);
|
|
92
93
|
});
|
|
93
94
|
httpsServer.on('error', (error: any) => {
|
|
94
95
|
logger.error('Error occurred on https server', error);
|
|
95
96
|
});
|
|
97
|
+
|
|
96
98
|
return httpsServer;
|
|
97
99
|
}
|
|
98
100
|
}
|
package/src/lib/db/db.ts
CHANGED
|
@@ -2,7 +2,12 @@ import { Logger } from '../logger';
|
|
|
2
2
|
import { DbConfig } from '../../models/db-config';
|
|
3
3
|
|
|
4
4
|
// Instead, Boolean values are stored as integers 0 (false) and 1 (true).
|
|
5
|
+
export type DbFieldExprssionProps = { exprssion: string; params?: (string | number)[] };
|
|
5
6
|
export type DbFieldValue = { [key: string]: string | number };
|
|
7
|
+
export type DbFieldExpression = { [key: string]: string | number | DbFieldExprssionProps };
|
|
8
|
+
const isDbFieldExprssion = (value: any): value is DbFieldExprssionProps => {
|
|
9
|
+
return value && typeof value === 'object' && 'exprssion' in value;
|
|
10
|
+
};
|
|
6
11
|
|
|
7
12
|
const logger = new Logger('db');
|
|
8
13
|
export class Db {
|
|
@@ -252,12 +257,28 @@ export class Db {
|
|
|
252
257
|
return await this.execute(sql, params);
|
|
253
258
|
}
|
|
254
259
|
|
|
255
|
-
public async updateObject(table: string, updateFieldValues:
|
|
260
|
+
public async updateObject(table: string, updateFieldValues: DbFieldExpression, whereFieldValues: DbFieldValue) {
|
|
256
261
|
table = this.replacePrefix(table);
|
|
257
262
|
const fields = Object.keys(updateFieldValues);
|
|
258
|
-
|
|
259
|
-
const params =
|
|
263
|
+
const setClauseParts: string[] = [];
|
|
264
|
+
const params: (string | number)[] = [];
|
|
265
|
+
// let sql = 'UPDATE ' + table + ' SET ' + fields.map((item) => `${item}=?`).join(',');
|
|
266
|
+
// const params = Object.values(updateFieldValues);
|
|
267
|
+
for (const field of fields) {
|
|
268
|
+
const value = updateFieldValues[field];
|
|
269
|
+
|
|
270
|
+
// expression
|
|
271
|
+
if (isDbFieldExprssion(value)) {
|
|
272
|
+
setClauseParts.push(`${field} = ${value.exprssion}`);
|
|
273
|
+
if (value.params) params.push(...value.params);
|
|
274
|
+
} else {
|
|
275
|
+
// static value
|
|
276
|
+
setClauseParts.push(`${field} = ?`);
|
|
277
|
+
params.push(value);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
260
280
|
|
|
281
|
+
let sql = `UPDATE ${table} SET ${setClauseParts.join(', ')}`;
|
|
261
282
|
if (whereFieldValues && Object.keys(whereFieldValues).length > 0) {
|
|
262
283
|
sql +=
|
|
263
284
|
' WHERE ' +
|
|
@@ -25,7 +25,7 @@ export interface IAppSharedStorage {
|
|
|
25
25
|
// this is primary, msg is from a client
|
|
26
26
|
messageFromSubProcess(msgObject: any): void;
|
|
27
27
|
load(appName: string, rootPath: string): Promise<void>;
|
|
28
|
-
save(appName?: string): Promise<void>;
|
|
28
|
+
save(appName?: string, exit?: boolean): Promise<void>;
|
|
29
29
|
get(appName: string, key: string): Promise<string>;
|
|
30
30
|
getWeb(appName: string, key: string): Promise<string>;
|
|
31
31
|
getApi(appName: string, key: string): Promise<string>;
|