/** * * yunkong2 node-red Adapter * * (c) 2014 bluefox * * Apache 2.0 License * */ /* jshint -W097 */// jshint strict:false /*jslint node: true */ 'use strict'; const utils = require(__dirname + '/lib/utils'); // Get common adapter utils const adapter = utils.Adapter({ name: 'node-red', systemConfig: true, // get the system configuration as systemConfig parameter of adapter unload: unloadRed }); const fs = require('fs'); const path = require('path'); const spawn = require('child_process').spawn; const Notify = require('fs.notify'); const attempts = {}; const additional = []; let secret; let userdataDir = __dirname + '/userdata/'; adapter.on('message', function (obj) { if (obj) processMessage(obj); processMessages(); }); adapter.on('ready', function () { installLibraries(main); }); function installNpm(npmLib, callback) { const path = __dirname; if (typeof npmLib === 'function') { callback = npmLib; npmLib = undefined; } const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '" --save'; adapter.log.info(cmd + ' (System call)'); // Install node modules as system call // System call used for update of js-controller itself, // because during installation npm packet will be deleted too, but some files must be loaded even during the install process. const exec = require('child_process').exec; const child = exec(cmd); child.stdout.on('data', function(buf) { adapter.log.info(buf.toString('utf8')); }); child.stderr.on('data', function(buf) { adapter.log.error(buf.toString('utf8')); }); child.on('exit', function (code, signal) { if (code) { adapter.log.error('Cannot install ' + npmLib + ': ' + code); } // command succeeded if (callback) callback(npmLib); }); } function installLibraries(callback) { let allInstalled = true; if (typeof adapter.common.npmLibs === 'string') { adapter.common.npmLibs = adapter.common.npmLibs.split(/[,;\s]+/); } if (adapter.common && adapter.common.npmLibs) { for (let lib = 0; lib < adapter.common.npmLibs.length; lib++) { if (adapter.common.npmLibs[lib] && adapter.common.npmLibs[lib].trim()) { adapter.common.npmLibs[lib] = adapter.common.npmLibs[lib].trim(); if (!fs.existsSync(__dirname + '/node_modules/' + adapter.common.npmLibs[lib] + '/package.json')) { if (!attempts[adapter.common.npmLibs[lib]]) { attempts[adapter.common.npmLibs[lib]] = 1; } else { attempts[adapter.common.npmLibs[lib]]++; } if (attempts[adapter.common.npmLibs[lib]] > 3) { adapter.log.error('Cannot install npm packet: ' + adapter.common.npmLibs[lib]); continue; } installNpm(adapter.common.npmLibs[lib], function () { installLibraries(callback); }); allInstalled = false; break; } else { if (additional.indexOf(adapter.common.npmLibs[lib]) === -1) additional.push(adapter.common.npmLibs[lib]); } } } } if (allInstalled) callback(); } // is called if a subscribed state changes //adapter.on('stateChange', function (id, state) { //}); function unloadRed (callback) { // Stop node-red stopping = true; if (redProcess) { adapter.log.info("kill node-red task"); redProcess.kill(); redProcess = null; } if (notificationsCreds) notificationsCreds.close(); if (notificationsFlows) notificationsFlows.close(); if (callback) callback(); } function processMessage(obj) { if (!obj || !obj.command) return; switch (obj.command) { case 'update': writeStateList(error => { if (typeof obj.callback === 'function') adapter.sendTo(obj.from, obj.command, error, obj.callback); }); break; case 'stopInstance': unloadRed(); break; } } function processMessages() { adapter.getMessage((err, obj) => { if (obj) { processMessage(obj.command, obj.message); processMessages(); } }); } function getNodeRedPath() { let nodeRed = __dirname + '/node_modules/node-red'; if (!fs.existsSync(nodeRed)) { nodeRed = path.normalize(__dirname + '/../node-red'); if (!fs.existsSync(nodeRed)) { nodeRed = path.normalize(__dirname + '/../node_modules/node-red'); if (!fs.existsSync(nodeRed)) { adapter && adapter.log && adapter.log.error('Cannot find node-red packet!'); throw new Error('Cannot find node-red packet!'); } } } return nodeRed; } let redProcess; let stopping; let notificationsFlows; let notificationsCreds; let saveTimer; const nodePath = getNodeRedPath(); function startNodeRed() { adapter.config.maxMemory = parseInt(adapter.config.maxMemory, 10) || 128; const args = ['--max-old-space-size=' + adapter.config.maxMemory, nodePath + '/red.js', '-v', '--settings', userdataDir + 'settings.js']; adapter.log.info('Starting node-red: ' + args.join(' ')); redProcess = spawn('node', args); redProcess.on('error', function (err) { adapter.log.error('catched exception from node-red:' + JSON.stringify(err)); }); redProcess.stdout.on('data', function (data) { if (!data) return; data = data.toString(); if (data[data.length - 2] === '\r' && data[data.length - 1] === '\n') data = data.substring(0, data.length - 2); if (data[data.length - 2] === '\n' && data[data.length - 1] === '\r') data = data.substring(0, data.length - 2); if (data[data.length - 1] === '\r') data = data.substring(0, data.length - 1); if (data.indexOf('[err') !== -1) { adapter.log.error(data); } else if (data.indexOf('[warn]') !== -1) { adapter.log.warn(data); } else { adapter.log.debug(data); } }); redProcess.stderr.on('data', function (data) { if (!data) return; if (data[0]) { let text = ''; for (let i = 0; i < data.length; i++) { text += String.fromCharCode(data[i]); } data = text; } if (data.indexOf && data.indexOf('[warn]') === -1) { adapter.log.warn(data); } else { adapter.log.error(JSON.stringify(data)); } }); redProcess.on('exit', function (exitCode) { adapter.log.info('node-red exited with ' + exitCode); redProcess = null; if (!stopping) { setTimeout(startNodeRed, 5000); } }); } function setOption(line, option, value) { const toFind = "'%%" + option + "%%'"; const pos = line.indexOf(toFind); if (pos !== -1) { return line.substring(0, pos) + ((value !== undefined) ? value : (adapter.config[option] === null || adapter.config[option] === undefined) ? '' : adapter.config[option]) + line.substring(pos + toFind.length); } return line; } function writeSettings() { const config = JSON.stringify(adapter.systemConfig); const text = fs.readFileSync(__dirname + '/settings.js').toString(); const lines = text.split('\n'); let npms = '\r\n'; const dir = __dirname.replace(/\\/g, '/') + '/node_modules/'; const nodesDir = '"' + __dirname.replace(/\\/g, '/') + '/nodes/"'; const bind = '"' + (adapter.config.bind || '0.0.0.0') + '"'; const auth = adapter.config.user && adapter.config.pass ? JSON.stringify({user: adapter.config.user, pass: adapter.config.pass}) : '""'; const pass = '"' + adapter.config.pass + '"'; const ui_auth = adapter.config.ui_user && adapter.config.ui_pass ? JSON.stringify({user: adapter.config.ui_user, pass: adapter.config.ui_pass}) : '""'; const ui_pass = '"' + adapter.config.ui_pass + '"'; const node_auth = adapter.config.ui_user && adapter.config.ui_pass ? JSON.stringify({user: adapter.config.ui_user, pass: adapter.config.ui_pass}) : '""'; const node_pass = '"' + adapter.config.ui_pass + '"'; for (let a = 0; a < additional.length; a++) { if (additional[a].match(/^node-red-/)) continue; npms += ' "' + additional[a] + '": require("' + dir + additional[a] + '")'; if (a !== additional.length - 1) { npms += ', \r\n'; } } // update from 1.0.1 (new convert-option) if (adapter.config.valueConvert === null || adapter.config.valueConvert === undefined || adapter.config.valueConvert === '' || adapter.config.valueConvert === 'true' || adapter.config.valueConvert === '1' || adapter.config.valueConvert === 1) { adapter.config.valueConvert = true; } if (adapter.config.valueConvert === 0 || adapter.config.valueConvert === '0' || adapter.config.valueConvert === 'false') { adapter.config.valueConvert = false; } for (let i = 0; i < lines.length; i++) { lines[i] = setOption(lines[i], 'port'); lines[i] = setOption(lines[i], 'auth', auth); lines[i] = setOption(lines[i], 'pass', pass); lines[i] = setOption(lines[i], 'bind', bind); lines[i] = setOption(lines[i], 'port'); lines[i] = setOption(lines[i], 'ui_auth', ui_auth); lines[i] = setOption(lines[i], 'ui_pass', ui_pass); lines[i] = setOption(lines[i], 'instance', adapter.instance); lines[i] = setOption(lines[i], 'config', config); lines[i] = setOption(lines[i], 'functionGlobalContext', npms); lines[i] = setOption(lines[i], 'nodesdir', nodesDir); lines[i] = setOption(lines[i], 'httpRoot'); lines[i] = setOption(lines[i], 'credentialSecret', secret); lines[i] = setOption(lines[i], 'valueConvert'); } fs.writeFileSync(userdataDir + 'settings.js', lines.join('\n')); } function writeStateList(callback) { adapter.getForeignObjects('*', 'state', ['rooms', 'functions'], function (err, obj) { // remove native information for (const i in obj) { if (obj.hasOwnProperty(i) && obj[i].native) { delete obj[i].native; } } fs.writeFileSync(nodePath + '/public/yunkong2.json', JSON.stringify(obj)); if (callback) callback(err); }); } function saveObjects() { if (saveTimer) { clearTimeout(saveTimer); saveTimer = null; } let cred = undefined; let flows = undefined; try { if (fs.existsSync(userdataDir + 'flows_cred.json')) { cred = JSON.parse(fs.readFileSync(userdataDir + 'flows_cred.json')); } } catch(e) { adapter.log.error('Cannot save ' + userdataDir + 'flows_cred.json'); } try { if (fs.existsSync(userdataDir + 'flows.json')) { flows = JSON.parse(fs.readFileSync(userdataDir + 'flows.json')); } } catch(e) { adapter.log.error('Cannot save ' + userdataDir + 'flows.json'); } //upload it to config adapter.setObject('flows', { common: { name: 'Flows for node-red' }, native: { cred: cred, flows: flows }, type: 'config' }, function () { adapter.log.info('Save ' + userdataDir + 'flows.json'); }); } function syncPublic(path) { if (!path) path = '/public'; const dir = fs.readdirSync(__dirname + path); if (!fs.existsSync(nodePath + path)) { fs.mkdirSync(nodePath + path); } for (let i = 0; i < dir.length; i++) { const stat = fs.statSync(__dirname + path + '/' + dir[i]); if (stat.isDirectory()) { syncPublic(path + '/' + dir[i]); } else { if (!fs.existsSync(nodePath + path + '/' + dir[i])) { fs.createReadStream(__dirname + path + '/' + dir[i]).pipe(fs.createWriteStream(nodePath + path + '/' + dir[i])); } } } } function installNotifierFlows(isFirst) { if (!notificationsFlows) { if (fs.existsSync(userdataDir + 'flows.json')) { if (!isFirst) saveObjects(); // monitor project file notificationsFlows = new Notify([userdataDir + 'flows.json']); notificationsFlows.on('change', function () { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(saveObjects, 500); }); } else { // Try to install notifier every 10 seconds till the file will be created setTimeout(function () { installNotifierFlows(); }, 10000); } } } function installNotifierCreds(isFirst) { if (!notificationsCreds) { if (fs.existsSync(userdataDir + 'flows_cred.json')) { if (!isFirst) saveObjects(); // monitor project file notificationsCreds = new Notify([userdataDir + 'flows_cred.json']); notificationsCreds.on('change', function () { if (saveTimer) clearTimeout(saveTimer); saveTimer = setTimeout(saveObjects, 500); }); } else { // Try to install notifier every 10 seconds till the file will be created setTimeout(function () { installNotifierCreds(); }, 10000); } } } function main() { // Find userdata directory // normally /opt/yunkong2/node_modules/yunkong2.js-controller // but can be /example/yunkong2.js-controller const controllerDir = utils.controllerDir; const parts = controllerDir.split('/'); if (parts.length > 1 && parts[parts.length - 2] === 'node_modules') { parts.splice(parts.length - 2, 2); userdataDir = parts.join('/'); userdataDir += '/yunkong2-data/node-red/'; } // create userdata directory if (!fs.existsSync(userdataDir)) { fs.mkdirSync(userdataDir); } syncPublic(); // Read configuration adapter.getObject('flows', function (err, obj) { if (obj && obj.native && obj.native.cred) { const c = JSON.stringify(obj.native.cred); // If really not empty if (c !== '{}' && c !== '[]') { fs.writeFileSync(userdataDir + 'flows_cred.json', JSON.stringify(obj.native.cred)); } } if (obj && obj.native && obj.native.flows) { const f = JSON.stringify(obj.native.flows); // If really not empty if (f !== '{}' && f !== '[]') { fs.writeFileSync(userdataDir + 'flows.json', JSON.stringify(obj.native.flows)); } } installNotifierFlows(true); installNotifierCreds(true); adapter.getForeignObject('system.config', (err, obj) => { if (obj && obj.native && obj.native.secret) { //noinspection JSUnresolvedVariable secret = obj.native.secret; } // Create settings for node-red writeSettings(); writeStateList(() => startNodeRed()); }); }); }