UNPKG

16.5 kBJavaScriptView Raw
1/**
2 *
3 * ioBroker node-red Adapter
4 *
5 * (c) 2014-2020 bluefox<bluefox@ccu.io>
6 *
7 * Apache 2.0 License
8 *
9 */
10/* jshint -W097 */
11/* jshint strict: false */
12/* jslint node: true */
13'use strict';
14
15const utils = require('@iobroker/adapter-core'); // Get common adapter utils
16const adapterName = require('./package.json').name.split('.').pop();
17
18let adapter;
19const fs = require('fs');
20const path = require('path');
21const spawn = require('child_process').spawn;
22const Notify = require('fs.notify');
23const attempts = {};
24const additional = [];
25let secret;
26
27let userDataDir = __dirname + '/userdata/';
28
29function startAdapter(options) {
30 options = options || {};
31 Object.assign(options, {
32 name: adapterName,
33 systemConfig: true,
34 unload: unloadRed
35 });
36
37 adapter = new utils.Adapter(options);
38
39 adapter.on('message', obj => obj && obj.command && processMessage(obj));
40
41 adapter.on('ready', () => installLibraries(main));
42
43 return adapter;
44}
45
46function installNpm(npmLib, callback) {
47 const path = __dirname;
48 if (typeof npmLib === 'function') {
49 callback = npmLib;
50 npmLib = undefined;
51 }
52
53 const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '" --save';
54 adapter.log.info(cmd + ' (System call)');
55 // Install node modules as system call
56
57 // System call used for update of js-controller itself,
58 // because during installation npm packet will be deleted too, but some files must be loaded even during the install process.
59 const exec = require('child_process').exec;
60 const child = exec(cmd);
61 child.stdout.on('data', buf => adapter.log.info(buf.toString('utf8')));
62 child.stderr.on('data', buf => adapter.log.error(buf.toString('utf8')));
63
64 child.on('exit', (code, signal) => {
65 code && adapter.log.error('Cannot install ' + npmLib + ': ' + code);
66 // command succeeded
67 callback && callback(npmLib);
68 });
69}
70
71function installLibraries(callback) {
72 let allInstalled = true;
73 if (typeof adapter.common.npmLibs === 'string') {
74 adapter.common.npmLibs = adapter.common.npmLibs.split(/[,;\s]+/);
75 }
76
77 if (adapter.common && adapter.common.npmLibs) {
78 for (let lib = 0; lib < adapter.common.npmLibs.length; lib++) {
79 if (adapter.common.npmLibs[lib] && adapter.common.npmLibs[lib].trim()) {
80 adapter.common.npmLibs[lib] = adapter.common.npmLibs[lib].trim();
81 if (!fs.existsSync(__dirname + '/node_modules/' + adapter.common.npmLibs[lib] + '/package.json')) {
82
83 if (!attempts[adapter.common.npmLibs[lib]]) {
84 attempts[adapter.common.npmLibs[lib]] = 1;
85 } else {
86 attempts[adapter.common.npmLibs[lib]]++;
87 }
88 if (attempts[adapter.common.npmLibs[lib]] > 3) {
89 adapter.log.error('Cannot install npm packet: ' + adapter.common.npmLibs[lib]);
90 continue;
91 }
92
93 installNpm(adapter.common.npmLibs[lib], () =>
94 setImmediate(() => installLibraries(callback)));
95
96 allInstalled = false;
97 break;
98 } else {
99 if (additional.indexOf(adapter.common.npmLibs[lib]) === -1) {
100 additional.push(adapter.common.npmLibs[lib]);
101 }
102 }
103 }
104 }
105 }
106 allInstalled && callback();
107}
108
109// is called if a subscribed state changes
110//adapter.on('stateChange', function (id, state) {
111//});
112function unloadRed (callback) {
113 // Stop node-red
114 stopping = true;
115 if (redProcess) {
116 adapter.log.info('kill node-red task');
117 redProcess.kill();
118 redProcess = null;
119 }
120 notificationsCreds && notificationsCreds.close();
121 notificationsFlows && notificationsFlows.close();
122
123 callback && callback();
124}
125
126function processMessage(obj) {
127 switch (obj.command) {
128 case 'update':
129 writeStateList(error =>
130 obj.callback && adapter.sendTo(obj.from, obj.command, error, obj.callback));
131 break;
132
133 case 'stopInstance':
134 unloadRed();
135 break;
136
137 }
138}
139
140function getNodeRedPath() {
141 let nodeRed = __dirname + '/node_modules/node-red';
142 if (!fs.existsSync(nodeRed)) {
143 nodeRed = path.normalize(__dirname + '/../node-red');
144 if (!fs.existsSync(nodeRed)) {
145 nodeRed = path.normalize(__dirname + '/../node_modules/node-red');
146 if (!fs.existsSync(nodeRed)) {
147 adapter && adapter.log && adapter.log.error('Cannot find node-red packet!');
148 throw new Error('Cannot find node-red packet!');
149 }
150 }
151 }
152
153 return nodeRed;
154}
155
156function getNodeRedEditorPath() {
157 let nodeRedEditor = __dirname + '/node_modules/@node-red/editor-client';
158 if (!fs.existsSync(nodeRedEditor)) {
159 nodeRedEditor = path.normalize(__dirname + '/../@node-red/editor-client');
160 if (!fs.existsSync(nodeRedEditor)) {
161 nodeRedEditor = path.normalize(__dirname + '/../node_modules/@node-red/editor-client');
162 if (!fs.existsSync(nodeRedEditor)) {
163 adapter && adapter.log && adapter.log.error('Cannot find @node-red/editor-client packet!');
164 throw new Error('Cannot find @node-red/editor-client packet!');
165 }
166 }
167 }
168 return nodeRedEditor;
169}
170
171let redProcess;
172let stopping;
173let notificationsFlows;
174let notificationsCreds;
175let saveTimer;
176const nodePath = getNodeRedPath();
177const editorClientPath = getNodeRedEditorPath();
178
179function startNodeRed() {
180 adapter.config.maxMemory = parseInt(adapter.config.maxMemory, 10) || 128;
181 const args = ['--max-old-space-size=' + adapter.config.maxMemory, nodePath + '/red.js', '-v', '--settings', userDataDir + 'settings.js'];
182
183 if (adapter.config.safeMode) {
184 args.push('--safe');
185 }
186
187 adapter.log.info('Starting node-red: ' + args.join(' '));
188
189 redProcess = spawn('node', args);
190
191 redProcess.on('error', err =>
192 adapter.log.error('catched exception from node-red:' + JSON.stringify(err)));
193
194 redProcess.stdout.on('data', data => {
195 if (!data) {
196 return;
197 }
198
199 data = data.toString();
200
201 if (data[data.length - 2] === '\r' && data[data.length - 1] === '\n') data = data.substring(0, data.length - 2);
202 if (data[data.length - 2] === '\n' && data[data.length - 1] === '\r') data = data.substring(0, data.length - 2);
203 if (data[data.length - 1] === '\r') data = data.substring(0, data.length - 1);
204
205 if (data.indexOf('[err') !== -1) {
206 adapter.log.error(data);
207 } else if (data.indexOf('[warn]') !== -1) {
208 adapter.log.warn(data);
209 } else if (data.indexOf('[info] [debug:') !== -1) {
210 adapter.log.info(data);
211 } else {
212 adapter.log.debug(data);
213 }
214 });
215
216 redProcess.stderr.on('data', data => {
217 if (!data) {
218 return;
219 }
220 if (data[0]) {
221 let text = '';
222 for (let i = 0; i < data.length; i++) {
223 text += String.fromCharCode(data[i]);
224 }
225 data = text;
226 }
227 if (data.indexOf && data.indexOf('[warn]') === -1) {
228 adapter.log.warn(data);
229 } else {
230 adapter.log.error(JSON.stringify(data));
231 }
232 });
233
234 redProcess.on('exit', exitCode => {
235 adapter.log.info('node-red exited with ' + exitCode);
236 redProcess = null;
237 if (!stopping) {
238 setTimeout(startNodeRed, 5000);
239 }
240 });
241}
242
243function setOption(line, option, value) {
244 const toFind = "'%%" + option + "%%'";
245 const pos = line.indexOf(toFind);
246 if (pos !== -1) {
247 return line.substring(0, pos) + ((value !== undefined) ? value : (adapter.config[option] === null || adapter.config[option] === undefined) ? '' : adapter.config[option]) + line.substring(pos + toFind.length);
248 }
249 return line;
250}
251
252function writeSettings() {
253 const config = JSON.stringify(adapter.systemConfig);
254 const text = fs.readFileSync(__dirname + '/settings.js').toString();
255 const lines = text.split('\n');
256 let npms = '\r\n';
257 const dir = __dirname.replace(/\\/g, '/') + '/node_modules/';
258 const nodesDir = '"' + __dirname.replace(/\\/g, '/') + '/nodes/"';
259
260 const bind = '"' + (adapter.config.bind || '0.0.0.0') + '"';
261 const auth = adapter.config.user && adapter.config.pass ? JSON.stringify({user: adapter.config.user, pass: adapter.config.pass}) : '""';
262 const pass = '"' + adapter.config.pass + '"';
263
264 for (let a = 0; a < additional.length; a++) {
265 if (additional[a].startsWith('node-red-')) {
266 continue;
267 }
268 npms += ' "' + additional[a] + '": require("' + dir + additional[a] + '")';
269 if (a !== additional.length - 1) {
270 npms += ', \r\n';
271 }
272 }
273
274 // update from 1.0.1 (new convert-option)
275 if (adapter.config.valueConvert === null ||
276 adapter.config.valueConvert === undefined ||
277 adapter.config.valueConvert === '' ||
278 adapter.config.valueConvert === 'true' ||
279 adapter.config.valueConvert === '1' ||
280 adapter.config.valueConvert === 1) {
281 adapter.config.valueConvert = true;
282 }
283 if (adapter.config.valueConvert === 0 ||
284 adapter.config.valueConvert === '0' ||
285 adapter.config.valueConvert === 'false') {
286 adapter.config.valueConvert = false;
287 }
288 for (let i = 0; i < lines.length; i++) {
289 lines[i] = setOption(lines[i], 'port');
290 lines[i] = setOption(lines[i], 'auth', auth);
291 lines[i] = setOption(lines[i], 'pass', pass);
292 lines[i] = setOption(lines[i], 'bind', bind);
293 lines[i] = setOption(lines[i], 'port');
294 lines[i] = setOption(lines[i], 'instance', adapter.instance);
295 lines[i] = setOption(lines[i], 'config', config);
296 lines[i] = setOption(lines[i], 'functionGlobalContext', npms);
297 lines[i] = setOption(lines[i], 'nodesdir', nodesDir);
298 lines[i] = setOption(lines[i], 'httpRoot');
299 lines[i] = setOption(lines[i], 'credentialSecret', secret);
300 lines[i] = setOption(lines[i], 'valueConvert');
301 lines[i] = setOption(lines[i], 'projectsEnabled', adapter.config.projectsEnabled);
302 lines[i] = setOption(lines[i], 'allowCreationOfForeignObjects', adapter.config.allowCreationOfForeignObjects);
303 }
304
305 const oldText = fs.existsSync(userDataDir + 'settings.js') ? fs.readFileSync(userDataDir + 'settings.js').toString('utf8') : '';
306 const newText = lines.join('\n');
307 if (oldText !== newText) {
308 fs.writeFileSync(userDataDir + 'settings.js', newText);
309 }
310}
311
312function writeStateList(callback) {
313 adapter.getForeignObjects('*', 'state', ['rooms', 'functions'], (err, obj) => {
314 // remove native information
315 for (const i in obj) {
316 if (obj.hasOwnProperty(i) && obj[i].native) {
317 delete obj[i].native;
318 }
319 }
320
321 fs.writeFileSync(editorClientPath + '/public/iobroker.json', JSON.stringify(obj));
322 callback && callback(err);
323 });
324}
325
326function saveObjects() {
327 if (saveTimer) {
328 clearTimeout(saveTimer);
329 saveTimer = null;
330 }
331 let cred = undefined;
332 let flows = undefined;
333
334 try {
335 if (fs.existsSync(userDataDir + 'flows_cred.json')) {
336 cred = JSON.parse(fs.readFileSync(userDataDir + 'flows_cred.json'));
337 }
338 } catch(e) {
339 adapter.log.error('Cannot save ' + userDataDir + 'flows_cred.json');
340 }
341 try {
342 if (fs.existsSync(userDataDir + 'flows.json')) {
343 flows = JSON.parse(fs.readFileSync(userDataDir + 'flows.json'));
344 }
345 } catch(e) {
346 adapter.log.error('Cannot save ' + userDataDir + 'flows.json');
347 }
348 //upload it to config
349 adapter.setObject('flows',
350 {
351 common: {
352 name: 'Flows for node-red'
353 },
354 native: {
355 cred: cred,
356 flows: flows
357 },
358 type: 'config'
359 },
360 () => adapter.log.info('Save ' + userDataDir + 'flows.json')
361 );
362}
363
364function syncPublic(path) {
365 path = path || '/public';
366
367 const dir = fs.readdirSync(__dirname + path);
368
369 if (!fs.existsSync(editorClientPath + path)) {
370 fs.mkdirSync(editorClientPath + path);
371 }
372
373 for (let i = 0; i < dir.length; i++) {
374 const stat = fs.statSync(__dirname + path + '/' + dir[i]);
375 if (stat.isDirectory()) {
376 syncPublic(path + '/' + dir[i]);
377 } else {
378 if (!fs.existsSync(editorClientPath + path + '/' + dir[i])) {
379 fs.createReadStream(__dirname + path + '/' + dir[i]).pipe(fs.createWriteStream(editorClientPath + path + '/' + dir[i]));
380 }
381 }
382 }
383}
384
385function installNotifierFlows(isFirst) {
386 if (!notificationsFlows) {
387 if (fs.existsSync(userDataDir + 'flows.json')) {
388 if (!isFirst) saveObjects();
389 // monitor project file
390 notificationsFlows = new Notify([userDataDir + 'flows.json']);
391 notificationsFlows.on('change', () => {
392 saveTimer && clearTimeout(saveTimer);
393 saveTimer = setTimeout(saveObjects, 500);
394 });
395 } else {
396 // Try to install notifier every 10 seconds till the file will be created
397 setTimeout(() => installNotifierFlows(), 10000);
398 }
399 }
400}
401
402function installNotifierCreds(isFirst) {
403 if (!notificationsCreds) {
404 if (fs.existsSync(userDataDir + 'flows_cred.json')) {
405 if (!isFirst) saveObjects();
406 // monitor project file
407 notificationsCreds = new Notify([userDataDir + 'flows_cred.json']);
408 notificationsCreds.on('change', () => {
409 saveTimer && clearTimeout(saveTimer);
410 saveTimer = setTimeout(saveObjects, 500);
411 });
412 } else {
413 // Try to install notifier every 10 seconds till the file will be created
414 setTimeout(() => installNotifierCreds(), 10000);
415 }
416 }
417}
418
419function main() {
420 if (adapter.config.projectsEnabled === undefined) adapter.config.projectsEnabled = false;
421 if (adapter.config.allowCreationOfForeignObjects === undefined) adapter.config.allowCreationOfForeignObjects = false;
422
423 // Find userdata directory
424
425 // normally /opt/iobroker/node_modules/iobroker.js-controller
426 // but can be /example/ioBroker.js-controller
427 const controllerDir = utils.controllerDir;
428 const parts = controllerDir.split('/');
429 if (parts.length > 1 && parts[parts.length - 2] === 'node_modules') {
430 parts.splice(parts.length - 2, 2);
431 userDataDir = parts.join('/');
432 userDataDir += '/iobroker-data/node-red/';
433 }
434
435 // create userData directory
436 if (!fs.existsSync(userDataDir)) {
437 fs.mkdirSync(userDataDir);
438 }
439
440 syncPublic();
441
442 // Read configuration
443 adapter.getObject('flows', (err, obj) => {
444 if (obj && obj.native && obj.native.cred) {
445 const c = JSON.stringify(obj.native.cred);
446 // If really not empty
447 if (c !== '{}' && c !== '[]') {
448 fs.writeFileSync(userDataDir + 'flows_cred.json', JSON.stringify(obj.native.cred));
449 }
450 }
451 if (obj && obj.native && obj.native.flows) {
452 const f = JSON.stringify(obj.native.flows);
453 // If really not empty
454 if (f !== '{}' && f !== '[]') {
455 fs.writeFileSync(userDataDir + 'flows.json', JSON.stringify(obj.native.flows));
456 }
457 }
458
459 installNotifierFlows(true);
460 installNotifierCreds(true);
461
462 adapter.getForeignObject('system.config', (err, obj) => {
463 if (obj && obj.native && obj.native.secret) {
464 //noinspection JSUnresolvedVariable
465 secret = obj.native.secret;
466 }
467 // Create settings for node-red
468 writeSettings();
469 writeStateList(() => startNodeRed());
470 });
471 });
472}
473
474// If started as allInOne/compact mode => return function to create instance
475if (module && module.parent) {
476 module.exports = startAdapter;
477} else {
478 // or start the instance directly
479 startAdapter();
480}
481