* [jsscripting] Reimplement timers to conform standard JS * [jsscripting] Name scheduled jobs by loggerName + id * [jsscripting] Update timer identifiers * [jsscripting] Update identifiers for scheduled jobs * [jsscripting] Synchronize method that is called when the script is reloaded * [jsscripting] Cancel all scheduled jobs when the engine is closed * [jsscripting] Ensure that a timerId is never reused by a subsequent call & Use long primitive type instead of Integer * [jsscripting] Use an abstraction class to inject features into the JS runtime * [jsscripting] Make ThreadsafeTimers threadsafe for concurrent access to the class itself * [jsscripting] Move the locking for `invokeFunction` to `OpenhabGraalJSScriptEngine` Signed-off-by: Florian Hotze <florianh_dev@icloud.com>
190 lines
5.9 KiB
JavaScript
190 lines
5.9 KiB
JavaScript
// ThreadsafeTimers is injected into the JS runtime
|
|
|
|
(function (global) {
|
|
'use strict';
|
|
|
|
// Append the script file name OR rule UID depending on which is available
|
|
const defaultIdentifier = "org.openhab.automation.script" + (globalThis["javax.script.filename"] ? ".file." + globalThis["javax.script.filename"].replace(/^.*[\\\/]/, '') : globalThis["ruleUID"] ? ".ui." + globalThis["ruleUID"] : "");
|
|
const System = Java.type('java.lang.System');
|
|
const formatRegExp = /%[sdj%]/g;
|
|
// Pass the defaultIdentifier to ThreadsafeTimers to enable naming of scheduled jobs
|
|
ThreadsafeTimers.setIdentifier(defaultIdentifier);
|
|
|
|
function createLogger(name = defaultIdentifier) {
|
|
return Java.type("org.slf4j.LoggerFactory").getLogger(name);
|
|
}
|
|
|
|
// User configurable
|
|
let log = createLogger();
|
|
|
|
function stringify(value) {
|
|
try {
|
|
if (Java.isJavaObject(value)) {
|
|
return value.toString();
|
|
} else {
|
|
// special cases
|
|
if (value === undefined) {
|
|
return "undefined"
|
|
}
|
|
if (typeof value === 'function') {
|
|
return "[Function]"
|
|
}
|
|
if (value instanceof RegExp) {
|
|
return value.toString();
|
|
}
|
|
// fallback to JSON
|
|
return JSON.stringify(value, null, 2);
|
|
}
|
|
} catch (e) {
|
|
return '[Circular: ' + e + ']';
|
|
}
|
|
}
|
|
|
|
function format(f) {
|
|
if (typeof f !== 'string') {
|
|
var objects = [];
|
|
for (var index = 0; index < arguments.length; index++) {
|
|
objects.push(stringify(arguments[index]));
|
|
}
|
|
return objects.join(' ');
|
|
}
|
|
|
|
if (arguments.length === 1) return f;
|
|
|
|
var i = 1;
|
|
var args = arguments;
|
|
var len = args.length;
|
|
var str = String(f).replace(formatRegExp, function (x) {
|
|
if (x === '%%') return '%';
|
|
if (i >= len) return x;
|
|
switch (x) {
|
|
case '%s': return String(args[i++]);
|
|
case '%d': return Number(args[i++]);
|
|
case '%j':
|
|
try {
|
|
return stringify(args[i++]);
|
|
} catch (_) {
|
|
return '[Circular]';
|
|
}
|
|
// falls through
|
|
default:
|
|
return x;
|
|
}
|
|
});
|
|
for (var x = args[i]; i < len; x = args[++i]) {
|
|
if (x === null || (typeof x !== 'object' && typeof x !== 'symbol')) {
|
|
str += ' ' + x;
|
|
} else {
|
|
str += ' ' + stringify(x);
|
|
}
|
|
}
|
|
return str;
|
|
}
|
|
|
|
const counters = {};
|
|
const timers = {};
|
|
|
|
// Polyfills for common NodeJS functions
|
|
|
|
const console = {
|
|
'assert': function (expression, message) {
|
|
if (!expression) {
|
|
log.error(message);
|
|
}
|
|
},
|
|
|
|
count: function (label) {
|
|
let counter;
|
|
|
|
if (label) {
|
|
if (counters.hasOwnProperty(label)) {
|
|
counter = counters[label];
|
|
} else {
|
|
counter = 0;
|
|
}
|
|
|
|
// update
|
|
counters[label] = ++counter;
|
|
log.debug(format.apply(null, [label + ':', counter]));
|
|
}
|
|
},
|
|
|
|
debug: function () {
|
|
log.debug(format.apply(null, arguments));
|
|
},
|
|
|
|
info: function () {
|
|
log.info(format.apply(null, arguments));
|
|
},
|
|
|
|
log: function () {
|
|
log.info(format.apply(null, arguments));
|
|
},
|
|
|
|
warn: function () {
|
|
log.warn(format.apply(null, arguments));
|
|
},
|
|
|
|
error: function () {
|
|
log.error(format.apply(null, arguments));
|
|
},
|
|
|
|
trace: function (e) {
|
|
if (Java.isJavaObject(e)) {
|
|
log.trace(e.getLocalizedMessage(), e);
|
|
} else {
|
|
if (e.stack) {
|
|
log.trace(e.stack);
|
|
} else {
|
|
if (e.message) {
|
|
log.trace(format.apply(null, [(e.name || 'Error') + ':', e.message]));
|
|
} else {
|
|
log.trace((e.name || 'Error'));
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
time: function (label) {
|
|
if (label) {
|
|
timers[label] = System.currentTimeMillis();
|
|
}
|
|
},
|
|
|
|
timeEnd: function (label) {
|
|
if (label) {
|
|
const now = System.currentTimeMillis();
|
|
if (timers.hasOwnProperty(label)) {
|
|
log.info(format.apply(null, [label + ':', (now - timers[label]) + 'ms']));
|
|
delete timers[label];
|
|
} else {
|
|
log.info(format.apply(null, [label + ':', '<no timer>']));
|
|
}
|
|
}
|
|
},
|
|
|
|
// Allow user customizable logging names
|
|
// Be aware that a log4j2 required a logger defined for the logger name, otherwise messages won't be logged!
|
|
set loggerName(name) {
|
|
log = createLogger(name);
|
|
this._loggerName = name;
|
|
ThreadsafeTimers.setIdentifier(name);
|
|
},
|
|
|
|
get loggerName() {
|
|
return this._loggerName || defaultIdentifier;
|
|
}
|
|
};
|
|
|
|
// Polyfill common NodeJS functions onto the global object
|
|
globalThis.console = console;
|
|
globalThis.setTimeout = ThreadsafeTimers.setTimeout;
|
|
globalThis.clearTimeout = ThreadsafeTimers.clearTimeout;
|
|
globalThis.setInterval = ThreadsafeTimers.setInterval;
|
|
globalThis.clearInterval = ThreadsafeTimers.clearInterval;
|
|
|
|
// Support legacy NodeJS libraries
|
|
globalThis.global = globalThis;
|
|
globalThis.process = { env: { NODE_ENV: '' } };
|
|
})(this);
|