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.
@@ -284,7 +284,7 @@ export const AdminReleasePage = () => {
284
284
  isLocal,
285
285
  });
286
286
  const dataResponse = await response.json;
287
- console.log('AdminRelease', dataResponse);
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={onRefreshCacheLocal} class='button-base'>
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}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.50",
3
+ "version": "1.1.52",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -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
- // if it's dev admin, then set app admin cookie as well
68
- let addLoginResponse = {};
69
- const appAdminHookSetCookie = adminApiHelper.getAppAdminHookSetCookie();
70
- if (appAdminHookSetCookie) {
71
- addLoginResponse = await appAdminHookSetCookie(req, res, devAdminSession.u);
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
- expireDays: 360,
109
- path: '/',
110
- httpOnly: false,
111
- secure: true,
112
- sameSite: 'none',
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.update.bind(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
+ };
@@ -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
- const newItem = item.substring(4);
165
- webSettingShortKey[newItem] = webSetting[item];
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, webSettingShortKey, req.locals.cookies());
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(webSettingShortKey) + '</script>');
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,
@@ -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
- broadcast(msgObject);
82
- // if it's suspend, the primary process will exit
83
- if (msgObject.message === 'suspend') {
84
- setTimeout(() => {
85
- console.log(`[server primary] Received suspend command.`, cluster.workers);
86
- cleanupAndExit();
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
- return this.getWithPrefix(appName, AppSharedStorageWebPrefix);
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
  }
@@ -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 : 0;
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
- console.log(`Worker ${worker.pid} died; starting a new one...`);
68
- cluster.fork();
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(`Starting Web Server, httpPort: ${httpPort}, httpsPort: ${httpsPort}`);
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
 
@@ -6,7 +6,7 @@ export const cleanupAndExit = async () => {
6
6
  // save shared storage first
7
7
  if (cluster.isPrimary) {
8
8
  // save only happens once
9
- await appStorage.save();
9
+ await appStorage.save('', true);
10
10
  }
11
11
  process.exit(0);
12
12
  };
@@ -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 === 'suspend') {
46
- // Only when it's debug mode, it can go here, otherwise suspend should be processed in processMessageFromWorker
47
- console.log(`[server] Received suspend command.`);
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/suspend') {
75
- console.log(`[server] Received suspend command.`);
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: 'suspend' });
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: 'suspend' });
95
+ await processDebugMessage({ id: 'debug', message: 'shutdown' });
83
96
  }
84
97
  } else if (req.url === '/debug/refresh') {
85
98
  await processRefreshCache(req);
@@ -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: DbFieldValue, whereFieldValues: DbFieldValue) {
260
+ public async updateObject(table: string, updateFieldValues: DbFieldExpression, whereFieldValues: DbFieldValue) {
256
261
  table = this.replacePrefix(table);
257
262
  const fields = Object.keys(updateFieldValues);
258
- let sql = 'UPDATE ' + table + ' SET ' + fields.map((item) => `${item}=?`).join(',');
259
- const params = Object.values(updateFieldValues);
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>;