iobroker.javascript
Version:
Rules Engine for ioBroker
1,072 lines (1,071 loc) • 124 kB
JavaScript
"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) {