UNPKG

iobroker.javascript

Version:
1,072 lines (1,071 loc) 124 kB
"use strict"; /* * Javascript adapter * * The MIT License (MIT) * * Copyright (c) 2014-2024 bluefox <dogafox@gmail.com>, * * Copyright (c) 2014 hobbyquaker */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const node_vm_1 = require("node:vm"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const node_child_process_1 = require("node:child_process"); const virtual_tsc_1 = require("virtual-tsc"); const node_util_1 = require("node:util"); const prettier_1 = __importDefault(require("prettier")); const dgram = __importStar(require("node:dgram")); const crypto = __importStar(require("node:crypto")); const dns = __importStar(require("node:dns")); const events = __importStar(require("node:events")); const http = __importStar(require("node:http")); const https = __importStar(require("node:https")); const http2 = __importStar(require("node:http2")); const net = __importStar(require("node:net")); const os = __importStar(require("node:os")); const path = __importStar(require("node:path")); const util = __importStar(require("node:util")); const child_process = __importStar(require("node:child_process")); const stream = __importStar(require("node:stream")); const zlib = __importStar(require("node:zlib")); // @ts-expect-error no types available const suncalc = __importStar(require("suncalc2")); const axios = __importStar(require("axios")); // @ts-expect-error no types available const wake_on_lan = __importStar(require("wake_on_lan")); const nodeSchedule = __importStar(require("node-schedule")); const adapter_core_1 = require("@iobroker/adapter-core"); const mirror_1 = require("./lib/mirror"); const protectFs_1 = __importDefault(require("./lib/protectFs")); const words_1 = require("./lib/words"); const sandbox_1 = require("./lib/sandbox"); const nodeModulesManagement_1 = require("./lib/nodeModulesManagement"); const eventObj_1 = require("./lib/eventObj"); const scheduler_1 = require("./lib/scheduler"); const typescriptSettings_1 = require("./lib/typescriptSettings"); const tools_1 = require("./lib/tools"); const typescriptTools_1 = require("./lib/typescriptTools"); /** * 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!! */ const forbiddenMirrorLocations = [ 'backup-objects', 'files', 'backitup', '../backups', '../node_modules', '../log', ]; const packageJson = JSON.parse((0, node_fs_1.readFileSync)(`${__dirname}/../package.json`).toString()); const SCRIPT_CODE_MARKER = 'script.js.'; let webstormDebug; const isCI = !!process.env.CI; // ambient declarations for typescript let tsAmbient; // TypeScript's scripts are only recompiled if their source hash changes. // If an adapter update fixes the compilation bugs, a user won't notice until the 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}`; // 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 + // the 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()); } const regExGlobalOld = /_global$/; const regExGlobalNew = /script\.js\.global\./; function checkIsGlobal(obj) { return obj?.common && (regExGlobalOld.test(obj.common.name) || regExGlobalNew.test(obj._id)); } 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; } function getNextTimeEvent(time, useNextDay) { const now = getAstroStartOfDay(); const [timeHours, timeMinutes] = time.split(':'); const nTimeHours = parseInt(timeHours, 10); const nTimeMinutes = parseInt(timeMinutes, 10); if (useNextDay && (now.getHours() > nTimeHours || (now.getHours() === nTimeHours && now.getMinutes() > nTimeMinutes))) { now.setDate(now.getDate() + 1); } now.setHours(nTimeHours); now.setMinutes(nTimeMinutes); return now; } function getAstroStartOfDay() { const d = new Date(); d.setMinutes(0); d.setSeconds(0); d.setMilliseconds(0); d.setTime(d.getTime() - d.getTimezoneOffset() * 60 * 1000); d.setUTCHours(0); return d; } function formatHoursMinutesSeconds(date) { const h = String(date.getHours()); const m = String(date.getMinutes()); const s = String(date.getSeconds()); return `${h.padStart(2, '0')}:${m.padStart(2, '0')}:${s.padStart(2, '0')}`; } // Due to a npm bug, virtual-tsc may be hoisted to the top level node_modules but // TypeScript may still be in the adapter level (https://npm.community/t/packages-with-peerdependencies-are-incorrectly-hoisted/4794), // so we need to tell virtual-tsc where TypeScript is (0, virtual_tsc_1.setTypeScriptResolveOptions)({ paths: [require.resolve('typescript')], }); // compiler instance for global JS declarations const jsDeclarationServer = new virtual_tsc_1.Server(typescriptSettings_1.jsDeclarationCompilerOptions, isCI ? false : undefined); /** * Stores the IDs of script objects whose change should be ignored because * the compiled source was just updated */ class JavaScript extends adapter_core_1.Adapter { context; errorLogFunction = { error: (msg) => console.error(msg), warn: (msg) => console.warn(msg), info: (msg) => console.log(msg), debug: (msg) => console.debug(msg), silly: (msg) => console.debug(msg), }; mods; objectsInitDone = false; statesInitDone = false; objects = {}; states = {}; interimStateValues = {}; stateIds = []; subscriptions = []; subscriptionsFile = []; subscriptionsObject = []; subscribedPatterns = {}; subscribedPatternsFile = {}; adapterSubs = {}; timers = {}; _enums = []; names = {}; // name: id scripts = {}; messageBusHandlers = {}; logSubscriptions = {}; tempDirectories = {}; // name: path folderCreationVerifiedObjects = {}; /** if logs are subscribed or not */ logSubscribed = false; timeSettings = { format12: false, leadingZeros: true }; dayScheduleTimer = null; // schedule for astrological day sunScheduleTimer = null; // schedule for sun moment times timeScheduleTimer = null; // schedule for astrological day activeStr = ''; // enabled state prefix mirror; stopCounters = {}; setStateCountCheckInterval = null; globalScript = ''; /** Generated declarations for global TypeScripts */ globalDeclarations = ''; // Remember which definitions the global scripts // have access to, because it depends on the compilation order knownGlobalDeclarationsByScript = {}; globalScriptLines = 0; // compiler instance for typescript tsServer; ignoreObjectChange = new Set(); debugState = { scriptName: '', child: null, promiseOnEnd: null, paused: false, started: 0, running: false, }; constructor(options = {}) { options = { ...options, name: 'javascript', // adapter name useFormatDate: true, /** * If the JS-Controller catches an unhandled error, this will be called * so we have a chance to handle it ourselves. */ 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.substring(scriptCodeMarkerIndex); scriptName = scriptName.substring(0, scriptName.indexOf(':')); this.logError(scriptName, 'Error:', 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 this.log.error('An error happened which is most likely from one of your scripts, but the originating script could not be detected.'); this.log.error(`Error: ${err.message}`); this.log.error(err.stack); // signal to the JS-Controller that we handled the error ourselves return true; } } return false; }, }; super(options); this.on('objectChange', this.onObjectChange.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); this.on('ready', this.onReady.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); this.on('fileChange', this.onFileChange.bind(this)); this.on('log', this.onLog.bind(this)); this.mods = { fs: {}, 'fs/promises': {}, dgram, crypto, dns, events, http, https, http2, net, os, path, util, child_process, stream, zlib, suncalc, axios, wake_on_lan, nodeSchedule, }; // check the webstorm debug and just debug modes 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]; } } } } this.context = { mods: this.mods, objects: this.objects, states: this.states, interimStateValues: this.interimStateValues, stateIds: this.stateIds, errorLogFunction: this.errorLogFunction, subscriptions: this.subscriptions, subscriptionsFile: this.subscriptionsFile, subscriptionsObject: this.subscriptionsObject, subscribedPatterns: this.subscribedPatterns, subscribedPatternsFile: this.subscribedPatternsFile, adapterSubs: this.adapterSubs, cacheObjectEnums: {}, timers: this.timers, enums: this._enums, names: this.names, scripts: this.scripts, messageBusHandlers: this.messageBusHandlers, logSubscriptions: this.logSubscriptions, tempDirectories: this.tempDirectories, folderCreationVerifiedObjects: this.folderCreationVerifiedObjects, isEnums: false, // If some subscription wants enum channels: null, devices: null, logWithLineInfo: this.logWithLineInfo.bind(this), scheduler: null, timerId: 0, rulesOpened: null, // opened rules language: this.language || 'en', updateLogSubscriptions: this.updateLogSubscriptions.bind(this), convertBackStringifiedValues: this.convertBackStringifiedValues.bind(this), updateObjectContext: this.updateObjectContext.bind(this), prepareStateObject: this.prepareStateObject.bind(this), debugMode, getAbsoluteDefaultDataDir: adapter_core_1.getAbsoluteDefaultDataDir, adapter: this, logError: this.logError.bind(this), }; this.tsServer = new virtual_tsc_1.Server(typescriptSettings_1.tsCompilerOptions, this.tsLog); } async onObjectChange(id, obj) { // Check if we should ignore this change (once!) because we just updated the compiled sources if (this.ignoreObjectChange.has(id)) { // Update the cached script object and do nothing more this.objects[id] = obj; this.ignoreObjectChange.delete(id); return; } // When still in initializing: already remember current values, // but data structures are initialized elsewhere if (!this.objectsInitDone) { if (obj) { this.objects[id] = obj; } return; } if (id.startsWith('enum.')) { // clear cache this.context.cacheObjectEnums = {}; // update this._enums array if (obj) { // If new if (!this._enums.includes(id)) { this._enums.push(id); this._enums.sort(); } } else { const pos = this._enums.indexOf(id); // if deleted if (pos !== -1) { this._enums.splice(pos, 1); } } } if (id === 'system.config' && obj?.common?.language) { // set language for debug messages (0, words_1.setLanguage)(obj.common.language); this.language = obj.common.language; this.context.language = this.language; } // update stored time format for variables.dayTime if (id === `${this.namespace}.variables.dayTime` && obj?.native) { this.timeSettings.format12 = obj.native.format12 || false; this.timeSettings.leadingZeros = obj.native.leadingZeros === undefined ? true : obj.native.leadingZeros; } // send changes to disk mirror this.mirror?.onObjectChange(id, obj); const formerObj = this.objects[id]; this.updateObjectContext(id, obj); // Update all Meta object data // for the alias object changes on the state objects, we need to manually update the // state cache value, because the new value is only published on the next change if (obj?.type === 'state' && id.startsWith('alias.0.')) { // execute async for speed this.getForeignStateAsync(id) .then(state => { if (state) { this.states[id] = state; } else if (this.states[id] !== undefined) { delete this.states[id]; } }) .catch(() => { /* ignore */ }); } this.subscriptionsObject.forEach(sub => { // ToDo: implement comparing with id.0.* too if (sub.pattern === id) { try { sub.callback(id, obj); } catch (err) { this.log.error(`Error in callback: ${err.toString()}`); } } }); // handle Script object updates if (!obj && 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.common.enabled) { this.log.info(`Active global Script ${id} deleted. Restart instance.`); this.restart(); } } else if (formerObj.common?.engine === `system.adapter.${this.namespace}`) { // It was a non-global Script and deleted => stop and remove it await this.stopScript(id); // delete scriptEnabled.blabla variable const idActive = `scriptEnabled.${id.substring(SCRIPT_CODE_MARKER.length)}`; await this.delStateAsync(idActive); await this.delObjectAsync(idActive); // delete scriptProblem.blabla variable const idProblem = `scriptProblem.${id.substring(SCRIPT_CODE_MARKER.length)}`; await this.delStateAsync(idProblem); await this.delObjectAsync(idProblem); } } else if (!formerObj && obj?.type === 'script') { // New script that does not exist before if (checkIsGlobal(obj)) { // new global script added => restart adapter if (obj.common.enabled) { this.log.info(`Active global Script ${id} created. Restart instance.`); this.restart(); } } else if (obj.common?.engine === `system.adapter.${this.namespace}`) { // new non-global script - create states for scripts await this.createActiveObject(id, !!obj.common.enabled); await this.createProblemObject(id); if (obj.common.enabled) { // if enabled => Start script await this.loadScriptById(id); } } } else if (obj?.type === 'script' && formerObj?.common) { // Script changed ... if (checkIsGlobal(obj)) { if (obj.common.enabled || formerObj.common.enabled) { this.log.info(`Global Script ${id} updated. Restart instance.`); this.restart(); } } else { // No global script if (obj.common?.engine === `system.adapter.${this.namespace}`) { // create states for scripts await this.createActiveObject(id, !!obj.common.enabled); await this.createProblemObject(id); } if ((formerObj.common.enabled && !obj.common.enabled) || (formerObj.common.engine === `system.adapter.${this.namespace}` && obj.common.engine !== `system.adapter.${this.namespace}`)) { // Script disabled if (formerObj.common.enabled && formerObj.common.engine === `system.adapter.${this.namespace}`) { // Remove it from executing await this.stopScript(id); } } else if ((!formerObj.common.enabled && obj.common.enabled) || (formerObj.common.engine !== `system.adapter.${this.namespace}` && obj.common.engine === `system.adapter.${this.namespace}`)) { // Script enabled if (obj.common.enabled && obj.common.engine === `system.adapter.${this.namespace}`) { // Start script await this.loadScriptById(id); } } else { // if (obj.common.source !== formerObj.common.source) { // Source changed => restart the script this.stopCounters[id] = this.stopCounters[id] ? this.stopCounters[id] + 1 : 1; void this.stopScript(id).then(() => { // only start again after stop when "last" object change to prevent problems on // multiple changes in fast frequency if (!--this.stopCounters[id]) { void this.loadScriptById(id); } }); } } } } onStateChange(id, state) { if (this.interimStateValues[id] !== undefined) { // any update invalidates the remembered interim value delete this.interimStateValues[id]; } if (!id || id.startsWith('messagebox.') || id.startsWith('log.')) { return; } if (id === `${this.namespace}.debug.to` && state && !state.ack) { if (!this.context.debugMode) { this.debugSendToInspector(state.val); } return; } // When still in initializing: already remember current values, // but data structures are initialized elsewhere if (!this.statesInitDone) { if (state) { this.states[id] = state; } return; } const oldState = this.states[id]; if (state) { if (oldState) { // enable or disable script if (!state.ack && id.startsWith(this.activeStr) && this.objects[id]?.native?.script) { void this.extendForeignObject(this.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 (this.adapterSubs[id]) { const parts = id.split('.'); const a = `${parts[2]}.${parts[3]}`; for (let t = 0; t < this.adapterSubs[id].length; t++) { this.log.info(`Detected coming adapter "${a}". Send subscribe: ${this.adapterSubs[id][t]}`); this.sendTo(a, 'subscribe', this.adapterSubs[id][t]); } } } } else if ( /*!oldState && */!this.stateIds.includes(id)) { this.stateIds.push(id); this.stateIds.sort(); } this.states[id] = state; } else { if (oldState) { delete this.states[id]; } state = {}; const pos = this.stateIds.indexOf(id); if (pos !== -1) { this.stateIds.splice(pos, 1); } } const _eventObj = (0, eventObj_1.createEventObject)(this.context, id, this.convertBackStringifiedValues(id, state), this.convertBackStringifiedValues(id, oldState)); // if this state matches any subscriptions for (let i = 0, l = this.subscriptions.length; i < l; i++) { const sub = this.subscriptions[i]; if (sub?.patternCompareFunctions && patternMatching(_eventObj, sub.patternCompareFunctions)) { try { sub.callback(_eventObj); } catch (err) { this.log.error(`Error in callback: ${err.toString()}`); } } } } onFileChange(id, fileName, size) { // if this file matches any subscriptions for (let i = 0, l = this.subscriptionsFile.length; i < l; i++) { const sub = this.subscriptionsFile[i]; if (sub && fileMatching(sub, id, fileName)) { try { sub.callback(id, fileName, size, sub.withFile); } catch (err) { this.log.error(`Error in callback: ${err.toString()}`); } } } } async onUnload(callback) { await this.debugStop(); this.stopTimeSchedules(); if (this.setStateCountCheckInterval) { clearInterval(this.setStateCountCheckInterval); this.setStateCountCheckInterval = null; } await this.stopAllScripts(); if (typeof callback === 'function') { callback(); } } async onReady() { this.errorLogFunction = this.log; this.context.errorLogFunction = this.log; this.config.maxSetStatePerMinute = parseInt(this.config.maxSetStatePerMinute, 10) || 1000; this.config.maxTriggersPerScript = parseInt(this.config.maxTriggersPerScript, 10) || 100; if (this.supportsFeature?.('PLUGINS')) { const sentryInstance = this.getPluginInstance('sentry'); if (sentryInstance) { const Sentry = sentryInstance.getSentryObject(); Sentry?.withScope(scope => { scope.addEventProcessor((event, _hint) => { if (event.exception?.values?.[0]) { const eventData = event.exception.values[0]; if (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?.includes(SCRIPT_CODE_MARKER))) { return null; } // Exclude event if own directory is included but not inside own node_modules const ownNodeModulesDir = (0, node_path_1.join)(__dirname, 'node_modules'); if (!eventData.stacktrace.frames.find(frame => frame.filename?.includes(__dirname) && !frame.filename.includes(ownNodeModulesDir))) { return null; } // We have exception data and did not sort it out, so report it return event; } } // No exception in it ... do not report return null; }); }); } } await this.main(); } onMessage(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}` === this.namespace || obj.message.instance === this.namespace)) { Object.keys(this.messageBusHandlers).forEach(name => { // script name could be script.js.xxx or only xxx if ((!obj.message.script || obj.message.script === name) && this.messageBusHandlers[name][obj.message.message]) { this.messageBusHandlers[name][obj.message.message].forEach(handler => { const sandbox = handler.sandbox; sandbox.verbose && sandbox.log(`onMessage: ${JSON.stringify(obj.message)}`, 'info'); try { if (obj.callback) { handler.cb.call(sandbox, obj.message.data, (result) => { if (sandbox.verbose) { sandbox.log(`onMessage result: ${JSON.stringify(result)}`, 'info'); } this.sendTo(obj.from, obj.command, result, obj.callback); }); } else { handler.cb.call(sandbox, obj.message.data, (result) => { sandbox.verbose && sandbox.log(`onMessage result: ${JSON.stringify(result)}`, 'info'); }); } } catch (err) { void this.setState(`scriptProblem.${name.substring(SCRIPT_CODE_MARKER.length)}`, true, true); this.logError(name, 'Error in callback:', err); } }); } }); } break; case 'loadTypings': { // Load typings for the editor const typings = {}; // try to load TypeScript lib files from disk try { const typescriptLibs = (0, typescriptTools_1.resolveTypescriptLibs)(typescriptSettings_1.targetTsLib); Object.assign(typings, typescriptLibs); } catch { /* 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(this.knownGlobalDeclarationsByScript)) { typings[`${globalScriptPaths}.d.ts`] = this.knownGlobalDeclarationsByScript[globalScriptPaths]; } if (obj.callback) { this.sendTo(obj.from, obj.command, { typings }, obj.callback); } break; } case 'calcAstroAll': { if (obj.message) { const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined ? this.config.sunriseOffset : obj.message.sunriseOffset, 10) || 0; const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined ? this.config.sunsetOffset : obj.message.sunsetOffset, 10) || 0; const longitude = parseFloat(obj.message.longitude === undefined ? this.config.longitude : obj.message.longitude) || 0; const latitude = parseFloat(obj.message.latitude === undefined ? this.config.latitude : obj.message.latitude) || 0; const today = getAstroStartOfDay(); let astroEvents = {}; try { astroEvents = this.mods.suncalc.getTimes(today, latitude, longitude); } catch (err) { this.log.error(`Cannot calculate astro data: ${err}`); } if (astroEvents) { try { astroEvents.nextSunrise = this.getAstroEvent(today, obj.message.sunriseEvent || this.config.sunriseEvent, obj.message.sunriseLimitStart || this.config.sunriseLimitStart, obj.message.sunriseLimitEnd || this.config.sunriseLimitEnd, sunriseOffset, false, latitude, longitude, true); astroEvents.nextSunset = this.getAstroEvent(today, obj.message.sunsetEvent || this.config.sunsetEvent, obj.message.sunsetLimitStart || this.config.sunsetLimitStart, obj.message.sunsetLimitEnd || this.config.sunsetLimitEnd, sunsetOffset, true, latitude, longitude, true); } catch (err) { this.log.error(`Cannot calculate astro data: ${err}`); } } const result = {}; const keys = Object.keys(astroEvents).sort((a, b) => astroEvents[a] - astroEvents[b]); keys.forEach(key => { const validDate = astroEvents[key] !== null && !isNaN(astroEvents[key].getTime()); result[key] = { isValidDate: validDate, serverTime: validDate ? formatHoursMinutesSeconds(astroEvents[key]) : 'n/a', date: validDate ? astroEvents[key].toISOString() : 'n/a', }; }); if (obj.callback) { this.sendTo(obj.from, obj.command, result, obj.callback); } } break; } case 'calcAstro': { if (obj.message) { const longitude = parseFloat(obj.message.longitude === undefined ? this.config.longitude : obj.message.longitude) || 0; const latitude = parseFloat(obj.message.latitude === undefined ? this.config.latitude : obj.message.latitude) || 0; const today = getAstroStartOfDay(); const sunriseEvent = obj.message?.sunriseEvent || this.config.sunriseEvent; const sunriseLimitStart = obj.message?.sunriseLimitStart || this.config.sunriseLimitStart; const sunriseLimitEnd = obj.message?.sunriseLimitEnd || this.config.sunriseLimitEnd; const sunriseOffset = parseInt(obj.message.sunriseOffset === undefined ? this.config.sunriseOffset : obj.message.sunriseOffset, 10) || 0; const nextSunrise = this.getAstroEvent(today, sunriseEvent, sunriseLimitStart, sunriseLimitEnd, sunriseOffset, false, latitude, longitude, true); const sunsetEvent = obj.message?.sunsetEvent || this.config.sunsetEvent; const sunsetLimitStart = obj.message?.sunsetLimitStart || this.config.sunsetLimitStart; const sunsetLimitEnd = obj.message?.sunsetLimitEnd || this.config.sunsetLimitEnd; const sunsetOffset = parseInt(obj.message.sunsetOffset === undefined ? this.config.sunsetOffset : obj.message.sunsetOffset, 10) || 0; const nextSunset = this.getAstroEvent(today, sunsetEvent, sunsetLimitStart, sunsetLimitEnd, sunsetOffset, true, latitude, longitude, true); const validDateSunrise = nextSunrise !== null && !isNaN(nextSunrise.getTime()); const validDateSunset = nextSunset !== null && !isNaN(nextSunset.getTime()); this.log.debug(`calcAstro sunrise: ${sunriseEvent} -> start ${sunriseLimitStart}, end: ${sunriseLimitEnd}, offset: ${sunriseOffset} - ${validDateSunrise ? nextSunrise.toISOString() : 'n/a'}`); this.log.debug(`calcAstro sunset: ${sunsetEvent} -> start ${sunsetLimitStart}, end: ${sunsetLimitEnd}, offset: ${sunsetOffset} - ${validDateSunset ? nextSunset.toISOString() : 'n/a'}`); if (obj.callback) { this.sendTo(obj.from, obj.command, { nextSunrise: { isValidDate: validDateSunrise, serverTime: validDateSunrise ? formatHoursMinutesSeconds(nextSunrise) : 'n/a', date: nextSunrise.toISOString(), }, nextSunset: { isValidDate: validDateSunset, serverTime: validDateSunset ? formatHoursMinutesSeconds(nextSunset) : 'n/a', date: nextSunset.toISOString(), }, }, obj.callback); } } break; } case 'debug': { if (!this.context.debugMode) { this.debugStart(obj.message); } break; } case 'debugStop': { if (!this.context.debugMode) { void this.debugStop().then(() => console.log('stopped')); } break; } case 'rulesOn': { this.context.rulesOpened = obj.message; console.log(`Enable messaging for ${this.context.rulesOpened}`); break; } case 'rulesOff': { // maybe if (context.rulesOpened === obj.message) console.log(`Disable messaging for ${this.context.rulesOpened}`); this.context.rulesOpened = null; break; } case 'getIoBrokerDataDir': { if (obj.callback) { this.sendTo(obj.from, obj.command, { dataDir: (0, adapter_core_1.getAbsoluteDefaultDataDir)(), sep: node_path_1.sep, }, obj.callback); } break; } case 'prettier': { // Format the code with Prettier if (obj.message && typeof obj.message.code === 'string') { try { prettier_1.default .format(obj.message.code, { parser: obj.message.type === 'typescript' ? 'babel-ts' : 'babel', printWidth: 120, semi: true, tabWidth: 4, useTabs: false, trailingComma: 'all', singleQuote: true, singleAttributePerLine: true, endOfLine: 'lf', bracketSpacing: true, arrowParens: 'avoid', quoteProps: 'as-needed', }) .then(formattedCode => { if (obj.callback) { this.sendTo(obj.from, obj.command, { code: formattedCode }, obj.callback); } else { this.logWithLineInfo(`Formatted code:\n${formattedCode}`); } }) .catch(e => { this.logError('Prettier', 'Error formatting code:', e); this.sendTo(obj.from, obj.command, { error: e.toString() }, obj.callback); }); } catch (e) { this.logError('Prettier', 'Error formatting code:', e); this.sendTo(obj.from, obj.command, { error: e.toString() }, obj.callback); } } else { this.sendTo(obj.from, obj.command, { error: 'No code provided' }, obj.callback); } break; } } } onLog(msg) { Object.keys(this.logSubscriptions).forEach((name) => this.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 = undefined; } })); } logError(scriptName, msg, e, offs) { const stack = e.stack ? e.stack.toString().split('\n') : e ? e.toString() : ''; if (!msg.includes('\n')) { msg = msg.replace(/[: ]*$/, ': '); } if (!msg.endsWith(' ')) { msg += ':'; } if (!scriptName.startsWith(SCRIPT_CODE_MARKER)) { scriptName = SCRIPT_CODE_MARKER + scriptName; } this.errorLogFunction.error(`${scriptName}: ${msg}${this.fixLineNo(stack[0])}`); for (let i = offs || 1; i < stack.length; i++) { if (!stack[i]) { continue; } if (stack[i].match(/runInNewContext|javascript\.js:/)) { break; } this.errorLogFunction.error(`${scriptName}: ${this.fixLineNo(stack[i])}`); } } logWithLineInfo(msg) { this.errorLogFunction.warn(msg); // get current error stack const stack = new Error().stack?.split('\n'); if (stack) { for (let i = 3; i < stack.length; i++) { if (!stack[i]) { continue; } if (stack[i].match(/runInContext|runInNewContext|javascript\.js:/)) { break; } this.errorLogFunction.warn(this.fixLineNo(stack[i])); } } } async main() { // Patch the font as it sometimes is wrong if (!this.context.debugMode) { if (await this.patchFont()) { this.log.debug('Font patched'); } } this.log.debug(`config.subscribe (Do not subscribe all states on start): ${this.config.subscribe}`); // correct jsonConfig for admin const instObj = await this.getForeignObjectAsync(`system.adapter.${this.namespace}`); if (instObj?.common) { if (instObj.common.adminUI?.config !== 'json') { if (instObj.common.adminUI) { instObj.common.adminUI.config = 'json'; } else { instObj.common.adminUI = { config: 'json' }; } void this.setForeignObject(instObj._id, instObj); } } if (webstormDebug) { this.errorLogFunction = { error: console.error, warn: console.warn, info: console.info, debug: console.log, silly: console.log, }; this.context.errorLogFunction = this.errorLogFunction; } this.activeStr = `${this.namespace}.scriptEnabled.`; this.mods.fs = new protectFs_1.default(this.log, (0, adapter_core_1.getAbsoluteDefaultDataDir)()); this.mods['fs/promises'] = this.mods.fs.promises; // to avoid require('fs/promises'); // try to read TS declarations try { tsAmbient = { 'javascript.d.ts': (0, node_fs_1.readFileSync)(this.mods.path.join(__dirname, 'lib/javascript.d.ts'), 'utf8'), }; this.tsServer.provideAmbientDeclarations(tsAmbient); jsDeclarationServer.provideAmbientDeclarations(tsAmbient); } catch (err) { this.log.warn(`Could not read TypeScript ambient declarations: ${err}`); // This should not happen, so send an error report to Sentry if (this.supportsFeature && this.supportsFeature('PLUGINS')) { const sentryInstance = this.getPluginInstance('sentry'); if (sentryInstance) { const sentryObject = sentryInstance.getSentryObject(); sentryObject?.captureException(err); } } // Keep the adapter from crashing when the included typings cannot be read tsAmbient = {}; } await this.installLibraries(); // Load the TS declarations for Node.js and all 3rd party modules this.loadTypeScriptDeclarations(); await this.getData(); this.context.scheduler = new scheduler_1.Scheduler(this.log, Date, this.mods.suncalc, this.config.latitude, this.config.longitude); await this.dayTimeSchedules(); await this.sunTimeSchedules(); await this.timeSchedule(); // Warning. It could have a side effect in compact mode, so all adapters will accept self-signed certificates if (this.config.allowSelfSignedCerts) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } const doc = await this.getObjectViewAsync('script', 'javascript', {}); if (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) {