@maddytp/node-red-dashboard-2-ui-reef
Version:
Set of UI nodes for aquarium automation based on @flowfuse/node-red-dashboard
433 lines (432 loc) • 18 kB
JavaScript
/* eslint-disable no-prototype-builtins */
module.exports = function (RED) {
'use strict'
const util = require('util')
const vm = require('vm')
const acorn = require('acorn')
const acornWalk = require('acorn-walk')
function UIOutput (config) {
RED.nodes.createNode(this, config)
const node = this
const group = RED.nodes.getNode(config.group)
const base = group.getBase()
node.name = config.name
node.func = config.func
node.outputs = config.outputs
node.libs = config.libs || []
node.topic = config.topic
node.outstandingTimers = []
node.outstandingIntervals = []
node.clearStatus = false
function emit (val) {
base.emit('msg-display:' + node.id, val, node)
}
function sendResults (node, send, _msgid, msgs, cloneFirstMessage) {
if (msgs == null) {
return
} else if (!Array.isArray(msgs)) {
msgs = [msgs]
}
let msgCount = 0
for (let m = 0; m < msgs.length; m++) {
if (msgs[m]) {
if (!Array.isArray(msgs[m])) {
msgs[m] = [msgs[m]]
}
for (let n = 0; n < msgs[m].length; n++) {
let msg = msgs[m][n]
if (msg !== null && msg !== undefined) {
if (typeof msg === 'object' && !Buffer.isBuffer(msg) && !Array.isArray(msg)) {
if (msgCount === 0 && cloneFirstMessage !== false) {
msgs[m][n] = RED.util.cloneMessage(msgs[m][n])
msg = msgs[m][n]
}
if (typeof msg.display !== 'undefined') { emit(msg.display) }
msg._msgid = _msgid
msgCount++
} else {
let type = typeof msg
if (type === 'object') {
type = Buffer.isBuffer(msg) ? 'Buffer' : (Array.isArray(msg) ? 'Array' : 'Date')
}
node.error(RED._('function.error.non-message-returned', { type }))
emit('ERR')
}
}
}
}
}
if (msgCount > 0) {
send(msgs)
}
}
function createVMOpt (node, kind) {
const opt = {
filename: 'Function node' + kind + ':' + node.id + (node.name ? ' [' + node.name + ']' : ''),
displayErrors: true
}
return opt
}
function updateErrorInfo (err) {
if (err.stack) {
const stack = err.stack.toString()
const m = /^([^:]+):([^:]+):(\d+).*/.exec(stack)
if (m) {
const line = parseInt(m[3]) - 1
let kind = 'body:'
if (/setup/.exec(m[1])) {
kind = 'setup:'
}
if (/cleanup/.exec(m[1])) {
kind = 'cleanup:'
}
err.message += ' (' + kind + 'line ' + line + ')'
}
}
}
if (RED.settings.functionExternalModules === false && node.libs.length > 0) {
throw new Error(RED._('function.error.externalModuleNotAllowed'))
}
const functionText = 'var results = null;' +
'results = (async function(msg,__send__,__done__){ ' +
'var __msgid__ = msg._msgid;' +
'var node = {' +
'id:__node__.id,' +
'name:__node__.name,' +
'path:__node__.path,' +
'outputCount:__node__.outputCount,' +
'log:__node__.log,' +
'error:__node__.error,' +
'warn:__node__.warn,' +
'debug:__node__.debug,' +
'trace:__node__.trace,' +
'on:__node__.on,' +
'status:__node__.status,' +
'send:function(msgs,cloneMsg){ __node__.send(__send__,__msgid__,msgs,cloneMsg);},' +
'done:__done__' +
'};\n' +
node.func + '\n' +
'})(msg,__send__,__done__);'
let handleNodeDoneCall = true
if (/node\.done\s*\(\s*\)/.test(functionText)) {
acornWalk.simple(acorn.parse(functionText, { ecmaVersion: 'latest' }), {
CallExpression (astNode) {
if (astNode.callee && astNode.callee.object) {
if (astNode.callee.object.name === 'node' && astNode.callee.property.name === 'done') {
handleNodeDoneCall = false
}
}
}
})
}
const sandbox = {
console,
util,
Buffer,
Date,
RED: {
util: {
...RED.util,
getSetting: function (_node, name, _flow) {
return RED.util.getSetting(node, name)
}
}
},
__node__: {
id: node.id,
name: node.name,
path: node._path,
outputCount: node.outputs,
log: function () {
node.log.apply(node, arguments)
},
error: function () {
node.error.apply(node, arguments)
},
warn: function () {
node.warn.apply(node, arguments)
},
debug: function () {
node.debug.apply(node, arguments)
},
trace: function () {
node.trace.apply(node, arguments)
},
send: function (send, id, msgs, cloneMsg) {
sendResults(node, send, id, msgs, cloneMsg)
},
on: function () {
if (arguments[0] === 'input') {
throw new Error(RED._('function.error.inputListener'))
}
node.on.apply(node, arguments)
},
status: function () {
node.clearStatus = true
node.status.apply(node, arguments)
}
},
context: {
set: function () {
node.context().set.apply(node, arguments)
},
get: function () {
return node.context().get.apply(node, arguments)
},
keys: function () {
return node.context().keys.apply(node, arguments)
},
get global () {
return node.context().global
},
get flow () {
return node.context().flow
}
},
flow: {
set: function () {
node.context().flow.set.apply(node, arguments)
},
get: function () {
return node.context().flow.get.apply(node, arguments)
},
keys: function () {
return node.context().flow.keys.apply(node, arguments)
}
},
global: {
set: function () {
node.context().global.set.apply(node, arguments)
},
get: function () {
return node.context().global.get.apply(node, arguments)
},
keys: function () {
return node.context().global.keys.apply(node, arguments)
}
},
env: {
get: function (envVar) {
return RED.util.getSetting(node, envVar)
}
},
setTimeout: function () {
const func = arguments[0]
arguments[0] = function () {
sandbox.clearTimeout(timerId)
try {
func.apply(node, arguments)
} catch (err) {
node.error(err, {})
}
}
const timerId = setTimeout.apply(node, arguments)
node.outstandingTimers.push(timerId)
return timerId
},
clearTimeout: function (id) {
clearTimeout(id)
const index = node.outstandingTimers.indexOf(id)
if (index > -1) {
node.outstandingTimers.splice(index, 1)
}
},
setInterval: function () {
const func = arguments[0]
arguments[0] = function () {
try {
func.apply(node, arguments)
} catch (err) {
node.error(err, {})
}
}
const timerId = setInterval.apply(node, arguments)
node.outstandingIntervals.push(timerId)
return timerId
},
clearInterval: function (id) {
clearInterval(id)
const index = node.outstandingIntervals.indexOf(id)
if (index > -1) {
node.outstandingIntervals.splice(index, 1)
}
}
}
if (util.hasOwnProperty('promisify')) {
sandbox.setTimeout[util.promisify.custom] = function (after, value) {
return new Promise(function (resolve, reject) {
sandbox.setTimeout(function () { resolve(value) }, after)
})
}
sandbox.promisify = util.promisify
}
const moduleLoadPromises = []
if (node.hasOwnProperty('libs')) {
let moduleErrors = false
const modules = node.libs
modules.forEach(module => {
const vname = module.hasOwnProperty('var') ? module.var : null
if (vname && (vname !== '')) {
if (sandbox.hasOwnProperty(vname) || vname === 'node') {
node.error(RED._('function.error.moduleNameError', { name: vname }))
moduleErrors = true
return
}
sandbox[vname] = null
const spec = module.module
if (spec && (spec !== '')) {
moduleLoadPromises.push(RED.import(module.module).then(lib => {
sandbox[vname] = lib.default || lib
}).catch(err => {
node.error(RED._('function.error.moduleLoadError', { module: module.spec, error: err.toString() }))
throw err
}))
}
}
})
if (moduleErrors) {
throw new Error(RED._('function.error.externalModuleLoadError'))
}
}
let func = false
let processMessage = () => {}
const evts = {
/**
* @param {object} msg - the last known msg received (prior to this new value)
* @param {boolean} value - the updated value sent by the widget
*/
onChange: async function (msg, value) {
node.context().set('state', value)
base.stores.data.save(base, node, value)
const p = value.payload
const n = RED.nodes.getNode(node.id)
n.status({ fill: 'blue', shape: 'dot', text: p.label })
delete msg._client
delete msg.display
msg.payload = p.value
if (config.topic) { msg.topic = config.topic }
if (p.valueType === 'func') {
func = true
} else {
func = false
n.send(msg)
}
},
onInput: function (msg, send) {}
}
const init = base.stores.data.get(node.id) === undefined ? node.context().get('state') || { payload: config.options[0] } : base.stores.data.get(node.id)
if (init !== undefined) {
base.stores.data.save(base, node, init)
const msg = {}
if (init.payload.valueType === 'func') {
func = true
} else {
func = false
if (config.topic) { msg.topic = config.topic }
msg.payload = init.payload.value
setTimeout(function () {
node.status({ fill: 'blue', shape: 'dot', text: init.payload.label })
node.send(msg)
}, 1000)
}
}
config.options.forEach(option => {
option.value = RED.util.evaluateNodeProperty(option.value, option.valueType, node)
})
if (group) {
group.register(node, config, evts)
} else {
node.error('No group configured')
}
node.on('input', function (msg, send, done) {
if (func) {
processMessage(msg, send, done)
}
})
Promise.all(moduleLoadPromises).then(() => {
const context = vm.createContext(sandbox)
try {
node.script = new vm.Script(functionText, createVMOpt(node, ''))
processMessage = function (msg, send, done) {
const start = process.hrtime()
context.msg = msg
context.__send__ = send
context.__done__ = done
let opts = {}
if (node.timeout > 0) {
opts = node.timeoutOptions
}
node.script.runInContext(context, opts)
context.results.then(function (results) {
sendResults(node, send, msg._msgid, results, false)
if (handleNodeDoneCall) {
done()
}
const duration = process.hrtime(start)
const converted = Math.floor((duration[0] * 1e9 + duration[1]) / 10000) / 100
node.metric('duration', msg, converted)
if (process.env.NODE_RED_FUNCTION_TIME) {
node.status({ fill: 'yellow', shape: 'dot', text: '' + converted })
}
}).catch(err => {
emit('ERR')
if ((typeof err === 'object') && err.hasOwnProperty('stack')) {
const index = err.stack.search(/\n\s*at ContextifyScript.Script.runInContext/)
err.stack = err.stack.slice(0, index).split('\n').slice(0, -1).join('\n')
const stack = err.stack.split(/\r?\n/)
msg.error = err
if (stack.length > 0) {
let line = 0
let errorMessage
while (line < stack.length && stack[line].indexOf('ReferenceError') !== 0) {
line++
}
if (line < stack.length) {
errorMessage = stack[line]
const m = /:(\d+):(\d+)$/.exec(stack[line + 1])
if (m) {
const lineno = Number(m[1]) - 1
const cha = m[2]
errorMessage += ' (line ' + lineno + ', col ' + cha + ')'
}
}
if (errorMessage) {
err.message = errorMessage
}
}
done(err)
} else if (typeof err === 'string') {
done(err)
} else {
done(JSON.stringify(err))
}
})
}
node.on('close', function () {
while (node.outstandingTimers.length > 0) {
clearTimeout(node.outstandingTimers.pop())
}
while (node.outstandingIntervals.length > 0) {
clearInterval(node.outstandingIntervals.pop())
}
if (node.clearStatus) {
node.status({})
}
})
} catch (err) {
updateErrorInfo(err)
node.error(err)
}
}).catch(() => {
node.error(RED._('function.error.externalModuleLoadError'))
})
}
RED.nodes.registerType('ui-output', UIOutput, {
dynamicModuleList: 'libs',
settings: {
functionExternalModules: { value: true, exportable: true },
functionTimeout: { value: 0, exportable: true }
}
})
RED.library.register('ui-output')
}