UNPKG

iobroker.javascript

Version:
1,128 lines (1,015 loc) 108 kB
/* * Javascript adapter * * The MIT License (MIT) * * Copyright (c) 2014-2024 bluefox <dogafox@gmail.com>, * * Copyright (c) 2014 hobbyquaker */ /* jshint -W097 */ /* jshint -W083 */ /* jshint strict: false */ /* jslint node: true */ /* jshint shadow: true */ 'use strict'; const vm = require('node:vm'); const nodeFS = require('node:fs'); const nodePath = require('node:path'); const tsc = require('virtual-tsc'); const Mirror = require('./lib/mirror'); const fork = require('child_process').fork; const mods = { fs: {}, dgram: require('node:dgram'), crypto: require('node:crypto'), dns: require('node:dns'), events: require('node:events'), http: require('node:http'), https: require('node:https'), http2: require('node:http2'), net: require('node:net'), os: require('node:os'), path: require('node:path'), util: require('node:util'), child_process: require('node:child_process'), stream: require('node:stream'), zlib: require('node:zlib'), suncalc: require('suncalc2'), axios: require('axios'), wake_on_lan: require('wake_on_lan'), nodeSchedule: require('node-schedule') }; /** * List of forbidden Locations for a mirror directory * relative to the default data directory * ATTENTION: the same list is also located in index_m.html!! * @type {*[]} */ const forbiddenMirrorLocations = [ 'backup-objects', 'files', 'backitup', '../backups', '../node_modules', '../log' ]; const utils = require('@iobroker/adapter-core'); const words = require('./lib/words'); const sandBox = require('./lib/sandbox'); const eventObj = require('./lib/eventObj'); const Scheduler = require('./lib/scheduler'); const { targetTsLib, tsCompilerOptions, jsDeclarationCompilerOptions } = require('./lib/typescriptSettings'); const { hashSource, isObject } = require('./lib/tools'); const { isDeepStrictEqual } = require('node:util'); const { resolveTypescriptLibs, resolveTypings, scriptIdToTSFilename, transformScriptBeforeCompilation, transformGlobalDeclarations } = require('./lib/typescriptTools'); const packageJson = require('./package.json'); const SCRIPT_CODE_MARKER = 'script.js.'; const stopCounters = {}; let setStateCountCheckInterval = null; let webstormDebug; let debugMode; if (process.argv) { for (let a = 1; a < process.argv.length; a++) { if (process.argv[a].startsWith('--webstorm')) { webstormDebug = process.argv[a].replace(/^(.*?=\s*)/, ''); } if (process.argv[a] === '--debugScript') { if (!process.argv[a + 1]) { console.log('No script name provided'); process.exit(300); } else { debugMode = process.argv[a + 1]; } } } } const isCI = !!process.env.CI; // ambient declarations for typescript /** @type {Record<string, string>} */ let tsAmbient; // TypeScript's scripts are only recompiled if their source hash changes. If an adapter update fixes compilation bugs, // a user won't notice until he changes and re-saves the script. To avoid that, we also include the // adapter version and TypeScript version in the hash const tsSourceHashBase = `versions:adapter=${packageJson.version},typescript=${packageJson.dependencies.typescript}`; let mirror; /** @type {boolean} if logs are subscribed or not */ let logSubscribed; /** * @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 }); } // taken from here: https://stackoverflow.com/questions/11887934/how-to-check-if-dst-daylight-saving-time-is-in-effect-and-if-so-the-offset function dstOffsetAtDate(dateInput) { const fullYear = dateInput.getFullYear() | 0; // "Leap Years are any year that can be exactly divided by 4 (2012, 2016, etc) // except if it can be exactly divided by 100, then it isn't (2100, 2200, etc) // except if it can be exactly divided by 400, then it is (2000, 2400)" // (https://www.mathsisfun.com/leap-years.html). const isLeapYear = ((fullYear & 3) | (fullYear/100 & 3)) === 0 ? 1 : 0; // (fullYear & 3) = (fullYear % 4), but faster //Alternative:var isLeapYear=(new Date(currentYear,1,29,12)).getDate()===29?1:0 const fullMonth = dateInput.getMonth() | 0; return ( // 1. We know what the time since the Epoch really is (+dateInput) // same as the dateInput.getTime() method // 2. We know what the time since the Epoch at the start of the year is - (+new Date(fullYear, 0)) // day defaults to 1 if not explicitly zeroed // 3. Now, subtract what we would expect the time to be if daylight savings // did not exist. This yields the time-offset due to daylight savings. - (( (( // Calculate the day of the year in the Gregorian calendar // The code below works based upon the facts of signed right shifts // • (x) >> n: shifts n and fills in the n highest bits with 0s // • (-x) >> n: shifts n and fills in the n highest bits with 1s // (This assumes that x is a positive integer) -1 + // first day in the year is day 1 (31 & ((-fullMonth) >> 4)) + // January // (-11)>>4 = -1 ((28 + isLeapYear) & ((1 - fullMonth) >> 4)) + // February (31 & ((2 - fullMonth) >> 4)) + // March (30 & ((3 - fullMonth) >> 4)) + // April (31 & ((4 - fullMonth) >> 4)) + // May (30 & ((5 - fullMonth) >> 4)) + // June (31 & ((6 - fullMonth) >> 4)) + // July (31 & ((7 - fullMonth) >> 4)) + // August (30 & ((8 - fullMonth) >> 4)) + // September (31 & ((9 - fullMonth) >> 4)) + // October (30 & ((10 - fullMonth) >> 4)) + // November // There are no months past December: the year rolls into the next. // Thus, fullMonth is 0-based, so it will never be 12 in Javascript (dateInput.getDate() | 0) // get day of the month ) & 0xffff) * 24 * 60 // 24 hours in a day, 60 minutes in an hour + (dateInput.getHours() & 0xff) * 60 // 60 minutes in an hour + (dateInput.getMinutes() & 0xff) )|0) * 60 * 1000 // 60 seconds in a minute * 1000 milliseconds in a second - (dateInput.getSeconds() & 0xff) * 1000 // 1000 milliseconds in a second - dateInput.getMilliseconds() ); } function loadTypeScriptDeclarations() { // try to load the typings on disk for all 3rd party modules const packages = [ 'node', // this provides auto-completion for most builtins 'request', // preloaded by the adapter ]; // Also include user-selected libraries (but only those that are also installed) if ( adapter.config && typeof adapter.config.libraries === 'string' && typeof adapter.config.libraryTypings === 'string' ) { const installedLibs = adapter.config.libraries .split(/[,;\s]+/) .map((s) => s.trim().split('@')[0]) .filter((s) => !!s); const wantsTypings = adapter.config.libraryTypings.split(/[,;\s]+/).map(s => s.trim()).filter(s => !!s); // Add all installed libraries the user has requested typings for to the list of packages for (const lib of installedLibs) { if (wantsTypings.includes(lib) && !packages.includes(lib)) { packages.push(lib); } } // Some packages have sub-modules (e.g. rxjs/operators) that are not exposed through the main entry point // If typings are requested for them, also add them if the base module is installed for (const lib of wantsTypings) { // Extract the package name and check if we need to add it if (!lib.includes('/')) { continue; } const pkgName = lib.substr(0, lib.indexOf('/')); if (installedLibs.includes(pkgName) && !packages.includes(lib)) { packages.push(lib); } } } for (const pkg of packages) { let pkgTypings = resolveTypings( pkg, // node needs ambient typings, so we don't wrap it in declare module pkg !== 'node' ); if (!pkgTypings) { // Create empty dummy declarations so users don't get the "not found" error // for installed packages pkgTypings = { [`node_modules/@types/${pkg}/index.d.ts`]: `declare module "${pkg}";`, }; } adapter.log.debug(`Loaded TypeScript definitions for ${pkg}: ${JSON.stringify(Object.keys(pkgTypings))}`); // remember the declarations for the editor Object.assign(tsAmbient, pkgTypings); // and give the language servers access to them tsServer.provideAmbientDeclarations(pkgTypings); jsDeclarationServer.provideAmbientDeclarations(pkgTypings); } } const context = { mods, objects: {}, states: {}, interimStateValues: {}, stateIds: [], errorLogFunction: null, subscriptions: [], subscriptionsFile: [], subscriptionsObject: [], subscribedPatterns: {}, subscribedPatternsFile: {}, adapterSubs: {}, cacheObjectEnums: {}, isEnums: false, // If some subscription wants enum channels: null, devices: null, logWithLineInfo: null, scheduler: null, timers: {}, enums: [], timerId: 0, names: {}, scripts: {}, messageBusHandlers: {}, logSubscriptions: {}, folderCreationVerifiedObjects: {}, updateLogSubscriptions, convertBackStringifiedValues, updateObjectContext, prepareStateObject, debugMode, timeSettings: { format12: false, leadingZeros: true }, rulesOpened: null, //opened rules getAbsoluteDefaultDataDir: utils.getAbsoluteDefaultDataDir, }; const regExGlobalOld = /_global$/; const regExGlobalNew = /script\.js\.global\./; function checkIsGlobal(obj) { return obj && obj.common && (regExGlobalOld.test(obj.common.name) || regExGlobalNew.test(obj._id)); } function convertBackStringifiedValues(id, state) { if (state && typeof state.val === 'string' && context.objects[id] && context.objects[id].common && (context.objects[id].common.type === 'array' || context.objects[id].common.type === 'object')) { try { state.val = JSON.parse(state.val); } catch (err) { if (id.startsWith('javascript.') || id.startsWith('0_userdata.0')) { adapter.log.info(`Could not parse value for id ${id} into ${context.objects[id].common.type}: ${err.message}`); } else { adapter.log.debug(`Could not parse value for id ${id} into ${context.objects[id].common.type}: ${err.message}`); } } } return state; } function prepareStateObject(id, state, isAck) { if (state === null) { state = {val: null}; } if (isAck === true || isAck === false || isAck === 'true' || isAck === 'false') { if (isObject(state) && state.val !== undefined) { // we assume that we were given a state object if // state is an object that contains a `val` property state.ack = isAck; } else { // otherwise assume that the given state is the value to be set state = {val: state, ack: isAck}; } } if (adapter.config.subscribe) { return state; } // set other values to have a full state object // mirrors logic from statesInRedis if (state.ts === undefined) { state.ts = Date.now(); } if (state.q === undefined) { state.q = 0; } state.from = typeof state.from === 'string' && state.from !== '' ? state.from : `system.adapter.${adapter.namespace}`; if (state.lc === undefined) { const formerStateValue = context.interimStateValues[id] || context.states[id]; if (!formerStateValue) { state.lc = state.ts; } else { // isDeepStrictEqual works on objects and primitive values const hasChanged = !isDeepStrictEqual(formerStateValue.val, state.val); if (!formerStateValue.lc || hasChanged) { state.lc = state.ts; } else { state.lc = formerStateValue.lc; } } } return state; } function fileMatching(sub, id, fileName) { if (sub.idRegEx) { if (!sub.idRegEx.test(id)) { return false; } } else { if (sub.id !== id) { return false; } } if (sub.fileRegEx) { if (!sub.fileRegEx.test(fileName)) { return false; } } else { if (sub.fileNamePattern !== fileName) { return false; } } return true; } /** * @type {Set<string>} * Stores the IDs of script objects whose change should be ignored because * the compiled source was just updated */ const ignoreObjectChange = new Set(); let objectsInitDone = false; let statesInitDone = false; /** @type {ioBroker.Adapter} */ let adapter; function startAdapter(options) { options = options || {}; Object.assign(options, { name: 'javascript', useFormatDate: true, // load float formatting /** * @param id { string } * @param obj { ioBroker.Object } */ objectChange: (id, obj) => { // Check if we should ignore this change (once!) because we just updated the compiled sources if (ignoreObjectChange.has(id)) { // Update the cached script object and do nothing more context.objects[id] = obj; ignoreObjectChange.delete(id); return; } // When still in initializing: already remember current values, // but data structures are initialized elsewhere if (!objectsInitDone) { if (obj) { context.objects[id] = obj; } return; } if (id.startsWith('enum.')) { // clear cache context.cacheObjectEnums = {}; // update context.enums array if (obj) { // If new if (!context.enums.includes(id)) { context.enums.push(id); context.enums.sort(); } } else { const pos = context.enums.indexOf(id); // if deleted if (pos !== -1) { context.enums.splice(pos, 1); } } } if (obj && id === 'system.config') { // set language for debug messages if (obj.common && obj.common.language) { words.setLanguage(obj.common.language); } } // update stored time format for variables.dayTime if (id === adapter.namespace + '.variables.dayTime' && obj && obj.native) { context.timeSettings.format12 = obj.native.format12 || false; context.timeSettings.leadingZeros = obj.native.leadingZeros === undefined ? true : obj.native.leadingZeros; } // send changes to disk mirror mirror && mirror.onObjectChange(id, obj); const formerObj = context.objects[id]; updateObjectContext(id, obj); // Update all Meta object data // for alias object changes on state objects we need to manually update the // state cache value because new value is only published on next change if (obj && obj.type === 'state' && id.startsWith('alias.0.')) { adapter.getForeignState(id, (err, state) => { if (err) { return; } if (state) { context.states[id] = state; } else if (context.states[id] !== undefined) { delete context.states[id]; } }); } context.subscriptionsObject.forEach(sub => { // ToDo: implement comparing with id.0.* too if (sub.pattern === id) { try { sub.callback(id, obj); } catch (err) { adapter.log.error(`Error in callback: ${err}`); } } }); // handle Script object updates if (!obj && formerObj && formerObj.type === 'script') { // Object Deleted just now if (checkIsGlobal(formerObj)) { // it was a global Script, and it was enabled and is now deleted => restart adapter if (formerObj.enabled) { adapter.log.info(`Active global Script ${id} deleted. Restart instance.`); adapter.restart(); } } else if (formerObj.common && formerObj.common.engine === `system.adapter.${adapter.namespace}`) { // It was a non-global Script and deleted => stop and remove it stop(id); // delete scriptEnabled.blabla variable const idActive = 'scriptEnabled.' + id.substring(SCRIPT_CODE_MARKER.length); adapter.delObject(idActive); adapter.delState(idActive); // delete scriptProblem.blabla variable const idProblem = 'scriptProblem.' + id.substring(SCRIPT_CODE_MARKER.length); adapter.delObject(idProblem); adapter.delState(idProblem); } } else if (!formerObj && obj && obj.type === 'script') { // New script that does not exist before if (checkIsGlobal(obj)) { // new global script added => restart adapter if (obj.common.enabled) { adapter.log.info(`Active global Script ${id} created. Restart instance.`); adapter.restart(); } } else if (obj.common && obj.common.engine === `system.adapter.${adapter.namespace}`) { // new non-global script - create states for scripts createActiveObject(id, obj.common.enabled, () => createProblemObject(id)); if (obj.common.enabled) { // if enabled => Start script load(id); } } } else if (obj && obj.type === 'script' && formerObj && formerObj.common) { // Script changed ... if (checkIsGlobal(obj)) { if (obj.common.enabled || formerObj.common.enabled) { adapter.log.info(`Global Script ${id} updated. Restart instance.`); adapter.restart(); } } else { // No global script if (obj.common && obj.common.engine === 'system.adapter.' + adapter.namespace) { // create states for scripts createActiveObject(id, obj.common.enabled, () => createProblemObject(id)); } if ((formerObj.common.enabled && !obj.common.enabled) || (formerObj.common.engine === 'system.adapter.' + adapter.namespace && obj.common.engine !== 'system.adapter.' + adapter.namespace)) { // Script disabled if (formerObj.common.enabled && formerObj.common.engine === 'system.adapter.' + adapter.namespace) { // Remove it from executing stop(id); } } else if ((!formerObj.common.enabled && obj.common.enabled) || (formerObj.common.engine !== 'system.adapter.' + adapter.namespace && obj.common.engine === 'system.adapter.' + adapter.namespace)) { // Script enabled if (obj.common.enabled && obj.common.engine === 'system.adapter.' + adapter.namespace) { // Start script load(id); } } else { //if (obj.common.source !== formerObj.common.source) { // Source changed => restart it stopCounters[id] = stopCounters[id] ? stopCounters[id] + 1 : 1; stop(id, (res, _id) => // only start again after stop when "last" object change to prevent problems on // multiple changes in fast frequency !--stopCounters[id] && load(_id)); } } } }, stateChange: (id, state) => { if (context.interimStateValues[id] !== undefined) { // any update invalidates the remembered interim value delete context.interimStateValues[id]; } if (!id || id.startsWith('messagebox.') || id.startsWith('log.')) { return; } if (id === `${adapter.namespace}.debug.to` && state && !state.ack) { return !debugMode && debugSendToInspector(state.val); } // When still in initializing: already remember current values, // but data structures are initialized elsewhere if (!statesInitDone) { if (state) { context.states[id] = state; } 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.includes(id)) { 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, context.convertBackStringifiedValues(id, state), context.convertBackStringifiedValues(id, 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)) { try { sub.callback(_eventObj); } catch (err) { adapter.log.error(`Error in callback: ${err}`); } } } }, fileChange: (id, fileName, size) => { // if this file matches any subscriptions for (let i = 0, l = context.subscriptionsFile.length; i < l; i++) { const sub = context.subscriptionsFile[i]; if (sub && fileMatching(sub, id, fileName)) { try { sub.callback(id, fileName, size, sub.withFile); } catch (err) { adapter.log.error(`Error in callback: ${err}`); } } } }, unload: callback => { debugStop() .then(() => { stopTimeSchedules(); setStateCountCheckInterval && clearInterval(setStateCountCheckInterval); stopAllScripts(callback); }); }, ready: () => { adapter.config.maxSetStatePerMinute = parseInt(adapter.config.maxSetStatePerMinute, 10) || 1000; adapter.config.maxTriggersPerScript = parseInt(adapter.config.maxTriggersPerScript, 10) || 100; if (adapter.supportsFeature && adapter.supportsFeature('PLUGINS')) { const sentryInstance = adapter.getPluginInstance('sentry'); if (sentryInstance) { const Sentry = sentryInstance.getSentryObject(); if (Sentry) { Sentry.configureScope(scope => { scope.addEventProcessor((event, _hint) => { if (event.exception && event.exception.values && event.exception.values[0]) { const eventData = event.exception.values[0]; if (eventData.stacktrace && eventData.stacktrace.frames && Array.isArray(eventData.stacktrace.frames) && eventData.stacktrace.frames.length) { // Exclude event if script Marker is included if (eventData.stacktrace.frames.find(frame => frame.filename && frame.filename.includes(SCRIPT_CODE_MARKER))) { return null; } //Exclude event if own directory is included but not inside own node_modules const ownNodeModulesDir = nodePath.join(__dirname, 'node_modules'); if (!eventData.stacktrace.frames.find(frame => frame.filename && frame.filename.includes(__dirname) && !frame.filename.includes(ownNodeModulesDir))) { return null; } // We have exception data and do not sorted it out, so report it return event; } } // No exception in it ... do not report return null; }); main(); }); } else { main(); } } else { main(); } } else { main(); } }, message: obj => { if (obj) { switch (obj.command) { // process messageTo commands case 'toScript': case 'jsMessageBus': if (obj.message && ( obj.message.instance === null || obj.message.instance === undefined || ('javascript.' + obj.message.instance === adapter.namespace) || (obj.message.instance === adapter.namespace) )) { Object.keys(context.messageBusHandlers).forEach(name => { // script name could be script.js.xxx or only xxx if ((!obj.message.script || obj.message.script === name) && context.messageBusHandlers[name][obj.message.message]) { context.messageBusHandlers[name][obj.message.message].forEach(handler => { try { if (obj.callback) { handler.cb.call(handler.sandbox, obj.message.data, result => adapter.sendTo(obj.from, obj.command, result, obj.callback)); } else { handler.cb.call(handler.sandbox, obj.message.data, result => {/* nop */ }); } } catch (e) { adapter.setState('scriptProblem.' + name.substring(SCRIPT_CODE_MARKER.length), true, true); context.logError('Error in callback', e); } }); } }); } break; case 'loadTypings': { // Load typings for the editor const typings = {}; // try to load TypeScript lib files from disk try { const typescriptLibs = resolveTypescriptLibs(targetTsLib); Object.assign(typings, typescriptLibs); } catch (e) { /* ok, no lib then */ } // provide the already-loaded ioBroker 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; } case 'calcAstroAll': { if (obj.message) { const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined ? adapter.config.sunriseOffset : obj.message.sunriseOffset, 10) || 0; const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined ? adapter.config.sunsetOffset : obj.message.sunsetOffset, 10) || 0; const longitude = parseFloat(obj.message.longitude === undefined ? adapter.config.longitude : obj.message.longitude) || 0; const latitude = parseFloat(obj.message.latitude === undefined ? adapter.config.latitude : obj.message.latitude) || 0; const today = getAstroStartOfDay(); let astroEvents = {}; try { astroEvents = mods.suncalc.getTimes(today, latitude, longitude); } catch (e) { adapter.log.error(`Cannot calculate astro data: ${e}`); } if (astroEvents) { try { astroEvents.nextSunrise = getAstroEvent( today, obj.message.sunriseEvent || adapter.config.sunriseEvent, obj.message.sunriseLimitStart || adapter.config.sunriseLimitStart, obj.message.sunriseLimitEnd || adapter.config.sunriseLimitEnd, sunriseOffset, false, latitude, longitude, true ); astroEvents.nextSunset = getAstroEvent( today, obj.message.sunsetEvent || adapter.config.sunsetEvent, obj.message.sunsetLimitStart || adapter.config.sunsetLimitStart, obj.message.sunsetLimitEnd || adapter.config.sunsetLimitEnd, sunsetOffset, true, latitude, longitude, true ); } catch (e) { adapter.log.error(`Cannot calculate astro data: ${e}`); } } const result = {}; const keys = Object.keys(astroEvents).sort((a, b) => astroEvents[a] - astroEvents[b]); keys.forEach(key => result[key] = { serverTime: formatHoursMinutesSeconds(astroEvents[key]), date: astroEvents[key].toISOString() }); obj.callback && adapter.sendTo(obj.from, obj.command, result, obj.callback); } break; } case 'calcAstro': { if (obj.message) { const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined ? adapter.config.sunriseOffset : obj.message.sunriseOffset, 10) || 0; const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined ? adapter.config.sunsetOffset : obj.message.sunsetOffset, 10) || 0; const longitude = parseFloat(obj.message.longitude === undefined ? adapter.config.longitude : obj.message.longitude) || 0; const latitude = parseFloat(obj.message.latitude === undefined ? adapter.config.latitude : obj.message.latitude) || 0; const today = getAstroStartOfDay(); const nextSunrise = getAstroEvent( today, obj.message.sunriseEvent || adapter.config.sunriseEvent, obj.message.sunriseLimitStart || adapter.config.sunriseLimitStart, obj.message.sunriseLimitEnd || adapter.config.sunriseLimitEnd, sunriseOffset, false, latitude, longitude, true ); const nextSunset = getAstroEvent( today, obj.message.sunsetEvent || adapter.config.sunsetEvent, obj.message.sunsetLimitStart || adapter.config.sunsetLimitStart, obj.message.sunsetLimitEnd || adapter.config.sunsetLimitEnd, sunsetOffset, true, latitude, longitude, true ); obj.callback && adapter.sendTo(obj.from, obj.command, { nextSunrise: { serverTime: formatHoursMinutesSeconds(nextSunrise), date: nextSunrise }, nextSunset: { serverTime: formatHoursMinutesSeconds(nextSunset), date: nextSunset } }, obj.callback); } break; } case 'debug': { !debugMode && debugStart(obj.message); break; } case 'debugStop': { !debugMode && debugStop() .then(() => console.log('stopped')); break; } case 'rulesOn': { context.rulesOpened = obj.message; console.log('Enable messaging for ' + context.rulesOpened); break; } case 'rulesOff': { // may be if (context.rulesOpened === obj.message) console.log('Disable messaging for ' + context.rulesOpened); context.rulesOpened = null; break; } case 'getIoBrokerDataDir': { obj.callback && adapter.sendTo(obj.from, obj.command, { dataDir: utils.getAbsoluteDefaultDataDir(), sep: nodePath.sep }, obj.callback); break; } } } }, /** * If the JS-Controller catches an unhandled error, this will be called * so we have a chance to handle it ourself. * @param {Error} err */ error: err => { // Identify unhandled errors originating from callbacks in scripts // These are not caught by wrapping the execution code in try-catch if (err && typeof err.stack === 'string') { const scriptCodeMarkerIndex = err.stack.indexOf(SCRIPT_CODE_MARKER); if (scriptCodeMarkerIndex > -1) { // This is a script error let scriptName = err.stack.substr(scriptCodeMarkerIndex); scriptName = scriptName.substr(0, scriptName.indexOf(':')); context.logError(scriptName, err); // Leave the script running for now // signal to the JS-Controller that we handled the error ourselves return true; } // check if a path contains adaptername but not own node_module // this regex matched "iobroker.javascript/" if NOT followed by "node_modules" if (!err.stack.match(/iobroker\.javascript[/\\](?!.*node_modules).*/g)) { // This is an error without any info on origin (mostly async errors like connection errors) // also consider it as being from a script adapter.log.error('An error happened which is most likely from one of your scripts, but the originating script could not be detected.'); adapter.log.error('Error: ' + err.message); adapter.log.error(err.stack); // signal to the JS-Controller that we handled the error ourselves return true; } } } }); adapter = new utils.Adapter(options); // handler for logs adapter.on('log', msg => Object.keys(context.logSubscriptions) .forEach(name => context.logSubscriptions[name].forEach(handler => { if (typeof handler.cb === 'function' && (handler.severity === '*' || handler.severity === msg.severity)) { handler.sandbox.logHandler = handler.severity || '*'; handler.cb.call(handler.sandbox, msg); handler.sandbox.logHandler = null; } }))); context.adapter = adapter; return adapter; } function updateObjectContext(id, obj) { if (obj) { // add state to state ID's list if (obj.type === 'state') { if (!context.stateIds.includes(id)) { context.stateIds.push(id); context.stateIds.sort(); } if (context.devices && context.channels) { const parts = id.split('.'); parts.pop(); const chn = parts.join('.'); context.channels[chn] = context.channels[chn] || []; context.channels[chn].push(id); parts.pop(); const dev = parts.join('.'); context.devices[dev] = context.devices[dev] || []; context.devices[dev].push(id); } } } else { // delete object from state ID's list const pos = context.stateIds.indexOf(id); pos !== -1 && context.stateIds.splice(pos, 1); if (context.devices && context.channels) { const parts = id.split('.'); parts.pop(); const chn = parts.join('.'); if (context.channels[chn]) { const posChn = context.channels[chn].indexOf(id); posChn !== -1 && context.channels[chn].splice(posChn, 1); } parts.pop(); const dev = parts.join('.'); if (context.devices[dev]) { const posDev = context.devices[dev].indexOf(id); posDev !== -1 && context.devices[dev].splice(posDev, 1); } } delete context.folderCreationVerifiedObjects[id]; } if (!obj && context.objects[id]) { // objects was deleted removeFromNames(id); delete context.objects[id]; } else if (obj && !context.objects[id]) { // object was added context.objects[id] = obj; addToNames(obj); } else if (obj && context.objects[id].common) { // Object just changed context.objects[id] = obj; const n = getName(id); let nn = context.objects[id].common ? context.objects[id].common.name : ''; if (nn && typeof nn === 'object') { nn = nn[words.getLanguage()] || nn.en; } if (n !== nn) { if (n) { removeFromNames(id); } if (nn) { addToNames(obj); } } } } function main() { !debugMode && patchFont() .then(patched => patched && adapter.log.debug('Font patched')); // todo context.errorLogFunction = webstormDebug ? console : adapter.log; activeStr = `${adapter.namespace}.scriptEnabled.`; mods.fs = new require('./lib/protectFs')(adapter.log, utils.getAbsoluteDefaultDataDir()); mods['fs/promises'] = mods.fs.promises; // to avoid require('fs/promises'); // 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.message}`); // This should not happen, so send a error report to Sentry if (adapter.supportsFeature && adapter.supportsFeature('PLUGINS')) { const sentryInstance = adapter.getPluginInstance('sentry'); if (sentryInstance) { const sentryObject = sentryInstance.getSentryObject(); if (sentryObject) sentryObject.captureException(e); } } // Keep the adapter from crashing when the included typings cannot be read tsAmbient = {}; } 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(() => { // Load the TS declarations for Node.js and all 3rd party modules loadTypeScriptDeclarations(); getData(() => { context.scheduler = new Scheduler(adapter.log, Date, mods.suncalc, adapter.config.latitude, adapter.config.longitude); dayTimeSchedules(adapter, context); sunTimeSchedules(adapter, context); timeSchedule(adapter, context); // Warning. It could have a side-effect in compact mode, so all adapters will accept self signed certificates if (adapter.config.allowSelfSignedCerts) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } adapter.getObjectView('script', 'javascript', {}, async (err, doc) => { globalScript = ''; globalDeclarations = ''; knownGlobalDeclarationsByScript = {}; if (doc && doc.rows && doc.rows.length) { // assemble global script for (let g = 0; g < doc.rows.length; g++) { const obj = doc.rows[g].value; if (checkIsGlobal(obj)) { if (obj && obj.common) { const engineType = (obj.common.engineType || '').toLowerCase(); if (obj.common.enabled) { if (engineType.startsWith('typescript')) { // TypeScript adapter.log.info(`${obj._id}: compiling TypeScript source...`); // In order to compile global TypeScript, we need to do some transformations // 1. For top-level-await, some statements must be wrapped in an immediately-invoked async function // 2. If any global script uses `import`, the declarations are no longer visible if they are not exported with `declare global` const transformedSource = transformScriptBeforeCompilation(obj.common.source, true); // The source code must be transformed in order to support top level await // Global scripts must not be treated as a module, otherwise their methods // cannot be found by the normal scripts // We need to hash both global declarations that are known until now