@nginstack/engine-node 78.0.1 → 78.0.9
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 +37 -37
- package/index.js +1 -1
- package/lib/Engine.js +535 -535
- package/lib/Engine.spec.js +96 -96
- package/package.json +2 -2
package/lib/Engine.js
CHANGED
|
@@ -1,535 +1,535 @@
|
|
|
1
|
-
const child_process = require('child_process');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const del = require('del');
|
|
5
|
-
const request = require('request');
|
|
6
|
-
const getPort = require('get-port');
|
|
7
|
-
const sleep = require('thread-sleep');
|
|
8
|
-
const findUp = require('find-up');
|
|
9
|
-
const dotenv = require('dotenv');
|
|
10
|
-
const crypto = require('crypto');
|
|
11
|
-
const os = require('os');
|
|
12
|
-
const tmp = require('tmp');
|
|
13
|
-
const net = require('net');
|
|
14
|
-
|
|
15
|
-
tmp.setGracefulCleanup();
|
|
16
|
-
|
|
17
|
-
const ENGINE_APP = os.platform() == 'win32' ? 'engine.exe' : 'engine';
|
|
18
|
-
const NGINT_APP_PATH = '/bin/win64/ngint.exe';
|
|
19
|
-
const EXIT_CODE_RESTART_REQUIRED = 4;
|
|
20
|
-
const ENGINE_STATUS_URL = '/api/service-status/v1/status';
|
|
21
|
-
const ENGINE_DOWNLOAD_URL = `/dist/${ENGINE_APP}`;
|
|
22
|
-
const ENGINE_STARTUP_TIMEOUT = 15 * 60000; // 15 minutes
|
|
23
|
-
const ENGINE_SHUTDOWN_TIMEOUT = 30000; // 30 seconds
|
|
24
|
-
const STATUS_MONITOR_INTERVAL = 500;
|
|
25
|
-
const UPDATE_INTERVAL = 30000; // 30 seconds
|
|
26
|
-
const INSTANCE_ID_LENGTH = 32;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* @typedef {Object} EngineOptions
|
|
30
|
-
* @param {number=} startupTimeout Time limit in milliseconds used to wait the Engine
|
|
31
|
-
* startup.
|
|
32
|
-
* @param {number=} httpPort HTTP port to be used by the new Engine process. If not
|
|
33
|
-
* informed, a random port will be used.
|
|
34
|
-
* @param {Array<string>=} jazArchives JAZ files to be included into
|
|
35
|
-
* Union File System (UFS). If the informed path is a directory, it will load all JAZ files
|
|
36
|
-
* contained in it.
|
|
37
|
-
* @param {string=} jazSearchPath Directory containing JAZ files that will be loaded
|
|
38
|
-
* by the Engine.
|
|
39
|
-
* @param {Array<string>=} ufsDirectories Directories to be included into
|
|
40
|
-
* Union File System (UFS).
|
|
41
|
-
* @param {boolean=} disableIdoSyncToDisk Disable IDO sync to disk to gain writing
|
|
42
|
-
* performance on DBCache. This options increases the risk of dbCache data corruption if the
|
|
43
|
-
* Engine is stopped forcefully.
|
|
44
|
-
* @param {string=} instanceId Engine instance id. This option should be used exclusively to
|
|
45
|
-
* create Engine instances with stable ids for testing purposes. Instance ids are 32 caracteres
|
|
46
|
-
* strings.
|
|
47
|
-
* @param {boolean=} v8 Use V8 as default JavaScript runtime.
|
|
48
|
-
* @param {boolean=} redirectLogsToStdout Redirect Engines logs to console stdout.
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
class Engine {
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Creates a new Engine wrapper that can be used to control a nginstack Engine process.
|
|
55
|
-
* @param {string} installPath Directory where the Engine will be installed.
|
|
56
|
-
* @param {string} serverUrl URL of the Engine used to server the database.
|
|
57
|
-
* @param {string} dbName Database name.
|
|
58
|
-
* @param {EngineOptions=} opt_options Advanced options of this class.
|
|
59
|
-
*/
|
|
60
|
-
constructor(installPath, serverUrl, dbName, opt_options) {
|
|
61
|
-
const options = opt_options || {};
|
|
62
|
-
this.serverUrl = serverUrl;
|
|
63
|
-
this.dbName = dbName;
|
|
64
|
-
this.installPath = installPath;
|
|
65
|
-
this.enginePath = path.join(this.installPath, ENGINE_APP);
|
|
66
|
-
this.ngintPath_ = path.join(this.installPath, NGINT_APP_PATH);
|
|
67
|
-
this.logsPath = path.join(this.installPath, 'logs');
|
|
68
|
-
this.process_ = null;
|
|
69
|
-
this.lastStatusCode_ = 0;
|
|
70
|
-
this.monitorEnabled_ = false;
|
|
71
|
-
this.monitorTimeoutId_ = null;
|
|
72
|
-
this.httpPort = options.httpPort || 0;
|
|
73
|
-
this.useRandomHttpPort_ = !this.httpPort;
|
|
74
|
-
this.jazArchives_ = options.jazArchives || [];
|
|
75
|
-
this.jazSearchPath_ = options.jazSearchPath || '';
|
|
76
|
-
this.ufsDirectories_ = options.ufsDirectories || [];
|
|
77
|
-
this.disableIdoSyncToDisk_ = options.disableIdoSyncToDisk || false;
|
|
78
|
-
this.v8_ = options.v8 || false;
|
|
79
|
-
this.startupTimeout = options.startupTimeout || ENGINE_STARTUP_TIMEOUT;
|
|
80
|
-
this.startedSuccessfully = false;
|
|
81
|
-
this.inShutdown_ = false;
|
|
82
|
-
this.redirectLogsToStdout_ = options.redirectLogsToStdout || false;
|
|
83
|
-
this.onEngineClose_ = null;
|
|
84
|
-
if (options.instanceId) {
|
|
85
|
-
if (options.instanceId.length === INSTANCE_ID_LENGTH) {
|
|
86
|
-
this.instanceId_ = options.instanceId;
|
|
87
|
-
} else {
|
|
88
|
-
throw new Error(`Invalid instanceId (${options.instanceId}). Inform a string ` +
|
|
89
|
-
`with length ${INSTANCE_ID_LENGTH}.`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Returns true if the Engine is installed in the informed install path.
|
|
96
|
-
* @return {boolean}
|
|
97
|
-
*/
|
|
98
|
-
get installed() {
|
|
99
|
-
return fs.existsSync(this.enginePath);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Returns true if the Engine was started and if it is capable to respond HTTP requests.
|
|
104
|
-
* @return {boolean}
|
|
105
|
-
*/
|
|
106
|
-
get running() {
|
|
107
|
-
return this.process_ !== null && this.lastStatusCode_ === 200;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* URL that can be used to do HTTP requests to this Engine.
|
|
112
|
-
* @return {string}
|
|
113
|
-
*/
|
|
114
|
-
get url() {
|
|
115
|
-
return 'http://127.0.0.1:' + this.httpPort;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Starts the Engine status monitor, used to discover if the Engine is running.
|
|
120
|
-
* @private
|
|
121
|
-
*/
|
|
122
|
-
startStatusMonitor_() {
|
|
123
|
-
if (this.monitorTimeoutId_) {
|
|
124
|
-
clearTimeout(this.monitorTimeoutId_);
|
|
125
|
-
this.monitorTimeoutId_ = null;
|
|
126
|
-
}
|
|
127
|
-
this.monitorEnabled_ = true;
|
|
128
|
-
request.get(`http://127.0.0.1:${this.httpPort}${ENGINE_STATUS_URL}`, (_error, response) => {
|
|
129
|
-
if (response && response.statusCode) {
|
|
130
|
-
this.lastStatusCode_ = response.statusCode;
|
|
131
|
-
if (this.lastStatusCode_ === 500 && !this.inShutdown_) {
|
|
132
|
-
console.info(`Engine response (${response.statusCode}):\n${response.body}`);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
this.lastStatusCode_ = null;
|
|
136
|
-
}
|
|
137
|
-
if (this.monitorEnabled_) {
|
|
138
|
-
this.monitorTimeoutId_ = setTimeout(this.startStatusMonitor_.bind(this),
|
|
139
|
-
STATUS_MONITOR_INTERVAL);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Stops the Engine status monitor, started previously by {@link #startStatusMonitor_}.
|
|
146
|
-
* @private
|
|
147
|
-
*/
|
|
148
|
-
stopStatusMonitor_() {
|
|
149
|
-
this.monitorEnabled_ = false;
|
|
150
|
-
if (this.monitorTimeoutId_) {
|
|
151
|
-
clearTimeout(this.monitorTimeoutId_);
|
|
152
|
-
this.monitorTimeoutId_ = null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Starts the monitoring of the Engine logs files. The log files output will be redirected to
|
|
158
|
-
* console.
|
|
159
|
-
* @return {string} Path to the logger configuration that should be used by Engine to
|
|
160
|
-
* enable the redirection of the logs outputs to the console.
|
|
161
|
-
* @private
|
|
162
|
-
*/
|
|
163
|
-
startLogsMonitoring_() {
|
|
164
|
-
if (this.tempLogConfigFile_ || this.logsServer_) {
|
|
165
|
-
this.stopLogsMonitoring_();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
this.tempLogConfigFile_ = tmp.fileSync({
|
|
169
|
-
prefix: 'log-',
|
|
170
|
-
postfix: '.config',
|
|
171
|
-
detachDescriptor: true
|
|
172
|
-
});
|
|
173
|
-
try {
|
|
174
|
-
let localSocketPath;
|
|
175
|
-
let serverName;
|
|
176
|
-
if (process.platform === 'win32') {
|
|
177
|
-
serverName = path.basename(this.tempLogConfigFile_.name);
|
|
178
|
-
localSocketPath = path.join('\\\\.\\pipe', serverName);
|
|
179
|
-
} else {
|
|
180
|
-
localSocketPath = this.tempLogConfigFile_.name + '.socket';
|
|
181
|
-
serverName = localSocketPath;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
this.logsServer_ = net.createServer((client) => {
|
|
185
|
-
client.on('data', data => {
|
|
186
|
-
const text = data.toString('utf-8').trim();
|
|
187
|
-
if (text) {
|
|
188
|
-
console.log(text);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
this.logsServer_.on('error', err => console.error('Error on local socket:', err));
|
|
193
|
-
this.logsServer_.listen(localSocketPath);
|
|
194
|
-
|
|
195
|
-
fs.writeFileSync(this.tempLogConfigFile_.fd,
|
|
196
|
-
'logger.appender.localSocket=LocalSocketAppender\n' +
|
|
197
|
-
`logger.appender.localSocket.serverName=${serverName}\n` +
|
|
198
|
-
'logger.appender.localSocket.layout=PatternLayout\n' +
|
|
199
|
-
'logger.appender.localSocket.layout.pattern=%d %.8t [%p] %c - %m %e%n\n' +
|
|
200
|
-
'logger.appender.localSocket.layout.dateFormat=dd/mm hh:nn:ss\n' +
|
|
201
|
-
'logger.appender.localSocket.filter1=PriorityFilter\n' +
|
|
202
|
-
'logger.appender.localSocket.filter1.match=PROGRESS\n' +
|
|
203
|
-
'logger.appender.localSocket.filter1.onMatch=DENY\n' +
|
|
204
|
-
'logger.appender.localSocket.filter1.onMismatch=NEUTRAL\n' +
|
|
205
|
-
'logger.additive.ngin=true\n' +
|
|
206
|
-
'logger.rootCategory=INFO,default,progress,jsdebugger,localSocket'
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
} finally {
|
|
210
|
-
fs.closeSync(this.tempLogConfigFile_.fd);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return this.tempLogConfigFile_.name;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Stops the monitoring of the Engine logs files.
|
|
218
|
-
* @private
|
|
219
|
-
*/
|
|
220
|
-
stopLogsMonitoring_() {
|
|
221
|
-
if (this.tempLogConfigFile_) {
|
|
222
|
-
this.tempLogConfigFile_.removeCallback();
|
|
223
|
-
this.tempLogConfigFile_ = null;
|
|
224
|
-
}
|
|
225
|
-
if (this.logsServer_) {
|
|
226
|
-
this.logsServer_.close();
|
|
227
|
-
this.logsServer_ = null;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Downloads the Engine and install it into the informed install path.
|
|
233
|
-
* @return {Promise}
|
|
234
|
-
*/
|
|
235
|
-
install() {
|
|
236
|
-
return new Promise((resolve, reject) => {
|
|
237
|
-
if (this.installed) {
|
|
238
|
-
resolve();
|
|
239
|
-
} else {
|
|
240
|
-
fs.mkdir(this.installPath, { recursive: true }, (err) => {
|
|
241
|
-
if (err && err.code !== 'EEXIST') {
|
|
242
|
-
reject(err);
|
|
243
|
-
} else {
|
|
244
|
-
let httpUrl = this.serverUrl;
|
|
245
|
-
if (!httpUrl.startsWith('http://') && !httpUrl.startsWith('https://')) {
|
|
246
|
-
httpUrl = 'http://' + httpUrl;
|
|
247
|
-
}
|
|
248
|
-
request.get(new URL(ENGINE_DOWNLOAD_URL, httpUrl).toString())
|
|
249
|
-
.pipe(fs.createWriteStream(this.enginePath))
|
|
250
|
-
.on('close', () => {
|
|
251
|
-
if (os.platform() != 'win32') {
|
|
252
|
-
fs.chmodSync(path.join(this.installPath, ENGINE_APP), '755');
|
|
253
|
-
}
|
|
254
|
-
resolve();
|
|
255
|
-
})
|
|
256
|
-
.on('error', (reqErr) => {
|
|
257
|
-
reject(reqErr);
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Remove all files of the informed install path.
|
|
267
|
-
* @return {Promise}
|
|
268
|
-
*/
|
|
269
|
-
uninstall() {
|
|
270
|
-
return del(this.installPath.replace(/\\/g, '/'), {
|
|
271
|
-
force: true
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Starts the Engine and wait for it to be able to handle HTTP requests. It will wait the full
|
|
277
|
-
* load and synchronization of the Engine DBCache.
|
|
278
|
-
* @return {Promise}
|
|
279
|
-
*/
|
|
280
|
-
start() {
|
|
281
|
-
if (this.instanceId_) {
|
|
282
|
-
const dirPath = path.resolve(this.installPath, 'conf');
|
|
283
|
-
if (!fs.existsSync(dirPath)) {
|
|
284
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
285
|
-
}
|
|
286
|
-
fs.writeFileSync(path.resolve(dirPath, 'engine_id.dat'), this.instanceId_);
|
|
287
|
-
}
|
|
288
|
-
return this.install().then(() => {
|
|
289
|
-
if (this.useRandomHttpPort_) {
|
|
290
|
-
return getPort();
|
|
291
|
-
} else {
|
|
292
|
-
return Promise.resolve(this.httpPort);
|
|
293
|
-
}
|
|
294
|
-
}).then(port => {
|
|
295
|
-
this.httpPort = port;
|
|
296
|
-
return new Promise((resolve, reject) => {
|
|
297
|
-
console.info(`Starting Engine process at port ${this.httpPort}. Server: ${this.serverUrl}` +
|
|
298
|
-
`. Database: ${this.dbName}. Path: "${this.installPath}".`);
|
|
299
|
-
const args = [this.serverUrl, this.dbName,
|
|
300
|
-
'--disableAutoRestart',
|
|
301
|
-
'--disableGui',
|
|
302
|
-
'--httpPort=' + this.httpPort];
|
|
303
|
-
|
|
304
|
-
const jazArchives = this.jazSearchPath_ && fs.existsSync(this.jazSearchPath_)
|
|
305
|
-
? fs.readdirSync(this.jazSearchPath_)
|
|
306
|
-
.filter(name => name.endsWith('.jaz'))
|
|
307
|
-
.map(name => path.resolve(this.jazSearchPath_, name))
|
|
308
|
-
: [];
|
|
309
|
-
|
|
310
|
-
(jazArchives.concat(this.jazArchives_)).forEach(jazPath => {
|
|
311
|
-
args.push('--jazArchive=\'' + jazPath + '\'');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
this.ufsDirectories_.forEach(dirPath => {
|
|
315
|
-
args.push('--ufsDirectory=\'' + dirPath + '\'');
|
|
316
|
-
});
|
|
317
|
-
if (this.disableIdoSyncToDisk_) {
|
|
318
|
-
args.push('--disableIdoSyncToDisk');
|
|
319
|
-
}
|
|
320
|
-
if (this.v8_) {
|
|
321
|
-
args.push('-v8');
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (this.redirectLogsToStdout_) {
|
|
325
|
-
const logConfig = this.startLogsMonitoring_();
|
|
326
|
-
args.push('--additionalLoggerConfig=\'' + logConfig + '\'');
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
this.inShutdown_ = false;
|
|
330
|
-
this.process_ = child_process.spawn(path.join(this.installPath, ENGINE_APP),
|
|
331
|
-
args,
|
|
332
|
-
{
|
|
333
|
-
cwd: this.installPath,
|
|
334
|
-
windowsHide: true
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
this.process_.on('error', (err) => {
|
|
338
|
-
console.error('Error starting Engine process:', err);
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
this.process_.stdout.on('data', (data) => {
|
|
342
|
-
console.info(`Engine stdout: ${data}`);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
this.process_.stderr.on('data', (data) => {
|
|
346
|
-
console.error(`Engine stderr: ${data}`);
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
let intervalId = null;
|
|
350
|
-
|
|
351
|
-
this.process_.on('close', (code) => {
|
|
352
|
-
this.stopStatusMonitor_();
|
|
353
|
-
if (this.redirectLogsToStdout_) {
|
|
354
|
-
this.stopLogsMonitoring_();
|
|
355
|
-
}
|
|
356
|
-
if (intervalId) {
|
|
357
|
-
clearInterval(intervalId);
|
|
358
|
-
intervalId = null;
|
|
359
|
-
}
|
|
360
|
-
console.info(`Engine process exited with code ${code || 0}`);
|
|
361
|
-
this.process_ = null;
|
|
362
|
-
this.lastStatusCode_ = 0;
|
|
363
|
-
this.httpPort = 0;
|
|
364
|
-
if (code === EXIT_CODE_RESTART_REQUIRED) {
|
|
365
|
-
console.info('Engine restart was required to finish the upgrade');
|
|
366
|
-
sleep(UPDATE_INTERVAL);
|
|
367
|
-
this.start().then(resolve, reject);
|
|
368
|
-
} else {
|
|
369
|
-
if (!this.startedSuccessfully) {
|
|
370
|
-
const msg = `Engine startup failed with error code ${code || 0}`;
|
|
371
|
-
console.error(msg);
|
|
372
|
-
reject(new Error(msg));
|
|
373
|
-
}
|
|
374
|
-
if (this.onEngineClose_) {
|
|
375
|
-
this.onEngineClose_();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
this.startedSuccessfully = false;
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
this.startStatusMonitor_();
|
|
382
|
-
intervalId = setInterval(() => {
|
|
383
|
-
if (this.process_ && this.monitorEnabled_) {
|
|
384
|
-
if (this.lastStatusCode_) {
|
|
385
|
-
if (this.lastStatusCode_ === 200) {
|
|
386
|
-
this.startedSuccessfully = true;
|
|
387
|
-
clearInterval(intervalId);
|
|
388
|
-
intervalId = null;
|
|
389
|
-
resolve();
|
|
390
|
-
} else if (this.lastStatusCode_ === 500) {
|
|
391
|
-
clearInterval(intervalId);
|
|
392
|
-
intervalId = null;
|
|
393
|
-
const msg = 'Unexpected Engine status code: ' + this.lastStatusCode_;
|
|
394
|
-
console.error(msg);
|
|
395
|
-
this.sendShutdownCommand_();
|
|
396
|
-
reject(new Error(msg));
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
} else {
|
|
400
|
-
if (intervalId) {
|
|
401
|
-
clearInterval(intervalId);
|
|
402
|
-
intervalId = null;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
}, STATUS_MONITOR_INTERVAL);
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Stops the Engine process.
|
|
412
|
-
* @return {Promise}
|
|
413
|
-
*/
|
|
414
|
-
stop() {
|
|
415
|
-
console.info('Engine stop requested');
|
|
416
|
-
return new Promise(resolve => {
|
|
417
|
-
if (this.running) {
|
|
418
|
-
this.onEngineClose_ = () => {
|
|
419
|
-
console.info('Engine stopped');
|
|
420
|
-
if (this.redirectLogsToStdout_) {
|
|
421
|
-
// Defers the resolve call to give time to log watching to be finished
|
|
422
|
-
setTimeout(resolve, 20);
|
|
423
|
-
} else {
|
|
424
|
-
resolve();
|
|
425
|
-
}
|
|
426
|
-
this.inShutdown_ = false;
|
|
427
|
-
};
|
|
428
|
-
this.sendShutdownCommand_();
|
|
429
|
-
} else {
|
|
430
|
-
resolve();
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Request a graceful shutdown to Engine. If request fails, send a force kill command.
|
|
437
|
-
* @private
|
|
438
|
-
*/
|
|
439
|
-
sendShutdownCommand_() {
|
|
440
|
-
if (this.process_) {
|
|
441
|
-
let forceKill = true;
|
|
442
|
-
this.inShutdown_ = true;
|
|
443
|
-
if (os.platform() == 'win32') {
|
|
444
|
-
if (fs.existsSync(this.ngintPath_)) {
|
|
445
|
-
try {
|
|
446
|
-
console.info('Shutdown command sent to Engine process');
|
|
447
|
-
child_process.execFileSync(
|
|
448
|
-
this.ngintPath_,
|
|
449
|
-
['shutdown', this.process_.pid, ENGINE_SHUTDOWN_TIMEOUT],
|
|
450
|
-
{
|
|
451
|
-
cwd: this.installPath,
|
|
452
|
-
windowsHide: true
|
|
453
|
-
}
|
|
454
|
-
);
|
|
455
|
-
forceKill = false;
|
|
456
|
-
} catch (e) {
|
|
457
|
-
console.error('Error stopping Engine', e);
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
console.info('ngint was not found');
|
|
461
|
-
}
|
|
462
|
-
} else {
|
|
463
|
-
process.kill(this.process_.pid, 'SIGTERM');
|
|
464
|
-
}
|
|
465
|
-
if (forceKill) {
|
|
466
|
-
console.info('Kill signal sent to Engine process');
|
|
467
|
-
this.process_.kill();
|
|
468
|
-
this.process_ = null;
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Creates a new Engine instance using the ".env" config file on the repository root.
|
|
475
|
-
*
|
|
476
|
-
* The Engine will be installed on `<root-dir>/.engine/<database-name>` directory. If was not
|
|
477
|
-
* informed the option `jazSearchPath` it will load all JAZ files inside the
|
|
478
|
-
* `<root-dir>/.build` directory (or "build" directory if ".build" does not exists). The
|
|
479
|
-
* option `disableIdoSyncToDisk` will be enabled by default.
|
|
480
|
-
*
|
|
481
|
-
* See the site https://nginstack.com for the more details about the adopted repository
|
|
482
|
-
* conventions.
|
|
483
|
-
* @param {EngineOptions=} options Advanced Engine options.
|
|
484
|
-
*/
|
|
485
|
-
static newUsingWorkspaceConfig(options) {
|
|
486
|
-
options = options || {};
|
|
487
|
-
const workspacePath = getWorkspacePath();
|
|
488
|
-
dotenv.config({
|
|
489
|
-
path: path.resolve(workspacePath, '.env')
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
if (!('jazSearchPath' in options)) {
|
|
493
|
-
let jazSearchPath = path.resolve(workspacePath, '.build');
|
|
494
|
-
if (!fs.existsSync(jazSearchPath)) {
|
|
495
|
-
// Compatibility with previous build directory name
|
|
496
|
-
jazSearchPath = path.resolve(workspacePath, 'build');
|
|
497
|
-
}
|
|
498
|
-
if (fs.existsSync(jazSearchPath)) {
|
|
499
|
-
options.jazSearchPath = jazSearchPath;
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (!('disableIdoSyncToDisk' in options)) {
|
|
503
|
-
options.disableIdoSyncToDisk = true;
|
|
504
|
-
}
|
|
505
|
-
options.v8 = options.v8 || process.argv.includes('-v8');
|
|
506
|
-
options.instanceId = Engine.formatWorkspaceInstanceId();
|
|
507
|
-
|
|
508
|
-
return new Engine(
|
|
509
|
-
path.resolve(workspacePath, '.engine', process.env.ENGINE_DATABASE),
|
|
510
|
-
process.env.ENGINE_URL,
|
|
511
|
-
process.env.ENGINE_DATABASE,
|
|
512
|
-
options
|
|
513
|
-
);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Creates an stable Engine instance id using the hostname and the workspace path.
|
|
518
|
-
*/
|
|
519
|
-
static formatWorkspaceInstanceId() {
|
|
520
|
-
const hash = crypto.createHash('sha256');
|
|
521
|
-
hash.update(os.hostname());
|
|
522
|
-
hash.update(getWorkspacePath());
|
|
523
|
-
return ('_' + hash.digest('hex').toUpperCase()).substring(0, INSTANCE_ID_LENGTH);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
function getWorkspacePath() {
|
|
528
|
-
let workspacePath = findUp.sync('.git', { type: 'directory' });
|
|
529
|
-
if (!workspacePath) {
|
|
530
|
-
workspacePath = findUp.sync('package.json');
|
|
531
|
-
}
|
|
532
|
-
return path.dirname(workspacePath);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
module.exports = Engine;
|
|
1
|
+
const child_process = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const del = require('del');
|
|
5
|
+
const request = require('request');
|
|
6
|
+
const getPort = require('get-port');
|
|
7
|
+
const sleep = require('thread-sleep');
|
|
8
|
+
const findUp = require('find-up');
|
|
9
|
+
const dotenv = require('dotenv');
|
|
10
|
+
const crypto = require('crypto');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const tmp = require('tmp');
|
|
13
|
+
const net = require('net');
|
|
14
|
+
|
|
15
|
+
tmp.setGracefulCleanup();
|
|
16
|
+
|
|
17
|
+
const ENGINE_APP = os.platform() == 'win32' ? 'engine.exe' : 'engine';
|
|
18
|
+
const NGINT_APP_PATH = '/bin/win64/ngint.exe';
|
|
19
|
+
const EXIT_CODE_RESTART_REQUIRED = 4;
|
|
20
|
+
const ENGINE_STATUS_URL = '/api/service-status/v1/status';
|
|
21
|
+
const ENGINE_DOWNLOAD_URL = `/dist/${ENGINE_APP}`;
|
|
22
|
+
const ENGINE_STARTUP_TIMEOUT = 15 * 60000; // 15 minutes
|
|
23
|
+
const ENGINE_SHUTDOWN_TIMEOUT = 30000; // 30 seconds
|
|
24
|
+
const STATUS_MONITOR_INTERVAL = 500;
|
|
25
|
+
const UPDATE_INTERVAL = 30000; // 30 seconds
|
|
26
|
+
const INSTANCE_ID_LENGTH = 32;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} EngineOptions
|
|
30
|
+
* @param {number=} startupTimeout Time limit in milliseconds used to wait the Engine
|
|
31
|
+
* startup.
|
|
32
|
+
* @param {number=} httpPort HTTP port to be used by the new Engine process. If not
|
|
33
|
+
* informed, a random port will be used.
|
|
34
|
+
* @param {Array<string>=} jazArchives JAZ files to be included into
|
|
35
|
+
* Union File System (UFS). If the informed path is a directory, it will load all JAZ files
|
|
36
|
+
* contained in it.
|
|
37
|
+
* @param {string=} jazSearchPath Directory containing JAZ files that will be loaded
|
|
38
|
+
* by the Engine.
|
|
39
|
+
* @param {Array<string>=} ufsDirectories Directories to be included into
|
|
40
|
+
* Union File System (UFS).
|
|
41
|
+
* @param {boolean=} disableIdoSyncToDisk Disable IDO sync to disk to gain writing
|
|
42
|
+
* performance on DBCache. This options increases the risk of dbCache data corruption if the
|
|
43
|
+
* Engine is stopped forcefully.
|
|
44
|
+
* @param {string=} instanceId Engine instance id. This option should be used exclusively to
|
|
45
|
+
* create Engine instances with stable ids for testing purposes. Instance ids are 32 caracteres
|
|
46
|
+
* strings.
|
|
47
|
+
* @param {boolean=} v8 Use V8 as default JavaScript runtime.
|
|
48
|
+
* @param {boolean=} redirectLogsToStdout Redirect Engines logs to console stdout.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
class Engine {
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Creates a new Engine wrapper that can be used to control a nginstack Engine process.
|
|
55
|
+
* @param {string} installPath Directory where the Engine will be installed.
|
|
56
|
+
* @param {string} serverUrl URL of the Engine used to server the database.
|
|
57
|
+
* @param {string} dbName Database name.
|
|
58
|
+
* @param {EngineOptions=} opt_options Advanced options of this class.
|
|
59
|
+
*/
|
|
60
|
+
constructor(installPath, serverUrl, dbName, opt_options) {
|
|
61
|
+
const options = opt_options || {};
|
|
62
|
+
this.serverUrl = serverUrl;
|
|
63
|
+
this.dbName = dbName;
|
|
64
|
+
this.installPath = installPath;
|
|
65
|
+
this.enginePath = path.join(this.installPath, ENGINE_APP);
|
|
66
|
+
this.ngintPath_ = path.join(this.installPath, NGINT_APP_PATH);
|
|
67
|
+
this.logsPath = path.join(this.installPath, 'logs');
|
|
68
|
+
this.process_ = null;
|
|
69
|
+
this.lastStatusCode_ = 0;
|
|
70
|
+
this.monitorEnabled_ = false;
|
|
71
|
+
this.monitorTimeoutId_ = null;
|
|
72
|
+
this.httpPort = options.httpPort || 0;
|
|
73
|
+
this.useRandomHttpPort_ = !this.httpPort;
|
|
74
|
+
this.jazArchives_ = options.jazArchives || [];
|
|
75
|
+
this.jazSearchPath_ = options.jazSearchPath || '';
|
|
76
|
+
this.ufsDirectories_ = options.ufsDirectories || [];
|
|
77
|
+
this.disableIdoSyncToDisk_ = options.disableIdoSyncToDisk || false;
|
|
78
|
+
this.v8_ = options.v8 || false;
|
|
79
|
+
this.startupTimeout = options.startupTimeout || ENGINE_STARTUP_TIMEOUT;
|
|
80
|
+
this.startedSuccessfully = false;
|
|
81
|
+
this.inShutdown_ = false;
|
|
82
|
+
this.redirectLogsToStdout_ = options.redirectLogsToStdout || false;
|
|
83
|
+
this.onEngineClose_ = null;
|
|
84
|
+
if (options.instanceId) {
|
|
85
|
+
if (options.instanceId.length === INSTANCE_ID_LENGTH) {
|
|
86
|
+
this.instanceId_ = options.instanceId;
|
|
87
|
+
} else {
|
|
88
|
+
throw new Error(`Invalid instanceId (${options.instanceId}). Inform a string ` +
|
|
89
|
+
`with length ${INSTANCE_ID_LENGTH}.`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Returns true if the Engine is installed in the informed install path.
|
|
96
|
+
* @return {boolean}
|
|
97
|
+
*/
|
|
98
|
+
get installed() {
|
|
99
|
+
return fs.existsSync(this.enginePath);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Returns true if the Engine was started and if it is capable to respond HTTP requests.
|
|
104
|
+
* @return {boolean}
|
|
105
|
+
*/
|
|
106
|
+
get running() {
|
|
107
|
+
return this.process_ !== null && this.lastStatusCode_ === 200;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* URL that can be used to do HTTP requests to this Engine.
|
|
112
|
+
* @return {string}
|
|
113
|
+
*/
|
|
114
|
+
get url() {
|
|
115
|
+
return 'http://127.0.0.1:' + this.httpPort;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Starts the Engine status monitor, used to discover if the Engine is running.
|
|
120
|
+
* @private
|
|
121
|
+
*/
|
|
122
|
+
startStatusMonitor_() {
|
|
123
|
+
if (this.monitorTimeoutId_) {
|
|
124
|
+
clearTimeout(this.monitorTimeoutId_);
|
|
125
|
+
this.monitorTimeoutId_ = null;
|
|
126
|
+
}
|
|
127
|
+
this.monitorEnabled_ = true;
|
|
128
|
+
request.get(`http://127.0.0.1:${this.httpPort}${ENGINE_STATUS_URL}`, (_error, response) => {
|
|
129
|
+
if (response && response.statusCode) {
|
|
130
|
+
this.lastStatusCode_ = response.statusCode;
|
|
131
|
+
if (this.lastStatusCode_ === 500 && !this.inShutdown_) {
|
|
132
|
+
console.info(`Engine response (${response.statusCode}):\n${response.body}`);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
this.lastStatusCode_ = null;
|
|
136
|
+
}
|
|
137
|
+
if (this.monitorEnabled_) {
|
|
138
|
+
this.monitorTimeoutId_ = setTimeout(this.startStatusMonitor_.bind(this),
|
|
139
|
+
STATUS_MONITOR_INTERVAL);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Stops the Engine status monitor, started previously by {@link #startStatusMonitor_}.
|
|
146
|
+
* @private
|
|
147
|
+
*/
|
|
148
|
+
stopStatusMonitor_() {
|
|
149
|
+
this.monitorEnabled_ = false;
|
|
150
|
+
if (this.monitorTimeoutId_) {
|
|
151
|
+
clearTimeout(this.monitorTimeoutId_);
|
|
152
|
+
this.monitorTimeoutId_ = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Starts the monitoring of the Engine logs files. The log files output will be redirected to
|
|
158
|
+
* console.
|
|
159
|
+
* @return {string} Path to the logger configuration that should be used by Engine to
|
|
160
|
+
* enable the redirection of the logs outputs to the console.
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
startLogsMonitoring_() {
|
|
164
|
+
if (this.tempLogConfigFile_ || this.logsServer_) {
|
|
165
|
+
this.stopLogsMonitoring_();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.tempLogConfigFile_ = tmp.fileSync({
|
|
169
|
+
prefix: 'log-',
|
|
170
|
+
postfix: '.config',
|
|
171
|
+
detachDescriptor: true
|
|
172
|
+
});
|
|
173
|
+
try {
|
|
174
|
+
let localSocketPath;
|
|
175
|
+
let serverName;
|
|
176
|
+
if (process.platform === 'win32') {
|
|
177
|
+
serverName = path.basename(this.tempLogConfigFile_.name);
|
|
178
|
+
localSocketPath = path.join('\\\\.\\pipe', serverName);
|
|
179
|
+
} else {
|
|
180
|
+
localSocketPath = this.tempLogConfigFile_.name + '.socket';
|
|
181
|
+
serverName = localSocketPath;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.logsServer_ = net.createServer((client) => {
|
|
185
|
+
client.on('data', data => {
|
|
186
|
+
const text = data.toString('utf-8').trim();
|
|
187
|
+
if (text) {
|
|
188
|
+
console.log(text);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
this.logsServer_.on('error', err => console.error('Error on local socket:', err));
|
|
193
|
+
this.logsServer_.listen(localSocketPath);
|
|
194
|
+
|
|
195
|
+
fs.writeFileSync(this.tempLogConfigFile_.fd,
|
|
196
|
+
'logger.appender.localSocket=LocalSocketAppender\n' +
|
|
197
|
+
`logger.appender.localSocket.serverName=${serverName}\n` +
|
|
198
|
+
'logger.appender.localSocket.layout=PatternLayout\n' +
|
|
199
|
+
'logger.appender.localSocket.layout.pattern=%d %.8t [%p] %c - %m %e%n\n' +
|
|
200
|
+
'logger.appender.localSocket.layout.dateFormat=dd/mm hh:nn:ss\n' +
|
|
201
|
+
'logger.appender.localSocket.filter1=PriorityFilter\n' +
|
|
202
|
+
'logger.appender.localSocket.filter1.match=PROGRESS\n' +
|
|
203
|
+
'logger.appender.localSocket.filter1.onMatch=DENY\n' +
|
|
204
|
+
'logger.appender.localSocket.filter1.onMismatch=NEUTRAL\n' +
|
|
205
|
+
'logger.additive.ngin=true\n' +
|
|
206
|
+
'logger.rootCategory=INFO,default,progress,jsdebugger,localSocket'
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
} finally {
|
|
210
|
+
fs.closeSync(this.tempLogConfigFile_.fd);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return this.tempLogConfigFile_.name;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Stops the monitoring of the Engine logs files.
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
stopLogsMonitoring_() {
|
|
221
|
+
if (this.tempLogConfigFile_) {
|
|
222
|
+
this.tempLogConfigFile_.removeCallback();
|
|
223
|
+
this.tempLogConfigFile_ = null;
|
|
224
|
+
}
|
|
225
|
+
if (this.logsServer_) {
|
|
226
|
+
this.logsServer_.close();
|
|
227
|
+
this.logsServer_ = null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Downloads the Engine and install it into the informed install path.
|
|
233
|
+
* @return {Promise}
|
|
234
|
+
*/
|
|
235
|
+
install() {
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
if (this.installed) {
|
|
238
|
+
resolve();
|
|
239
|
+
} else {
|
|
240
|
+
fs.mkdir(this.installPath, { recursive: true }, (err) => {
|
|
241
|
+
if (err && err.code !== 'EEXIST') {
|
|
242
|
+
reject(err);
|
|
243
|
+
} else {
|
|
244
|
+
let httpUrl = this.serverUrl;
|
|
245
|
+
if (!httpUrl.startsWith('http://') && !httpUrl.startsWith('https://')) {
|
|
246
|
+
httpUrl = 'http://' + httpUrl;
|
|
247
|
+
}
|
|
248
|
+
request.get(new URL(ENGINE_DOWNLOAD_URL, httpUrl).toString())
|
|
249
|
+
.pipe(fs.createWriteStream(this.enginePath))
|
|
250
|
+
.on('close', () => {
|
|
251
|
+
if (os.platform() != 'win32') {
|
|
252
|
+
fs.chmodSync(path.join(this.installPath, ENGINE_APP), '755');
|
|
253
|
+
}
|
|
254
|
+
resolve();
|
|
255
|
+
})
|
|
256
|
+
.on('error', (reqErr) => {
|
|
257
|
+
reject(reqErr);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Remove all files of the informed install path.
|
|
267
|
+
* @return {Promise}
|
|
268
|
+
*/
|
|
269
|
+
uninstall() {
|
|
270
|
+
return del(this.installPath.replace(/\\/g, '/'), {
|
|
271
|
+
force: true
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Starts the Engine and wait for it to be able to handle HTTP requests. It will wait the full
|
|
277
|
+
* load and synchronization of the Engine DBCache.
|
|
278
|
+
* @return {Promise}
|
|
279
|
+
*/
|
|
280
|
+
start() {
|
|
281
|
+
if (this.instanceId_) {
|
|
282
|
+
const dirPath = path.resolve(this.installPath, 'conf');
|
|
283
|
+
if (!fs.existsSync(dirPath)) {
|
|
284
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
285
|
+
}
|
|
286
|
+
fs.writeFileSync(path.resolve(dirPath, 'engine_id.dat'), this.instanceId_);
|
|
287
|
+
}
|
|
288
|
+
return this.install().then(() => {
|
|
289
|
+
if (this.useRandomHttpPort_) {
|
|
290
|
+
return getPort();
|
|
291
|
+
} else {
|
|
292
|
+
return Promise.resolve(this.httpPort);
|
|
293
|
+
}
|
|
294
|
+
}).then(port => {
|
|
295
|
+
this.httpPort = port;
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
console.info(`Starting Engine process at port ${this.httpPort}. Server: ${this.serverUrl}` +
|
|
298
|
+
`. Database: ${this.dbName}. Path: "${this.installPath}".`);
|
|
299
|
+
const args = [this.serverUrl, this.dbName,
|
|
300
|
+
'--disableAutoRestart',
|
|
301
|
+
'--disableGui',
|
|
302
|
+
'--httpPort=' + this.httpPort];
|
|
303
|
+
|
|
304
|
+
const jazArchives = this.jazSearchPath_ && fs.existsSync(this.jazSearchPath_)
|
|
305
|
+
? fs.readdirSync(this.jazSearchPath_)
|
|
306
|
+
.filter(name => name.endsWith('.jaz'))
|
|
307
|
+
.map(name => path.resolve(this.jazSearchPath_, name))
|
|
308
|
+
: [];
|
|
309
|
+
|
|
310
|
+
(jazArchives.concat(this.jazArchives_)).forEach(jazPath => {
|
|
311
|
+
args.push('--jazArchive=\'' + jazPath + '\'');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
this.ufsDirectories_.forEach(dirPath => {
|
|
315
|
+
args.push('--ufsDirectory=\'' + dirPath + '\'');
|
|
316
|
+
});
|
|
317
|
+
if (this.disableIdoSyncToDisk_) {
|
|
318
|
+
args.push('--disableIdoSyncToDisk');
|
|
319
|
+
}
|
|
320
|
+
if (this.v8_) {
|
|
321
|
+
args.push('-v8');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (this.redirectLogsToStdout_) {
|
|
325
|
+
const logConfig = this.startLogsMonitoring_();
|
|
326
|
+
args.push('--additionalLoggerConfig=\'' + logConfig + '\'');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.inShutdown_ = false;
|
|
330
|
+
this.process_ = child_process.spawn(path.join(this.installPath, ENGINE_APP),
|
|
331
|
+
args,
|
|
332
|
+
{
|
|
333
|
+
cwd: this.installPath,
|
|
334
|
+
windowsHide: true
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
this.process_.on('error', (err) => {
|
|
338
|
+
console.error('Error starting Engine process:', err);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
this.process_.stdout.on('data', (data) => {
|
|
342
|
+
console.info(`Engine stdout: ${data}`);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
this.process_.stderr.on('data', (data) => {
|
|
346
|
+
console.error(`Engine stderr: ${data}`);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
let intervalId = null;
|
|
350
|
+
|
|
351
|
+
this.process_.on('close', (code) => {
|
|
352
|
+
this.stopStatusMonitor_();
|
|
353
|
+
if (this.redirectLogsToStdout_) {
|
|
354
|
+
this.stopLogsMonitoring_();
|
|
355
|
+
}
|
|
356
|
+
if (intervalId) {
|
|
357
|
+
clearInterval(intervalId);
|
|
358
|
+
intervalId = null;
|
|
359
|
+
}
|
|
360
|
+
console.info(`Engine process exited with code ${code || 0}`);
|
|
361
|
+
this.process_ = null;
|
|
362
|
+
this.lastStatusCode_ = 0;
|
|
363
|
+
this.httpPort = 0;
|
|
364
|
+
if (code === EXIT_CODE_RESTART_REQUIRED) {
|
|
365
|
+
console.info('Engine restart was required to finish the upgrade');
|
|
366
|
+
sleep(UPDATE_INTERVAL);
|
|
367
|
+
this.start().then(resolve, reject);
|
|
368
|
+
} else {
|
|
369
|
+
if (!this.startedSuccessfully) {
|
|
370
|
+
const msg = `Engine startup failed with error code ${code || 0}`;
|
|
371
|
+
console.error(msg);
|
|
372
|
+
reject(new Error(msg));
|
|
373
|
+
}
|
|
374
|
+
if (this.onEngineClose_) {
|
|
375
|
+
this.onEngineClose_();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
this.startedSuccessfully = false;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
this.startStatusMonitor_();
|
|
382
|
+
intervalId = setInterval(() => {
|
|
383
|
+
if (this.process_ && this.monitorEnabled_) {
|
|
384
|
+
if (this.lastStatusCode_) {
|
|
385
|
+
if (this.lastStatusCode_ === 200) {
|
|
386
|
+
this.startedSuccessfully = true;
|
|
387
|
+
clearInterval(intervalId);
|
|
388
|
+
intervalId = null;
|
|
389
|
+
resolve();
|
|
390
|
+
} else if (this.lastStatusCode_ === 500) {
|
|
391
|
+
clearInterval(intervalId);
|
|
392
|
+
intervalId = null;
|
|
393
|
+
const msg = 'Unexpected Engine status code: ' + this.lastStatusCode_;
|
|
394
|
+
console.error(msg);
|
|
395
|
+
this.sendShutdownCommand_();
|
|
396
|
+
reject(new Error(msg));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
} else {
|
|
400
|
+
if (intervalId) {
|
|
401
|
+
clearInterval(intervalId);
|
|
402
|
+
intervalId = null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}, STATUS_MONITOR_INTERVAL);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Stops the Engine process.
|
|
412
|
+
* @return {Promise}
|
|
413
|
+
*/
|
|
414
|
+
stop() {
|
|
415
|
+
console.info('Engine stop requested');
|
|
416
|
+
return new Promise(resolve => {
|
|
417
|
+
if (this.running) {
|
|
418
|
+
this.onEngineClose_ = () => {
|
|
419
|
+
console.info('Engine stopped');
|
|
420
|
+
if (this.redirectLogsToStdout_) {
|
|
421
|
+
// Defers the resolve call to give time to log watching to be finished
|
|
422
|
+
setTimeout(resolve, 20);
|
|
423
|
+
} else {
|
|
424
|
+
resolve();
|
|
425
|
+
}
|
|
426
|
+
this.inShutdown_ = false;
|
|
427
|
+
};
|
|
428
|
+
this.sendShutdownCommand_();
|
|
429
|
+
} else {
|
|
430
|
+
resolve();
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Request a graceful shutdown to Engine. If request fails, send a force kill command.
|
|
437
|
+
* @private
|
|
438
|
+
*/
|
|
439
|
+
sendShutdownCommand_() {
|
|
440
|
+
if (this.process_) {
|
|
441
|
+
let forceKill = true;
|
|
442
|
+
this.inShutdown_ = true;
|
|
443
|
+
if (os.platform() == 'win32') {
|
|
444
|
+
if (fs.existsSync(this.ngintPath_)) {
|
|
445
|
+
try {
|
|
446
|
+
console.info('Shutdown command sent to Engine process');
|
|
447
|
+
child_process.execFileSync(
|
|
448
|
+
this.ngintPath_,
|
|
449
|
+
['shutdown', this.process_.pid, ENGINE_SHUTDOWN_TIMEOUT],
|
|
450
|
+
{
|
|
451
|
+
cwd: this.installPath,
|
|
452
|
+
windowsHide: true
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
forceKill = false;
|
|
456
|
+
} catch (e) {
|
|
457
|
+
console.error('Error stopping Engine', e);
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
console.info('ngint was not found');
|
|
461
|
+
}
|
|
462
|
+
} else {
|
|
463
|
+
process.kill(this.process_.pid, 'SIGTERM');
|
|
464
|
+
}
|
|
465
|
+
if (forceKill) {
|
|
466
|
+
console.info('Kill signal sent to Engine process');
|
|
467
|
+
this.process_.kill();
|
|
468
|
+
this.process_ = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Creates a new Engine instance using the ".env" config file on the repository root.
|
|
475
|
+
*
|
|
476
|
+
* The Engine will be installed on `<root-dir>/.engine/<database-name>` directory. If was not
|
|
477
|
+
* informed the option `jazSearchPath` it will load all JAZ files inside the
|
|
478
|
+
* `<root-dir>/.build` directory (or "build" directory if ".build" does not exists). The
|
|
479
|
+
* option `disableIdoSyncToDisk` will be enabled by default.
|
|
480
|
+
*
|
|
481
|
+
* See the site https://nginstack.com for the more details about the adopted repository
|
|
482
|
+
* conventions.
|
|
483
|
+
* @param {EngineOptions=} options Advanced Engine options.
|
|
484
|
+
*/
|
|
485
|
+
static newUsingWorkspaceConfig(options) {
|
|
486
|
+
options = options || {};
|
|
487
|
+
const workspacePath = getWorkspacePath();
|
|
488
|
+
dotenv.config({
|
|
489
|
+
path: path.resolve(workspacePath, '.env')
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
if (!('jazSearchPath' in options)) {
|
|
493
|
+
let jazSearchPath = path.resolve(workspacePath, '.build');
|
|
494
|
+
if (!fs.existsSync(jazSearchPath)) {
|
|
495
|
+
// Compatibility with previous build directory name
|
|
496
|
+
jazSearchPath = path.resolve(workspacePath, 'build');
|
|
497
|
+
}
|
|
498
|
+
if (fs.existsSync(jazSearchPath)) {
|
|
499
|
+
options.jazSearchPath = jazSearchPath;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (!('disableIdoSyncToDisk' in options)) {
|
|
503
|
+
options.disableIdoSyncToDisk = true;
|
|
504
|
+
}
|
|
505
|
+
options.v8 = options.v8 || process.argv.includes('-v8');
|
|
506
|
+
options.instanceId = Engine.formatWorkspaceInstanceId();
|
|
507
|
+
|
|
508
|
+
return new Engine(
|
|
509
|
+
path.resolve(workspacePath, '.engine', process.env.ENGINE_DATABASE),
|
|
510
|
+
process.env.ENGINE_URL,
|
|
511
|
+
process.env.ENGINE_DATABASE,
|
|
512
|
+
options
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Creates an stable Engine instance id using the hostname and the workspace path.
|
|
518
|
+
*/
|
|
519
|
+
static formatWorkspaceInstanceId() {
|
|
520
|
+
const hash = crypto.createHash('sha256');
|
|
521
|
+
hash.update(os.hostname());
|
|
522
|
+
hash.update(getWorkspacePath());
|
|
523
|
+
return ('_' + hash.digest('hex').toUpperCase()).substring(0, INSTANCE_ID_LENGTH);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function getWorkspacePath() {
|
|
528
|
+
let workspacePath = findUp.sync('.git', { type: 'directory' });
|
|
529
|
+
if (!workspacePath) {
|
|
530
|
+
workspacePath = findUp.sync('package.json');
|
|
531
|
+
}
|
|
532
|
+
return path.dirname(workspacePath);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
module.exports = Engine;
|