'use strict' /** * Copyright (c) 2017~2024, OBCon Inc. * All rights reserved. */ /** * @file * @copyright 2017~2024, OBCon Inc. * @author gye hyun james kim [pnuskgh@gmail.com] */ const fs = require('fs'); const path = require('path'); const arg = require('arg'); const moment = require('moment'); const cluster = require('cluster'); const appl = (global.appl = global.appl || {}); global.type = (appl && appl.type) ? appl.type : (global.type || ''); class Application { constructor(options) { // logger가 아직 초기화 전이라도 안전하게 콘솔로 시작 global.logger = console; Object.assign(appl, { isECMAScriptModules: typeof (require.main) == 'undefined', datetime: moment().format('YYYY-MM-DD HH:mm:ss'), // Application 시작 일시 ...options }); // ✅ PATCH: name/type 기본값 보정 (PM2에서 "*** undefined Application ..." 방지) if (!appl.name) appl.name = (process.env.APP_NAME || 'OBCon SCADA'); if (!appl.type && global.type) appl.type = global.type; if (process.platform == 'darwin') { // 오류 발생 :: MaxListenersExceededWarning: Possible EventEmitter memory leak detected. process.setMaxListeners(50); process.on('unhandledRejection', error => { console.error('Unhandled promise rejection:', error); }); } this._programName = 'bin/scada.js'; global.funcShutdowns = []; // 종료시 실행할 함수 { name: string, func: async function } this._intervalDeviceIndex = null; } _using() { console.log(`Using : node ${this._programName} --conf CONFFILE`); console.log(' CONFFILE : 설정 파일'); console.log(''); } _copyFile(source, target) { const content = fs.readFileSync(path.join(appl.root, source)); fs.writeFileSync(path.join(appl.root, target), content); } /** * logger.js가 여러 형태로 export될 수 있어서 안전하게 초기화: * 1) { createLogger() } 형태 * 2) module.exports = class Logger 형태 * 3) module.exports = function(...) 형태 * 실패 시 console로 fallback */ _initGlobalLogger() { try { const loggerMod = utils.getModule('include', 'logger.js'); // case 1) createLogger 제공 if (loggerMod && typeof loggerMod.createLogger === 'function') { global.logger = loggerMod.createLogger(); return; } // case 2) 클래스/함수 자체 export (module.exports = class Logger) if (typeof loggerMod === 'function') { try { // class로 new 가능한 경우 global.logger = new loggerMod(); return; } catch (e) { // new가 안되면 함수 호출 시도 try { global.logger = loggerMod(); if (!global.logger) global.logger = console; return; } catch (e2) { global.logger = console; return; } } } // case 3) 객체인데 createLogger가 없으면 console fallback global.logger = console; } catch (e) { global.logger = console; } } async initApplication() { try { //--- 1. 환경 변수 등 설정 process.env.NODE_ENV = 'production'; // ✅ PATCH: PM2 감지 강화 appl.isPM2 = !!( process.env.PM2_HOME || process.env.pm_id || process.env.PM2 || process.env.PM2_UUID || process.env.NODE_APP_INSTANCE || process.env.INSTANCE_ID ); appl.platform = process.platform; // win32, linux, darwin process.chdir(path.join(__dirname, '..')); appl.root = process.cwd().replace(/\\/g, '/'); if (['scada_batch'].includes(appl.type)) { appl.worker = { id: 1, type: 'master', seq: 1 }; } else { // https://pm2.keymetrics.io/docs/usage/quick-start/ if (appl.isPM2) { if (!appl.worker) appl.worker = { id: ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1) }; // INSTANCE_ID / NODE_APP_INSTANCE / pm_id -> 0,1,2... 형태이므로 +1 해서 1-based로 사용 const rawInstance = (process.env.INSTANCE_ID ?? process.env.NODE_APP_INSTANCE ?? process.env.pm_id ?? '0'); const parsed = parseInt(rawInstance, 10); const instanceId = (Number.isFinite(parsed) ? parsed : 0) + 1; appl.worker = { id: instanceId, type: 'pm2', seq: instanceId }; } else { appl.worker = { id: (cluster.isMaster) ? 1 : -1, type: 'master', seq: 1, }; } } this._programName = process.argv[1].replace(/\\/g, '/').replace(`${appl.root}/`, ''); //--- 2. 설정 정보 로딩 if (process.argv.length <= 2) { this._using(); process.exit(1); } const args = arg( { '--conf': String, '-c': '--conf' }, { argv: process.argv.slice(2) } ); const configFile = args['--conf']; if (!configFile) { console.error('Error: 설정 파일을 지정 하세요.'); this._using(); process.exit(2); } if (!fs.existsSync(path.join(appl.root, configFile))) { console.error(`Error: 설정 파일이 존재하지 않습니다 - ${configFile}`); this._using(); process.exit(3); } global.config = require(path.join(appl.root, configFile)); global.conf = global.config; // ✅ PATCH: config에 서비스 이름/타입이 있으면 appl 기본값 보정 try { if (!appl.name) appl.name = (config?.service?.name || config?.service?.title || 'OBCon SCADA'); if (!appl.type) appl.type = (config?.service?.type || appl.type); } catch (e) { } if (config && typeof config.funcAfter === 'function') { config.funcAfter(); } this._copyFile('version.json', 'version_local.json'); //--- 3. Global Utility 함수 로드 const UtilityPrev = require(path.join(appl.root, 'include', 'UtilityPrev.js')); const utilityPrev = new UtilityPrev(); global.utils = utilityPrev.getModule('include', 'Utility.js'); // [hotfix] utilityPrev.getModule() 결과가 wrapper일 수 있어 Utility 인스턴스로 보정 try { const utilMod = require(path.join(__dirname, 'Utility.js')); if (!global.utils || (typeof global.utils.existField !== 'function') || (typeof global.utils.isNetworkError !== 'function') || (typeof global.utils.getModule !== 'function')) { global.utils = (typeof utilMod === 'function') ? new utilMod() : utilMod; } global.utils.obj = global.utils.obj || {}; global.utils.obj.loggerError = global.utils.obj.loggerError || function (e) { try { logger.error(e && e.stack ? e.stack : e); } catch (_) { } }; } catch (e) { global.utils = global.utils || {}; global.utils.obj = global.utils.obj || {}; global.utils.obj.loggerError = global.utils.obj.loggerError || function (err) { try { logger.error(err && err.stack ? err.stack : err); } catch (_) { } }; } //--- 4. Cluster 설정 if (appl.isPM2 == false) { switch (appl.type) { case 'obcon_scada': case 'obcon_proxy': case 'obcon_cms': case 'obcon_iot': global.clusters = utils.getModule('include', 'cluster.js'); if (global.clusters && typeof global.clusters.forks === 'function') { global.clusters.forks(); } else { try { console.error('[WARN] cluster.js has no forks(), continue without clustering'); } catch (e) { } } if (cluster.isMaster == false) { // cluster에서 worker에게 메시지를 보내는 시간보다 5초간 대기 한다. if (utils && typeof utils.wait !== "function") { utils.wait = async (condFn, timeoutMs = 5000, intervalMs = 50) => { const start = Date.now(); while (true) { let ok = false; try { ok = !!condFn(); } catch (e) { ok = false; } if (ok) return true; if (Date.now() - start > timeoutMs) throw new Error(`utils.wait timeout after ${timeoutMs}ms`); await new Promise(r => setTimeout(r, intervalMs)); } }; } await utils.wait(() => ((appl.worker && (appl.worker && appl.worker.id != -1))), 5 * 1000); } break; } } //--- 5. Global Logger 설정 : logger.info(), logger.warn(), logger.error() this._initGlobalLogger(); const dbg = (msg) => { try { if (global.logger && typeof global.logger.debug === 'function') global.logger.debug(msg); else console.log(msg); } catch (e) { } }; dbg(`1. 환경 변수 등 설정`); dbg(`2. 설정 정보 로딩`); dbg(`3. Global Utility 함수 로드`); dbg(`4. Cluster 설정`); dbg(`5. Global Logger 설정`); //--- 6. Global Security 설정 global.security = utils.getModule('include', 'Security.js'); dbg(`6. Global Security 설정`); //--- 7. Global Cache 설정 global.caches = utils.getModule('include', 'caches.js'); // Cluster에서 사용 dbg(`7. Global Cache 설정`); //--- Global WatchDogTimer 설정 global.watchDogTimer = utils.getModule('include', 'watch_dog_timer.js'); dbg(`Global WatchDogTimer 설정`); //--- 8. Global Emails 설정 global.emails = utils.getModule('include', 'Emails.js'); dbg(`8. Global Emails 설정`); //--- 9. Global Telegram 설정 global.telegram = (config.telegram && config.telegram.use) ? await utils.getModule('include', 'Telegram.js') : null; dbg(`9. Global Telegram 설정`); //--- 10. Global SMS 설정 global.sms = await utils.getModule('include', 'sms.js'); dbg(`10. Global SMS 설정- ${this._programName}`); //--- 11. Global Database 설정 global.databases = utils.getModule('include', 'databases.js'); global.db = { dataTypes: databases.dataTypes, db_scada: databases.scada, db_data: databases.data }; dbg(`11. Global Database 설정`); //--- 보안: 비밀번호는 마스킹해서 로그에 남김. HF Logs 가 공개될 수 있음. try { const _sc = global.conf?.databases?.scada || {}; const _safe = { ...(_sc), password: _sc.password ? `` : null }; console.log("[DB-CONF scada]", _safe); } catch (e) { console.log("[DB-CONF scada] "); } console.log("[CONF keys]", global.conf ? Object.keys(global.conf) : null); console.log("[CONF.databases keys]", global.conf?.databases ? Object.keys(global.conf.databases) : null); console.log("[CONF file hint]", global.conf?.__filename || global.conf?.configFile || null); //--- 12. Fields 정의 global.fields = utils.getModule('include', 'fields.js'); dbg(`12. Global Fields 설정`); //--- 13. Module별로 Model과 View 정의 require(path.join(appl.root, 'modules', 'modules.js')); dbg(`13. Module별로 Model과 View 정의`); //--- 14. Settings 정의 global.settingSync = utils.getModule('include', 'settings.js'); dbg(`14. Global Settings 정의`); //--- 15. Global Interfaces 설정 switch (appl.type) { case 'obcon_scada': global.internalProxyClient = utils.getModule('modules', 'proxies', 'proxy_client.js'); dbg(`15. Global Proxy Client 설정`); // intentional fallthrough case 'obcon_scada_device': global.interfaces = utils.getModule('interfaces', 'interfaces.js'); dbg(`15. Global Interfaces 설정`); break; } //--- MQTT 설정 require(path.join(appl.root, 'modules', 'mqtt_app.js')); dbg(`Global MQTT Client (mqtt_client) 설정`); // ---- HOTFIX: utils.wait polyfill if (typeof utils.wait !== 'function') { utils.wait = async (condFn, timeoutMs = 5000, intervalMs = 50) => { const start = Date.now(); while (true) { try { if (typeof condFn === 'function' ? !!condFn() : !!condFn) return true; } catch (e) { } if ((Date.now() - start) >= timeoutMs) { throw new Error(`utils.wait timeout after ${timeoutMs}ms`); } await new Promise(r => setTimeout(r, intervalMs)); } }; } //--- 16. Redis 설정 if (config.redis && config.redis.isUse) { global.redis = utils.getModule('include', 'Redis.js'); await utils.wait(() => global.redis.ready, 5 * 60 * 1000); setTimeout(async function () { try { await modules.devices.view.getDevicesInfo(true); } catch (e) { } }, 1000); } dbg(`16. Redis 설정`); //--- IECP 설정 global.iecp = utils.getModule('interfaces', 'IECP.js'); iecp.startGarbageCollection(); dbg(`IECP 설정`); } catch (e) { if (typeof (logger) == 'undefined') { console.error(e.message); console.error(e.stack); this._using(); } else { try { logger.error(e.message); } catch (ex) { } console.error(e.stack); } process.exit(3); } } async run() { global.app_http = null; global.app_io = null; global.app_tcp = null; global.app_udp = null; global.app_modbus = null; global.app_proxy = null; const workType = () => { if (appl.isPM2) { if (!appl.worker) appl.worker = { id: ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1) }; const wid = appl.worker?.id ?? (parseInt(process.env.NODE_APP_INSTANCE ?? process.env.INSTANCE_ID ?? process.env.pm_id ?? '0', 10) + 1); return `PM2 worker ${wid}`; } else { return (cluster.isMaster) ? 'Master' : `Worker ${(appl.worker && appl.worker.id)}`; } } if (appl.isPM2) { if (!appl.worker) appl.worker = { id: ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1) }; process.on('SIGINT', function () { watchDogTimer.stop(); // WatchDogTimer를 종료 한다. if (global.app_io != null) { try { global.app_io.close(function () { }.bind(this)); } catch (e) { utils.obj.loggerError(e); } } if ((global.app_tcp != null) && (global.app_tcp.server != null)) { try { global.app_tcp.server.close(function () { }.bind(this)); } catch (e) { utils.obj.loggerError(e); } } if ((global.app_udp != null) && (global.app_udp._serverSocket != null)) { try { global.app_udp._serverSocket.close(function () { }.bind(this)); } catch (e) { utils.obj.loggerError(e); } } if ((global.app_modbus != null) && (global.app_modbus.server != null)) { try { global.app_modbus.server.close(function () { }.bind(this)); } catch (e) { utils.obj.loggerError(e); } } if ((typeof (global.app_proxy) != 'undefined') && (global.app_proxy != null)) { try { global.app_proxy.close(); } catch (e) { utils.obj.loggerError(e); } } try { logger.debug('Bye bye'); } catch (e) { } try { logger.info('------------------------------------------------------------'); logger.info('SIGINT ------------------------------------------------------------'); logger.info('------------------------------------------------------------'); } catch (e) { } process.exit(0); }.bind(this)); } try { logger.info(`*** ${appl.name || 'OBCon SCADA'} Application - ${workType()} ***`); switch (appl.type) { case 'obcon_scada': this._scada_app(); break; case 'scada_schedule': this._schedule_app(); break; case 'obcon_proxy': this._proxy_app(); break; case 'obcon_scada_device': await this._device_app(); break; case 'obcon_iot': this._http_app(); break; case 'scada_batch': this._batch_app(); break; case 'obcon_cms': this._http_app(); break; // Deprecated case 'cms_batch': this._batch_app(); break; // Deprecated default: logger.error('정의되지 않은 application type 입니다.'); break; } } catch (e) { console.error('error', e); utils.obj.loggerError(e); watchDogTimer.stop(); if ((appl.isPM2 == false) && (cluster.isMaster)) { for (let idx in cluster.workers) { cluster.workers[idx].kill(); } } process.exit(4); } } // PM2에게 ready 신호를 보내 application이 실행 준비가 되었음을 알린다. _pm2_ready() { if (appl.isPM2) { if (!appl.worker) appl.worker = { id: ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1) }; setTimeout(function () { process.send('ready'); }, 500); } } async _scada_app() { try { global.idxDevices = {}; // Device별 Interface Version 관리 if (appl.isPM2) { if (!appl.worker) appl.worker = { id: ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1) }; // ✅ PM2에서는 cluster.worker가 없을 수 있으니 appl.worker.id 사용 const pm2WorkerId = ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1); this._mqtt_client_init('pm2', pm2WorkerId); setTimeout(async function () { await this._mqtt_client_makeIndex('pm2'); let appTypes = []; if (0 < config.cluster.worker_count_http) appTypes.push('http'); if (0 < config.cluster.worker_count_tcp) appTypes.push('tcp'); if (0 < config.cluster.worker_count_udp) appTypes.push('udp'); if (0 < config.cluster.worker_count_modbus) appTypes.push('modbus'); if (0 < config.cluster.worker_count_proxy) appTypes.push('proxy'); for (let type of appTypes) { if (type == 'modbus') { require(path.resolve(utils.getCustomPath('modules', 'ModbusApp.js'))); } else if (type == 'proxy') { global.app_proxy = utils.getModule('modules', 'proxies', 'proxy_app.js'); global.app_proxy.run(); } else { require(path.resolve(utils.getCustomPath('modules', `${type}_app.js`))); } logger.info(`start ${type} service`); } mqtt_client.publish( 'publish_command', '{ "cmd": "stop checkAlive", "clientId": "' + mqtt_client.clientId + '" }' ); setTimeout(function () { if (process.env.OBCON_DISABLE_CHECKALIVE === '1') { console.log('[HF] checkAlive disabled (OBCON_DISABLE_CHECKALIVE=1) - snapshot demo mode'); } else { watchDogTimer.set_wdt('checkAlive', 10, modules.devices.view.checkAlive.bind(modules.devices.view), moment()); } this._pm2_ready(); }.bind(this), 1000); }.bind(this), 1000); } else { // ✅ PATCH: cluster.worker 안전 접근 if (cluster.isWorker && cluster.worker && cluster.worker.id != null) { let type = 'proxy'; if (cluster.worker.id <= config.cluster.worker_count_http) type = 'http'; else if (cluster.worker.id <= config.cluster.worker_count_http + config.cluster.worker_count_tcp) type = 'tcp'; else if (cluster.worker.id <= config.cluster.worker_count_http + config.cluster.worker_count_tcp + config.cluster.worker_count_udp) type = 'udp'; else if (cluster.worker.id <= config.cluster.worker_count_http + config.cluster.worker_count_tcp + config.cluster.worker_count_udp + config.cluster.worker_count_modbus) type = 'modbus'; this._mqtt_client_init(type, cluster.worker.id); setTimeout(async function () { await this._mqtt_client_makeIndex(type); if (type == 'modbus') { require(path.resolve(utils.getCustomPath('modules', 'ModbusApp.js'))); } else if (type == 'proxy') { global.app_proxy = utils.getModule('modules', 'proxies', 'proxy_app.js'); global.app_proxy.run(); } else { require(path.resolve(utils.getCustomPath('modules', `${type}_app.js`))); } logger.info(`start ${type} service`); }.bind(this), 1000); } else { if (process.env.OBCON_DISABLE_CHECKALIVE === '1') { console.log('[HF] checkAlive disabled (OBCON_DISABLE_CHECKALIVE=1) - snapshot demo mode'); } else { watchDogTimer.set_wdt('checkAlive', 10, modules.devices.view.checkAlive.bind(modules.devices.view), moment()); } logger.debug(`master start checkAlive`); this._mqtt_client_init('master', 'master'); setTimeout(async function () { await this._mqtt_client_makeIndex('master'); }.bind(this), 1000); } } } catch (ex) { console.error(ex); } } async _proxy_app() { if (appl.isPM2) { if (!appl.worker) appl.worker = { id: ((parseInt(process.env.INSTANCE_ID || process.env.NODE_APP_INSTANCE || process.env.pm_id || "0", 10) || 0) + 1) }; setTimeout(async function () { global.app_proxy = utils.getModule('modules', 'proxies', 'proxy_app.js'); await global.app_proxy.run(appl.type); logger.info(`start ${appl.type} service`); }.bind(this), 1000); } else { if (cluster.isWorker) { setTimeout(async function () { global.app_proxy = utils.getModule('modules', 'proxies', 'proxy_app.js'); await global.app_proxy.run(appl.type); logger.info(`start proxy service`); }.bind(this), 1000); } } } _getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min)) + min; // 최댓값은 제외, 최솟값은 포함 } /** * http_app.js 로딩 실패 시에도 로그 남기고, * PM2 ready는 finally에서 항상 실행 */ _http_app() { if ((appl.isPM2) || (cluster.isWorker)) { setTimeout(function () { try { const p = path.resolve(utils.getCustomPath('modules', 'http_app.js')); try { logger.info(`[HTTP] require: ${p}`); } catch (e) { console.log(`[HTTP] require: ${p}`); } require(p); try { logger.info(`woker start http service : ${config.http.host}:${config.http.port}`); } catch (e) { } } catch (e) { try { logger.error(`[HTTP] http_app.js load failed: ${e && (e.stack || e)}`); } catch (ex) { console.error(`[HTTP] http_app.js load failed: ${e && (e.stack || e)}`); } } finally { this._pm2_ready(); } }.bind(this), 1000); } } _batch_app() { setTimeout(function () { let jobBatch = null; switch (config.batch.path.length) { case 1: jobBatch = utils.getModule(config.batch.path[0]); break; case 2: jobBatch = utils.getModule(config.batch.path[0], config.batch.path[1]); break; case 3: jobBatch = utils.getModule(config.batch.path[0], config.batch.path[1], config.batch.path[2]); break; case 4: jobBatch = utils.getModule(config.batch.path[0], config.batch.path[1], config.batch.path[2], config.batch.path[3]); break; case 5: jobBatch = utils.getModule(config.batch.path[0], config.batch.path[1], config.batch.path[2], config.batch.path[3], config.batch.path[4]); break; default: logger.error(`실행할 batch 파일을 올바르게 지정 하세요 - ${config.batch.path.join('/')}`); process.exit(5); } logger.info(`Start batch - ${config.batch.path.join('/')}`); this._pm2_ready(); jobBatch.batchJob(config.batch.options, function (code, message) { logger.info(`Batch job complete : code - ${code}, message - [${message}]`); setTimeout(function () { process.exit(0); }.bind(this), config.batch.waitTime * 1000); }); }.bind(this), 1000); } async _device_app() { if (config.emulator.isUse == false) { console.error('장비 애뮬레이터를 지원하지 않습니다.'); return; } console.log('30초 sleep . . . . . .'); await utils.sleep(30 * 1000); global.idxDevices = {}; await modules.devices.view.makeDeviceIndexForInterface(); console.log('30초 sleep . . . . . . again'); await utils.sleep(30 * 1000); await modules.devices.view.makeDeviceIndexForInterface(); setTimeout(function () { let query = { where: { deleted: false } }; modules.sites.model.table.findAll(query) .then(function (sites) { let idxSites = modules.sites.view.makeIdx(sites, 'id'); query = { where: { statusUse: 'Active', deleted: false } }; modules.devices.model.table.findAll(query) .then(function (devices) { let idxDevices = modules.devices.view.makeIdx(devices, 'deviceKey'); for (let idx = 0; idx < config.emulator.devices.length; idx++) { let client = config.emulator.devices[idx]; let site = idxSites[`idx_${client.site}`]; let device = idxDevices[`idx_${client.deviceKey}`]; logger.info(`${client.clientType} Client : ${site.siteKey}_${device.type}_${device.deviceKey}`); let emulator = null; if (client.clientType == '') { emulator = utils.getModule('modules', 'clients', `ModbusClient_${device.type}.js`); } else { emulator = utils.getModule('modules', 'clients', `${client.clientType}_client_${device.type}.js`); } emulator.init(device, site); setTimeout(function () { if (emulator._site != null) { switch (client.clientType) { case 'modbus': emulator.startConnect(config.modbus.server.host, config.modbus.server.port); break; case 'udp': emulator.startConnect(config.udp.server.host, config.udp.server.port); break; default: emulator.startConnect(config.tcp.server.host, config.tcp.server.port); break; } setTimeout(function () { emulator.startAction(); }, 500); } }.bind(this), 500); } this._pm2_ready(); }.bind(this)).catch(utils.obj.loggerError); }.bind(this)).catch(utils.obj.loggerError); }.bind(this), 1000); } _schedule_app() { global.jobs = { jobFiles: [] }; logger.info(`registerAll()`); modules.jobManagers.view.registerAll(); setTimeout(function () { logger.info(`startAll()`); modules.jobManagers.view.startAll(); }.bind(this), 2000); this._pm2_ready(); } _mqtt_client_init(type, clientId) { mqtt_client.type = type; mqtt_client.clientId = config.mqtt.clientIdPrefix + clientId; mqtt_client.init(); } async _mqtt_client_makeIndex(type) { mqtt_client.subscribe('publish_command'); mqtt_client.subscribe('iecp'); // IECP 통신을 위한 topic 등록 if (this._intervalDeviceIndex == null) { this._intervalDeviceIndex = setInterval(async function () { try { await modules.devices.view.makeDeviceIndexForInterface(); } catch (ex) { } }.bind(this), 60 * 60 * 1000); } } } module.exports = Application;