lupine.api 1.1.51 → 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}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.51",
3
+ "version": "1.1.52",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -69,14 +69,15 @@ export const devAdminAuth = async (req: ServerRequest, res: ServerResponse) => {
69
69
  ApiHelper.sendJson(req, res, response);
70
70
  return true;
71
71
  }
72
- // if it's dev admin, then set app admin cookie as well
73
- let addLoginResponse = {};
74
- const appAdminHookSetCookie = adminApiHelper.getAppAdminHookSetCookie();
75
- if (appAdminHookSetCookie) {
76
- addLoginResponse = await appAdminHookSetCookie(req, res, devAdminSession.u);
77
- }
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
+ // }
78
79
  const response = {
79
- ...addLoginResponse,
80
+ // ...addLoginResponse,
80
81
  status: 'ok',
81
82
  message: langHelper.getLang('shared:login_success'),
82
83
  devLogin: CryptoUtils.encrypt(JSON.stringify(devAdminSession), cryptoKey),
@@ -109,13 +110,13 @@ export const devAdminAuth = async (req: ServerRequest, res: ServerResponse) => {
109
110
  message: langHelper.getLang('shared:login_success'),
110
111
  devLogin: tokenCookie,
111
112
  };
112
- req.locals.setCookie('_token', tokenCookie, {
113
- expireDays: 360,
114
- path: '/',
115
- httpOnly: false,
116
- secure: true,
117
- sameSite: 'none',
118
- });
113
+ // req.locals.setCookie('_token', tokenCookie, {
114
+ // expireDays: 360,
115
+ // path: '/',
116
+ // httpOnly: false,
117
+ // secure: true,
118
+ // sameSite: 'none',
119
+ // });
119
120
  ApiHelper.sendJson(req, res, response);
120
121
  return true;
121
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';
@@ -36,11 +37,13 @@ export class AdminRelease implements IApiBase {
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
  }
@@ -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
+ };
@@ -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
 
@@ -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
  }