@nginstack/engine-node 78.0.1 → 78.0.8

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/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;