1192 lines
44 KiB
JavaScript
1192 lines
44 KiB
JavaScript
/* jshint -W097 */
|
|
/* jshint -W083 */
|
|
/* jshint strict:false */
|
|
/* jslint node: true */
|
|
/* jshint shadow:true */
|
|
'use strict';
|
|
let NodeVM;
|
|
let VMScript;
|
|
let vm;
|
|
if (true || parseInt(process.versions.node.split('.')[0]) < 6) {
|
|
vm = require('vm');
|
|
} else {
|
|
try {
|
|
const VM2 = require('vm2');
|
|
NodeVM = VM2.NodeVM;
|
|
VMScript = VM2.VMScript;
|
|
} catch (e) {
|
|
vm = require('vm');
|
|
}
|
|
}
|
|
const nodeFS = require('fs');
|
|
const coffeeCompiler = require('coffee-compiler');
|
|
const tsc = require('virtual-tsc');
|
|
const typescript = require('typescript');
|
|
const nodeSchedule = require('node-schedule');
|
|
|
|
const mods = {
|
|
fs: {},
|
|
dgram: require('dgram'),
|
|
crypto: require('crypto'),
|
|
dns: require('dns'),
|
|
events: require('events'),
|
|
http: require('http'),
|
|
https: require('https'),
|
|
net: require('net'),
|
|
os: require('os'),
|
|
path: require('path'),
|
|
util: require('util'),
|
|
child_process: require('child_process'),
|
|
suncalc: require('suncalc'),
|
|
request: require('request'),
|
|
wake_on_lan: require('wake_on_lan')
|
|
};
|
|
|
|
const utils = require('./lib/utils'); // Get common adapter utils
|
|
const words = require('./lib/words');
|
|
const sandBox = require('./lib/sandbox');
|
|
const eventObj = require('./lib/eventObj');
|
|
|
|
// for node version <= 0.12
|
|
if (''.startsWith === undefined) {
|
|
String.prototype.startsWith = function (s) {
|
|
return this.indexOf(s) === 0;
|
|
};
|
|
}
|
|
if (''.endsWith === undefined) {
|
|
String.prototype.endsWith = function (s) {
|
|
return this.slice(0 - s.length) === s;
|
|
};
|
|
}
|
|
///
|
|
|
|
let webstormDebug;
|
|
if (process.argv) {
|
|
for (let a = 1; a < process.argv.length; a++) {
|
|
if (process.argv[a].startsWith('--webstorm')) {
|
|
webstormDebug = process.argv[a].replace(/^(.*?=\s*)/, '');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @type {typescript.CompilerOptions} */
|
|
const tsCompilerOptions = {
|
|
// don't compile faulty scripts
|
|
noEmitOnError: true,
|
|
// emit declarations for global scripts
|
|
declaration: true,
|
|
// In order to run scripts as a NodeJS vm.Script,
|
|
// we need to target ES5, otherwise the compiled
|
|
// scripts may include `import` keywords, which are not
|
|
// supported by vm.Script.
|
|
target: typescript.ScriptTarget.ES5,
|
|
// NodeJS 4+ supports the features of ES2015
|
|
// When upgrading the minimum supported version to NodeJS 8 or higher,
|
|
// consider changing this, so we get to support the newest features too
|
|
lib: ['lib.es2015.d.ts'],
|
|
};
|
|
const jsDeclarationCompilerOptions = Object.assign(
|
|
{}, tsCompilerOptions,
|
|
{
|
|
// we only care about the declarations
|
|
emitDeclarationOnly: true,
|
|
// allow errors
|
|
noEmitOnError: false,
|
|
noImplicitAny: false,
|
|
strict: false,
|
|
}
|
|
);
|
|
|
|
// ambient declarations for typescript
|
|
/** @type {Record<string, string>} */
|
|
let tsAmbient;
|
|
/** @type {tsc.Server} */
|
|
let tsServer;
|
|
/** @type {tsc.Server} */
|
|
let jsDeclarationServer;
|
|
|
|
/**
|
|
* @param {string} scriptID - The current script the declarations were generated from
|
|
* @param {string} declarations
|
|
*/
|
|
function provideDeclarationsForGlobalScript(scriptID, declarations) {
|
|
// Remember which declarations this global script had access to
|
|
// we need this so the editor doesn't show a duplicate identifier error
|
|
if (globalDeclarations != null && globalDeclarations !== '') {
|
|
knownGlobalDeclarationsByScript[scriptID] = globalDeclarations;
|
|
}
|
|
// and concatenate the global declarations for the next scripts
|
|
globalDeclarations += declarations + '\n';
|
|
// remember all previously generated global declarations,
|
|
// so global scripts can reference each other
|
|
const globalDeclarationPath = 'global.d.ts';
|
|
tsAmbient[globalDeclarationPath] = globalDeclarations;
|
|
// make sure the next script compilation has access to the updated declarations
|
|
tsServer.provideAmbientDeclarations({
|
|
[globalDeclarationPath]: globalDeclarations
|
|
});
|
|
jsDeclarationServer.provideAmbientDeclarations({
|
|
[globalDeclarationPath]: globalDeclarations
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Translates a script ID to a filename for the compiler
|
|
* @param {string} scriptID The ID of the script
|
|
*/
|
|
function scriptIdToTSFilename(scriptID) {
|
|
return scriptID.replace(/^script.js./, '').replace(/\./g, '/') + '.ts';
|
|
}
|
|
|
|
const context = {
|
|
mods,
|
|
objects: {},
|
|
states: {},
|
|
stateIds: [],
|
|
errorLogFunction: null,
|
|
subscriptions: [],
|
|
adapterSubs: {},
|
|
subscribedPatterns: {},
|
|
cacheObjectEnums: {},
|
|
isEnums: false, // If some subscription wants enum
|
|
channels: null,
|
|
devices: null,
|
|
logWithLineInfo: null,
|
|
timers: {},
|
|
enums: [],
|
|
timerId: 0,
|
|
names: {},
|
|
scripts: {}
|
|
};
|
|
|
|
const regExEnum = /^enum\./;
|
|
const regExGlobalOld = /_global$/;
|
|
const regExGlobalNew = /script\.js\.global\./;
|
|
|
|
function checkIsGlobal(obj) {
|
|
return regExGlobalOld.test(obj.common.name) || regExGlobalNew.test(obj._id);
|
|
}
|
|
|
|
const adapter = new utils.Adapter({
|
|
|
|
name: 'javascript',
|
|
|
|
useFormatDate: true, // load float formatting
|
|
|
|
objectChange: (id, obj) => {
|
|
if (regExEnum.test(id)) {
|
|
// clear cache
|
|
context.cacheObjectEnums = {};
|
|
}
|
|
|
|
if (!obj) {
|
|
// object deleted
|
|
if (!context.objects[id]) return;
|
|
|
|
// Script deleted => remove it
|
|
if (context.objects[id].type === 'script' && context.objects[id].common.engine === 'system.adapter.' + adapter.namespace) {
|
|
stop(id);
|
|
|
|
const idActive = 'scriptEnabled.' + id.substring('script.js.'.length);
|
|
adapter.delObject(idActive);
|
|
adapter.delState(idActive);
|
|
}
|
|
|
|
removeFromNames(id);
|
|
delete context.objects[id];
|
|
} else if (!context.objects[id]) {
|
|
// New object
|
|
context.objects[id] = obj;
|
|
|
|
addToNames(obj);
|
|
|
|
if (obj.type === 'script' && obj.common.engine === 'system.adapter.' + adapter.namespace) {
|
|
// create states for scripts
|
|
createActiveObject(id, obj.common.enabled);
|
|
|
|
if (obj.common.enabled) {
|
|
if (checkIsGlobal(obj)) {
|
|
// restart adapter
|
|
adapter.getForeignObject('system.adapter.' + adapter.namespace, (err, _obj) => {
|
|
if (_obj) adapter.setForeignObject('system.adapter.' + adapter.namespace, _obj);
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Start script
|
|
load(id);
|
|
}
|
|
}
|
|
// added new script to this engine
|
|
} else if (context.objects[id].common) {
|
|
const n = getName(id);
|
|
|
|
if (n !== context.objects[id].common.name) {
|
|
if (n) removeFromNames(id);
|
|
if (context.objects[id].common.name) addToNames(obj);
|
|
}
|
|
|
|
// Object just changed
|
|
if (obj.type !== 'script') {
|
|
context.objects[id] = obj;
|
|
|
|
if (id === 'system.config') {
|
|
// set langugae for debug messages
|
|
if (context.objects['system.config'].common.language) words.setLanguage(context.objects['system.config'].common.language);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (checkIsGlobal(context.objects[id])) {
|
|
// restart adapter
|
|
adapter.getForeignObject('system.adapter.' + adapter.namespace, function (err, obj) {
|
|
if (obj) {
|
|
adapter.setForeignObject('system.adapter.' + adapter.namespace, obj);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (obj.common && obj.common.engine === 'system.adapter.' + adapter.namespace) {
|
|
// create states for scripts
|
|
createActiveObject(id, obj.common.enabled);
|
|
}
|
|
|
|
if ((context.objects[id].common.enabled && !obj.common.enabled) ||
|
|
(context.objects[id].common.engine === 'system.adapter.' + adapter.namespace && obj.common.engine !== 'system.adapter.' + adapter.namespace)) {
|
|
|
|
// Script disabled
|
|
if (context.objects[id].common.enabled && context.objects[id].common.engine === 'system.adapter.' + adapter.namespace) {
|
|
// Remove it from executing
|
|
context.objects[id] = obj;
|
|
stop(id);
|
|
} else {
|
|
context.objects[id] = obj;
|
|
}
|
|
} else if ((!context.objects[id].common.enabled && obj.common.enabled) ||
|
|
(context.objects[id].common.engine !== 'system.adapter.' + adapter.namespace && obj.common.engine === 'system.adapter.' + adapter.namespace)) {
|
|
// Script enabled
|
|
context.objects[id] = obj;
|
|
|
|
if (context.objects[id].common.enabled && context.objects[id].common.engine === 'system.adapter.' + adapter.namespace) {
|
|
// Start script
|
|
load(id);
|
|
}
|
|
} else { //if (obj.common.source !== context.objects[id].common.source) {
|
|
context.objects[id] = obj;
|
|
|
|
// Source changed => restart it
|
|
stop(id, function (res, _id) {
|
|
load(_id);
|
|
});
|
|
} /*else {
|
|
// Something changed or not for us
|
|
objects[id] = obj;
|
|
}*/
|
|
}
|
|
},
|
|
|
|
stateChange: (id, state) => {
|
|
if (!id || id.startsWith('messagebox.') || id.startsWith('log.')) return;
|
|
|
|
const oldState = context.states[id];
|
|
if (state) {
|
|
if (oldState) {
|
|
// enable or disable script
|
|
if (!state.ack && id.startsWith(activeStr) && context.objects[id] && context.objects[id].native && context.objects[id].native.script) {
|
|
adapter.extendForeignObject(context.objects[id].native.script, {common: {enabled: state.val}});
|
|
}
|
|
|
|
// monitor if adapter is alive and send all subscriptions once more, after adapter goes online
|
|
if (/*oldState && */oldState.val === false && state.val && id.endsWith('.alive')) {
|
|
if (context.adapterSubs[id]) {
|
|
const parts = id.split('.');
|
|
const a = parts[2] + '.' + parts[3];
|
|
for (let t = 0; t < context.adapterSubs[id].length; t++) {
|
|
adapter.log.info('Detected coming adapter "' + a + '". Send subscribe: ' + context.adapterSubs[id][t]);
|
|
adapter.sendTo(a, 'subscribe', context.adapterSubs[id][t]);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (/*!oldState && */context.stateIds.indexOf(id) === -1) {
|
|
context.stateIds.push(id);
|
|
context.stateIds.sort();
|
|
}
|
|
}
|
|
context.states[id] = state;
|
|
} else {
|
|
if (oldState) delete context.states[id];
|
|
state = {};
|
|
const pos = context.stateIds.indexOf(id);
|
|
if (pos !== -1) {
|
|
context.stateIds.splice(pos, 1);
|
|
}
|
|
}
|
|
const _eventObj = eventObj.createEventObject(context, id, state, oldState);
|
|
|
|
// if this state matches any subscriptions
|
|
for (let i = 0, l = context.subscriptions.length; i < l; i++) {
|
|
const sub = context.subscriptions[i];
|
|
if (sub && patternMatching(_eventObj, sub.patternCompareFunctions)) {
|
|
sub.callback(_eventObj);
|
|
}
|
|
}
|
|
},
|
|
|
|
unload: callback => callback(),
|
|
|
|
ready: function () {
|
|
// todo
|
|
context.errorLogFunction = webstormDebug ? console : adapter.log;
|
|
activeStr = adapter.namespace + '.scriptEnabled.';
|
|
|
|
// try to read TS declarations
|
|
try {
|
|
tsAmbient = {
|
|
'javascript.d.ts': nodeFS.readFileSync(mods.path.join(__dirname, 'lib/javascript.d.ts'), 'utf8')
|
|
};
|
|
tsServer.provideAmbientDeclarations(tsAmbient);
|
|
jsDeclarationServer.provideAmbientDeclarations(tsAmbient);
|
|
} catch (e) {
|
|
adapter.log.warn('Could not read TypeScript ambient declarations: ' + e);
|
|
}
|
|
|
|
context.logWithLineInfo = function (level, msg) {
|
|
if (msg === undefined) {
|
|
return context.logWithLineInfo ('info', msg);
|
|
}
|
|
|
|
context.errorLogFunction && context.errorLogFunction[level](msg);
|
|
|
|
const stack = (new Error().stack).split('\n');
|
|
|
|
for (let i = 3; i < stack.length; i++) {
|
|
if (!stack[i]) continue;
|
|
if (stack[i].match(/runInContext|runInNewContext|javascript\.js:/)) break;
|
|
context.errorLogFunction && context.errorLogFunction[level](fixLineNo(stack[i]));
|
|
}
|
|
};
|
|
|
|
context.logWithLineInfo.warn = context.logWithLineInfo.bind(1, 'warn');
|
|
context.logWithLineInfo.error = context.logWithLineInfo.bind(1, 'error');
|
|
context.logWithLineInfo.info = context.logWithLineInfo.bind(1, 'info');
|
|
|
|
installLibraries(() => {
|
|
getData(() => {
|
|
adapter.subscribeForeignObjects('*');
|
|
|
|
if (!adapter.config.subscribe) {
|
|
adapter.subscribeForeignStates('*');
|
|
}
|
|
|
|
adapter.objects.getObjectView('script', 'javascript', {}, (err, doc) => {
|
|
globalScript = '';
|
|
globalDeclarations = '';
|
|
knownGlobalDeclarationsByScript = {};
|
|
let count = 0;
|
|
if (doc && doc.rows && doc.rows.length) {
|
|
// assemble global script
|
|
for (let g = 0; g < doc.rows.length; g++) {
|
|
if (checkIsGlobal(doc.rows[g].value)) {
|
|
const obj = doc.rows[g].value;
|
|
|
|
if (obj && obj.common.enabled) {
|
|
if (obj.common.engineType.match(/^[cC]offee/)) {
|
|
count++;
|
|
coffeeCompiler.fromSource(obj.common.source, {
|
|
sourceMap: false,
|
|
bare: true
|
|
}, function (err, js) {
|
|
if (err) {
|
|
adapter.log.error('coffee compile ' + err);
|
|
return;
|
|
}
|
|
globalScript += js + '\n';
|
|
if (!--count) {
|
|
globalScriptLines = globalScript.split(/\r\n|\n|\r/g).length;
|
|
// load all scripts
|
|
for (let i = 0; i < doc.rows.length; i++) {
|
|
if (!checkIsGlobal(doc.rows[i].value)) {
|
|
load(doc.rows[i].value._id);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
} else if (obj.common.engineType.match(/^[tT]ype[sS]cript/)) {
|
|
// compile the current global script
|
|
const filename = scriptIdToTSFilename(obj._id);
|
|
const tsCompiled = tsServer.compile(filename, obj.common.source);
|
|
|
|
const errors = tsCompiled.diagnostics.map(diag => diag.annotatedSource + '\n').join('\n');
|
|
|
|
if (tsCompiled.success) {
|
|
if (errors.length > 0) {
|
|
adapter.log.warn('TypeScript compilation completed with errors: \n' + errors);
|
|
} else {
|
|
adapter.log.info('TypeScript compilation successful');
|
|
}
|
|
globalScript += tsCompiled.result + '\n';
|
|
|
|
// if declarations were generated, remember them
|
|
if (tsCompiled.declarations != null) {
|
|
provideDeclarationsForGlobalScript(obj._id, tsCompiled.declarations);
|
|
}
|
|
} else {
|
|
adapter.log.error('TypeScript compilation failed: \n' + errors);
|
|
}
|
|
} else { // javascript
|
|
const sourceCode = obj.common.source;
|
|
globalScript += sourceCode + '\n';
|
|
|
|
// try to compile the declarations so TypeScripts can use
|
|
// functions defined in global JavaScripts
|
|
const filename = scriptIdToTSFilename(obj._id);
|
|
const tsCompiled = jsDeclarationServer.compile(filename, sourceCode);
|
|
// if declarations were generated, remember them
|
|
if (tsCompiled.success && tsCompiled.declarations != null) {
|
|
provideDeclarationsForGlobalScript(obj._id, tsCompiled.declarations);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!count) {
|
|
globalScript = globalScript.replace(/\r\n/g, '\n');
|
|
globalScriptLines = globalScript.split(/\n/g).length - 1;
|
|
|
|
if (doc && doc.rows && doc.rows.length) {
|
|
// load all scripts
|
|
for (let i = 0; i < doc.rows.length; i++) {
|
|
if (!checkIsGlobal(doc.rows[i].value)) {
|
|
load(doc.rows[i].value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
message: (obj) => {
|
|
if (obj) {
|
|
switch (obj.command) {
|
|
case 'loadTypings': { // Load typings for the editor
|
|
const typings = {};
|
|
|
|
// try to load TypeScript lib files from disk
|
|
const libFiles = [
|
|
// This is lib.es2015.d.ts:
|
|
'lib.es5.d.ts',
|
|
'lib.es2015.core.d.ts',
|
|
'lib.es2015.collection.d.ts',
|
|
'lib.es2015.generator.d.ts',
|
|
'lib.es2015.promise.d.ts',
|
|
'lib.es2015.iterable.d.ts',
|
|
'lib.es2015.proxy.d.ts',
|
|
'lib.es2015.reflect.d.ts',
|
|
'lib.es2015.symbol.d.ts',
|
|
'lib.es2015.symbol.wellknown.d.ts'
|
|
];
|
|
for (const libFile of libFiles) {
|
|
try {
|
|
const libPath = require.resolve(`typescript/lib/${libFile}`);
|
|
typings[libFile] = nodeFS.readFileSync(libPath, 'utf8');
|
|
} catch (e) { /* ok, no lib then */ }
|
|
}
|
|
|
|
// try to load node.js typings from disk
|
|
try {
|
|
const nodeTypingsPath = require.resolve('@types/node/index.d.ts');
|
|
typings['node_modules/@types/node/index.d.ts'] = nodeFS.readFileSync(nodeTypingsPath, 'utf8');
|
|
} catch (e) { /* ok, no typings then */ }
|
|
|
|
// provide the already-loaded yunkong2 typings and global script declarations
|
|
Object.assign(typings, tsAmbient);
|
|
// also provide the known global declarations for each global script
|
|
for (const globalScriptPaths of Object.keys(knownGlobalDeclarationsByScript)) {
|
|
typings[globalScriptPaths + '.d.ts'] = knownGlobalDeclarationsByScript[globalScriptPaths];
|
|
}
|
|
|
|
if (obj.callback) {
|
|
adapter.sendTo(obj.from, obj.command, { typings }, obj.callback);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
function checkObjectsJson(file) {
|
|
if (mods.path.normalize(file).replace(/\\/g, '/').indexOf('-data/objects.json') !== -1) {
|
|
if (adapter) {
|
|
adapter.log.error('May not read ' + file);
|
|
} else {
|
|
console.error('May not read ' + file);
|
|
}
|
|
throw new Error('Permission denied');
|
|
}
|
|
}
|
|
|
|
mods.fs.readFile = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.readFile.apply(this, arguments);
|
|
};
|
|
mods.fs.readFileSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.readFileSync.apply(this, arguments);
|
|
};
|
|
mods.fs.writeFile = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.writeFile.apply(this, arguments);
|
|
};
|
|
mods.fs.writeFileSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.writeFileSync.apply(this, arguments);
|
|
};
|
|
mods.fs.unlink = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.unlink.apply(this, arguments);
|
|
};
|
|
mods.fs.unlinkSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.unlinkSync.apply(this, arguments);
|
|
};
|
|
mods.fs.appendFile = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.appendFile.apply(this, arguments);
|
|
};
|
|
mods.fs.appendFileSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.appendFileSync.apply(this, arguments);
|
|
};
|
|
mods.fs.chmod = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.chmod.apply(this, arguments);
|
|
};
|
|
mods.fs.chmodSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.chmodSync.apply(this, arguments);
|
|
};
|
|
mods.fs.chown = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.chmodSync.apply(this, arguments);
|
|
};
|
|
mods.fs.chownSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.chownSync.apply(this, arguments);
|
|
};
|
|
mods.fs.copyFile = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
checkObjectsJson(arguments[1]);
|
|
if (nodeFS.copyFile) {
|
|
return nodeFS.copyFile.apply(this, arguments);
|
|
} else {
|
|
const cb = arguments[2];
|
|
return nodeFS.readFile(arguments[0], (err, data) => {
|
|
if (err) {
|
|
cb && cb(err);
|
|
} else {
|
|
nodeFS.writeFile(arguments[1], data, err => cb && cb(err));
|
|
}
|
|
});
|
|
}
|
|
};
|
|
mods.fs.copyFileSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
checkObjectsJson(arguments[1]);
|
|
if (nodeFS.copyFileSync) {
|
|
return nodeFS.copyFileSync.apply(this, arguments);
|
|
} else {
|
|
return nodeFS.writeFileSync(arguments[1], nodeFS.readFileSync(arguments[0]));
|
|
}
|
|
};
|
|
mods.fs.open = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.open.apply(this, arguments);
|
|
};
|
|
mods.fs.openSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.openSync.apply(this, arguments);
|
|
};
|
|
mods.fs.rename = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
checkObjectsJson(arguments[1]);
|
|
return nodeFS.rename.apply(this, arguments);
|
|
};
|
|
mods.fs.renameSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
checkObjectsJson(arguments[1]);
|
|
return nodeFS.renameSync.apply(this, arguments);
|
|
};
|
|
mods.fs.truncate = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.truncate.apply(this, arguments);
|
|
};
|
|
mods.fs.truncateSync = function () {
|
|
checkObjectsJson(arguments[0]);
|
|
return nodeFS.truncateSync.apply(this, arguments);
|
|
};
|
|
|
|
context.adapter = adapter;
|
|
|
|
let attempts = {};
|
|
let globalScript = '';
|
|
/** Generated declarations for global TypeScripts */
|
|
let globalDeclarations = '';
|
|
// Remember which definitions the global scripts
|
|
// have access to, because it depends on the compile order
|
|
let knownGlobalDeclarationsByScript = {};
|
|
let globalScriptLines = 0;
|
|
// let activeRegEx = null;
|
|
let activeStr = '';
|
|
|
|
/**
|
|
* Redirects the virtual-tsc log output to the yunkong2 log
|
|
* @param {string} msg message
|
|
* @param {string} sev severity (info, silly, debug, warn, error)
|
|
*/
|
|
function tsLog(msg, sev) {
|
|
// shift the severities around, we don't care about the small details
|
|
if (sev == null || sev === 'info') sev = 'debug';
|
|
else if (sev === 'debug') sev = 'silly';
|
|
|
|
if (adapter && adapter.log) {
|
|
adapter.log[sev](msg);
|
|
} else {
|
|
console.log(`[${sev.toUpperCase()}] ${msg}`);
|
|
}
|
|
}
|
|
// compiler instance for typescript
|
|
tsServer = new tsc.Server(tsCompilerOptions, tsLog);
|
|
// compiler instance for global JS declarations
|
|
jsDeclarationServer = new tsc.Server(jsDeclarationCompilerOptions);
|
|
|
|
function addGetProperty(object) {
|
|
Object.defineProperty(object, 'get', {
|
|
value: function (id) {
|
|
return this[id] || this[adapter.namespace + '.' + id];
|
|
},
|
|
enumerable: false
|
|
});
|
|
}
|
|
|
|
function fixLineNo(line) {
|
|
if (line.indexOf('javascript.js:') >= 0) return line;
|
|
if (!/script[s]?\.js[.\\\/]/.test(line)) return line;
|
|
if (/:([\d]+):/.test(line)) {
|
|
line = line.replace(/:([\d]+):/, function ($0, $1) {
|
|
return ':' + ($1 > globalScriptLines ? $1 - globalScriptLines : $1) + ':';
|
|
});
|
|
} else {
|
|
line = line.replace(/:([\d]+)$/, function ($0, $1) {
|
|
return ':' + ($1 > globalScriptLines ? $1 - globalScriptLines : $1);
|
|
});
|
|
}
|
|
return line;
|
|
}
|
|
|
|
context.logError = function (msg, e, offs) {
|
|
const stack = e.stack.split('\n');
|
|
if (msg.indexOf('\n') < 0) {
|
|
msg = msg.replace(/[: ]*$/, ': ');
|
|
}
|
|
|
|
//errorLogFunction.error(msg + stack[0]);
|
|
context.errorLogFunction.error(msg + fixLineNo(stack[0]));
|
|
for (let i = offs || 1; i < stack.length; i++) {
|
|
if (!stack[i]) continue;
|
|
if (stack[i].match(/runInNewContext|javascript\.js:/)) break;
|
|
//adapter.log.error(fixLineNo(stack[i]));
|
|
context.errorLogFunction.error(fixLineNo(stack[i]));
|
|
}
|
|
};
|
|
|
|
function createActiveObject(id, enabled, cb) {
|
|
const idActive = adapter.namespace + '.scriptEnabled.' + id.substring('script.js.'.length);
|
|
|
|
if (!context.objects[idActive]) {
|
|
context.objects[idActive] = {
|
|
_id: idActive,
|
|
common: {
|
|
name: 'scriptEnabled.' + id.substring('script.js.'.length),
|
|
desc: 'controls script activity',
|
|
type: 'boolean',
|
|
role: 'switch.active',
|
|
expert: true
|
|
},
|
|
native: {
|
|
script: id
|
|
},
|
|
type: 'state'
|
|
};
|
|
adapter.setForeignObject(idActive, context.objects[idActive], err => {
|
|
if (!err) {
|
|
adapter.setForeignState(idActive, enabled, true, cb);
|
|
} else if (cb) {
|
|
cb();
|
|
}
|
|
});
|
|
} else {
|
|
adapter.getForeignState(idActive, (err, state) => {
|
|
if (state && state.val !== enabled) {
|
|
adapter.setForeignState(idActive, enabled, true, cb);
|
|
} else if (cb) {
|
|
cb();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function addToNames(obj) {
|
|
const id = obj._id;
|
|
if (obj.common && obj.common.name) {
|
|
const name = obj.common.name;
|
|
if (typeof name !== 'string') return;
|
|
|
|
if (!context.names[name]) {
|
|
context.names[name] = id;
|
|
} else {
|
|
if (typeof context.names[name] === 'string') {
|
|
context.names[name] = [context.names[name]];
|
|
}
|
|
context.names[name].push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeFromNames(id) {
|
|
const n = getName(id);
|
|
|
|
if (n) {
|
|
let pos;
|
|
if (context.names[n] === 'object') {
|
|
pos = context.names[n].indexOf(id);
|
|
if (pos !== -1) {
|
|
context.names[n].splice(pos, 1);
|
|
if (context.names[n].length) {
|
|
context.names[n] = context.names[n][0];
|
|
}
|
|
}
|
|
} else {
|
|
delete context.names[n];
|
|
}
|
|
}
|
|
}
|
|
|
|
function getName(id) {
|
|
let pos;
|
|
for (const n in context.names) {
|
|
if (context.names.hasOwnProperty(n)) {
|
|
if (context.names[n] && typeof context.names[n] === 'object') {
|
|
pos = context.names[n].indexOf(id);
|
|
if (pos !== -1) return n;
|
|
} else if (context.names[n] === id) {
|
|
return n;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function installNpm(npmLib, callback) {
|
|
const path = __dirname;
|
|
if (typeof npmLib === 'function') {
|
|
callback = npmLib;
|
|
npmLib = undefined;
|
|
}
|
|
|
|
const cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '"';
|
|
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 child = mods['child_process'].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 (typeof callback === 'function') callback(npmLib);
|
|
});
|
|
}
|
|
|
|
function installLibraries(callback) {
|
|
let allInstalled = true;
|
|
if (adapter.config && adapter.config.libraries) {
|
|
const libraries = adapter.config.libraries.split(/[,;\s]+/);
|
|
|
|
for (let lib = 0; lib < libraries.length; lib++) {
|
|
if (libraries[lib] && libraries[lib].trim()) {
|
|
libraries[lib] = libraries[lib].trim();
|
|
if (!nodeFS.existsSync(__dirname + '/node_modules/' + libraries[lib] + '/package.json')) {
|
|
|
|
if (!attempts[libraries[lib]]) {
|
|
attempts[libraries[lib]] = 1;
|
|
} else {
|
|
attempts[libraries[lib]]++;
|
|
}
|
|
if (attempts[libraries[lib]] > 3) {
|
|
adapter.log.error('Cannot install npm packet: ' + libraries[lib]);
|
|
continue;
|
|
}
|
|
|
|
installNpm(libraries[lib], function () {
|
|
installLibraries(callback);
|
|
});
|
|
allInstalled = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (allInstalled) callback();
|
|
}
|
|
|
|
function compile(source, name) {
|
|
source += "\n;\nlog('registered ' + __engine.__subscriptions + ' subscription' + (__engine.__subscriptions === 1 ? '' : 's' ) + ' and ' + __engine.__schedules + ' schedule' + (__engine.__schedules === 1 ? '' : 's' ));\n";
|
|
try {
|
|
if (VMScript) {
|
|
return {
|
|
script: new VMScript(source, name)
|
|
};
|
|
} else {
|
|
const options = {
|
|
filename: name,
|
|
displayErrors: true
|
|
//lineOffset: globalScriptLines
|
|
};
|
|
return {
|
|
script: vm.createScript(source, options)
|
|
};
|
|
}
|
|
} catch (e) {
|
|
context.logError(name + ' compile failed:\r\nat ', e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function execute(script, name, verbose, debug) {
|
|
script.intervals = [];
|
|
script.timeouts = [];
|
|
script.schedules = [];
|
|
script.name = name;
|
|
script._id = Math.floor(Math.random() * 0xFFFFFFFF);
|
|
script.subscribes = {};
|
|
|
|
const sandbox = sandBox(script, name, verbose, debug, context);
|
|
|
|
if (NodeVM) {
|
|
const vm = new NodeVM({
|
|
sandbox,
|
|
require: {
|
|
external: true,
|
|
builtin: ['*'],
|
|
root: '',
|
|
mock: mods
|
|
}
|
|
});
|
|
|
|
try {
|
|
vm.run(script.script, name);
|
|
} catch (e) {
|
|
context.logError(name, e);
|
|
}
|
|
} else {
|
|
try {
|
|
script.script.runInNewContext(sandbox, {
|
|
filename: name,
|
|
displayErrors: true
|
|
//lineOffset: globalScriptLines
|
|
});
|
|
} catch (e) {
|
|
context.logError(name, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function unsubscribe(id) {
|
|
if (!id) {
|
|
adapter.log.warn('unsubscribe: empty name');
|
|
return;
|
|
}
|
|
|
|
if (typeof id === 'object' && id && id.constructor && id.constructor.name === 'RegExp') {
|
|
//adapter.log.warn('unsubscribe: todo - process regexp');
|
|
return;
|
|
}
|
|
|
|
if (typeof id !== 'string') {
|
|
adapter.log.error('unsubscribe: invalid type of id - ' + typeof id);
|
|
return;
|
|
}
|
|
const parts = id.split('.');
|
|
const _adapter = 'system.adapter.' + parts[0] + '.' + parts[1];
|
|
if (context.objects[_adapter] && context.objects[_adapter].common && context.objects[_adapter].common.subscribable) {
|
|
const a = parts[0] + '.' + parts[1];
|
|
const alive = 'system.adapter.' + a + '.alive';
|
|
if (context.adapterSubs[alive]) {
|
|
const pos = context.adapterSubs[alive].indexOf(id);
|
|
if (pos !== -1) context.adapterSubs[alive].splice(pos, 1);
|
|
if (!context.adapterSubs[alive].length) delete context.adapterSubs[alive];
|
|
}
|
|
adapter.sendTo(a, 'unsubscribe', id);
|
|
}
|
|
}
|
|
|
|
function stop(name, callback) {
|
|
adapter.log.info('Stop script ' + name);
|
|
|
|
adapter.setState('scriptEnabled.' + name.substring('script.js.'.length), false, true);
|
|
|
|
if (context.scripts[name]) {
|
|
// Remove from subscriptions
|
|
context.isEnums = false;
|
|
if (adapter.config.subscribe) {
|
|
// check all subscribed IDs
|
|
for (const id in context.scripts[name].subscribes) {
|
|
if (!context.scripts[name].subscribes.hasOwnProperty(id)) continue;
|
|
if (context.subscribedPatterns[id]) {
|
|
context.subscribedPatterns[id] -= context.scripts[name].subscribes[id];
|
|
if (context.subscribedPatterns[id] <= 0) {
|
|
adapter.unsubscribeForeignStates(id);
|
|
delete context.subscribedPatterns[id];
|
|
if (context.states[id]) delete context.states[id];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let i = context.subscriptions.length - 1; i >= 0; i--) {
|
|
if (context.subscriptions[i].name === name) {
|
|
const sub = context.subscriptions.splice(i, 1)[0];
|
|
if (sub) {
|
|
unsubscribe(sub.pattern.id);
|
|
}
|
|
} else {
|
|
if (!context.isEnums && context.subscriptions[i].pattern.enumName || context.subscriptions[i].pattern.enumId) context.isEnums = true;
|
|
}
|
|
}
|
|
|
|
// Stop all timeouts
|
|
for (let i = 0; i < context.scripts[name].timeouts.length; i++) {
|
|
clearTimeout(context.scripts[name].timeouts[i]);
|
|
}
|
|
// Stop all intervals
|
|
for (let i = 0; i < context.scripts[name].intervals.length; i++) {
|
|
clearInterval(context.scripts[name].intervals[i]);
|
|
}
|
|
// Stop all scheduled jobs
|
|
for (let i = 0; i < context.scripts[name].schedules.length; i++) {
|
|
if (context.scripts[name].schedules[i]) {
|
|
const _name = context.scripts[name].schedules[i].name;
|
|
if (!nodeSchedule.cancelJob(context.scripts[name].schedules[i])) {
|
|
adapter.log.error('Error by canceling scheduled job "' + _name + '"');
|
|
}
|
|
}
|
|
}
|
|
|
|
// if callback for on stop
|
|
if (typeof context.scripts[name].onStopCb === 'function') {
|
|
context.scripts[name].onStopTimeout = parseInt(context.scripts[name].onStopTimeout, 10) || 1000;
|
|
|
|
let timeout = setTimeout(function () {
|
|
if (timeout) {
|
|
timeout = null;
|
|
delete context.scripts[name];
|
|
if (typeof callback === 'function') callback(true, name);
|
|
}
|
|
}, context.scripts[name].onStopTimeout);
|
|
|
|
try {
|
|
context.scripts[name].onStopCb(function () {
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
timeout = null;
|
|
delete context.scripts[name];
|
|
if (typeof callback === 'function') callback(true, name);
|
|
}
|
|
});
|
|
} catch (e) {
|
|
adapter.log.error('error in onStop callback: ' + e);
|
|
}
|
|
|
|
} else {
|
|
delete context.scripts[name];
|
|
if (typeof callback === 'function') callback(true, name);
|
|
}
|
|
} else {
|
|
if (typeof callback === 'function') callback(false, name);
|
|
}
|
|
}
|
|
|
|
function prepareScript(obj, callback) {
|
|
if (obj &&
|
|
obj.common.enabled &&
|
|
obj.common.engine === 'system.adapter.' + adapter.namespace &&
|
|
obj.common.source) {
|
|
const name = obj._id;
|
|
|
|
adapter.setState('scriptEnabled.' + name.substring('script.js.'.length), true, true);
|
|
|
|
if ((obj.common.engineType.match(/^[jJ]ava[sS]cript/) || obj.common.engineType === 'Blockly')) {
|
|
// Javascript
|
|
adapter.log.info('Start javascript ' + name);
|
|
|
|
let sourceFn = name;
|
|
if (webstormDebug) {
|
|
const fn = name.replace(/^script.js./, '').replace(/\./g, '/');
|
|
sourceFn = mods.path.join(webstormDebug, fn + '.js');
|
|
}
|
|
context.scripts[name] = compile(globalScript + obj.common.source, sourceFn);
|
|
if (context.scripts[name]) execute(context.scripts[name], sourceFn, obj.common.verbose, obj.common.debug);
|
|
if (typeof callback === 'function') callback(true, name);
|
|
} else if (obj.common.engineType.match(/^[cC]offee/)) {
|
|
// CoffeeScript
|
|
coffeeCompiler.fromSource(obj.common.source, { sourceMap: false, bare: true }, function (err, js) {
|
|
if (err) {
|
|
adapter.log.error(name + ' coffee compile ' + err);
|
|
if (typeof callback === 'function') callback(false, name);
|
|
return;
|
|
}
|
|
adapter.log.info('Start coffescript ' + name);
|
|
context.scripts[name] = compile(globalScript + '\n' + js, name);
|
|
if (context.scripts[name]) execute(context.scripts[name], name, obj.common.verbose, obj.common.debug);
|
|
if (typeof callback === 'function') callback(true, name);
|
|
});
|
|
} else if (obj.common.engineType.match(/^[tT]ype[sS]cript/)) {
|
|
// TypeScript
|
|
adapter.log.info(name + ': compiling TypeScript source...');
|
|
const filename = scriptIdToTSFilename(name);
|
|
const tsCompiled = tsServer.compile(filename, obj.common.source);
|
|
|
|
const errors = tsCompiled.diagnostics.map(function (diag) {
|
|
return diag.annotatedSource + '\n';
|
|
}).join('\n');
|
|
|
|
if (tsCompiled.success) {
|
|
if (errors.length > 0) {
|
|
adapter.log.warn(name + ': TypeScript compilation had errors: \n' + errors);
|
|
} else {
|
|
adapter.log.info(name + ': TypeScript compilation successful');
|
|
}
|
|
context.scripts[name] = compile(globalScript + '\n' + tsCompiled.result, name);
|
|
if (context.scripts[name]) execute(context.scripts[name], name, obj.common.verbose, obj.common.debug);
|
|
if (typeof callback === 'function') callback(true, name);
|
|
} else {
|
|
adapter.log.error(name + ': TypeScript compilation failed: \n' + errors);
|
|
}
|
|
}
|
|
} else {
|
|
let _name;
|
|
if (obj && obj._id) {
|
|
_name = obj._id;
|
|
adapter.setState('scriptEnabled.' + _name.substring('script.js.'.length), false, true);
|
|
}
|
|
if (!obj) adapter.log.error('Invalid script');
|
|
if (typeof callback === 'function') callback(false, _name);
|
|
}
|
|
}
|
|
|
|
function load(nameOrObject, callback) {
|
|
if (typeof nameOrObject === 'object') {
|
|
// create states for scripts
|
|
createActiveObject(nameOrObject._id, nameOrObject && nameOrObject.common && nameOrObject.common.enabled, () => {
|
|
return prepareScript(nameOrObject, callback);
|
|
});
|
|
} else {
|
|
adapter.getForeignObject(nameOrObject, function (err, obj) {
|
|
if (!obj || err) {
|
|
if (err) adapter.log.error('Invalid script "' + nameOrObject + '": ' + err);
|
|
if (typeof callback === 'function') callback(false, nameOrObject);
|
|
} else {
|
|
return load(obj, callback);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function patternMatching(event, patternFunctions) {
|
|
let matched = false;
|
|
for (let i = 0, len = patternFunctions.length; i < len; i++) {
|
|
if (patternFunctions[i](event)) {
|
|
if (patternFunctions.logic === 'or') return true;
|
|
|
|
matched = true;
|
|
} else {
|
|
if (patternFunctions.logic === 'and') return false;
|
|
}
|
|
}
|
|
return matched;
|
|
}
|
|
|
|
function getData(callback) {
|
|
let statesReady;
|
|
let objectsReady;
|
|
adapter.log.info('requesting all states');
|
|
adapter.getForeignStates('*', function (err, res) {
|
|
if (!adapter.config.subscribe) {
|
|
context.states = res;
|
|
}
|
|
|
|
addGetProperty(context.states);
|
|
|
|
// remember all IDs
|
|
for (const id in res) {
|
|
if (res.hasOwnProperty(id)) {
|
|
context.stateIds.push(id);
|
|
}
|
|
}
|
|
statesReady = true;
|
|
adapter.log.info('received all states');
|
|
if (objectsReady && typeof callback === 'function') callback();
|
|
});
|
|
|
|
adapter.log.info('requesting all objects');
|
|
|
|
adapter.objects.getObjectList({include_docs: true}, (err, res) => {
|
|
res = res.rows;
|
|
context.objects = {};
|
|
for (let i = 0; i < res.length; i++) {
|
|
context.objects[res[i].doc._id] = res[i].doc;
|
|
if (res[i].doc.type === 'enum') context.enums.push(res[i].doc._id);
|
|
|
|
// Collect all names
|
|
addToNames(context.objects[res[i].doc._id]);
|
|
}
|
|
addGetProperty(context.objects);
|
|
|
|
// set language for debug messages
|
|
if (context.objects['system.config'] && context.objects['system.config'].common.language) words.setLanguage(context.objects['system.config'].common.language);
|
|
|
|
// try to use system coordinates
|
|
if (adapter.config.useSystemGPS && context.objects['system.config'] &&
|
|
context.objects['system.config'].common.latitude) {
|
|
adapter.config.latitude = context.objects['system.config'].common.latitude;
|
|
adapter.config.longitude = context.objects['system.config'].common.longitude;
|
|
}
|
|
adapter.config.latitude = parseFloat(adapter.config.latitude);
|
|
adapter.config.longitude = parseFloat(adapter.config.longitude);
|
|
|
|
objectsReady = true;
|
|
adapter.log.info('received all objects');
|
|
if (statesReady && typeof callback === 'function') callback();
|
|
});
|
|
}
|
|
|