[jsscripting] openhab-js integration (#11656)

Fixes #11222

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
Dan Cunningham 2021-12-12 23:13:13 -08:00 committed by GitHub
parent 306c30eda1
commit 4481ecff61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 811 additions and 10 deletions

View File

@ -25,6 +25,7 @@
<graal.version>21.3.0</graal.version>
<asm.version>6.2.1</asm.version>
<oh.version>${project.version}</oh.version>
<ohjs.version>openhab@0.0.1-beta.3</ohjs.version>
</properties>
<build>
@ -44,6 +45,62 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.12.0</version>
<configuration>
<nodeVersion>v12.16.1</nodeVersion>
<workingDirectory>target/js</workingDirectory>
</configuration>
<executions>
<execution>
<id>Install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<phase>generate-sources</phase>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install ${ohjs.version} webpack webpack-cli</arguments>
</configuration>
</execution>
<execution>
<id>npx webpack</id>
<goals>
<goal>npx</goal>
</goals>
<configuration>
<arguments>webpack -c ./node_modules/openhab/webpack.config.js --entry ./node_modules/openhab/ -o ./dist</arguments>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>add-resource</goal>
</goals>
<phase>generate-sources</phase>
<configuration>
<resources>
<resource>
<directory>target/js/dist</directory>
<targetPath>node_modules</targetPath>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -20,15 +20,26 @@ import java.util.Map;
import javax.script.ScriptEngine;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.openhab.core.config.core.ConfigurableService;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
/**
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
*
* @author Jonathan Gilbert - Initial contribution
* @author Dan Cunningham - Script injections
*/
@Component(service = ScriptEngineFactory.class)
@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.automation.jsscripting", property = Constants.SERVICE_PID
+ "=org.openhab.automation.jsscripting")
@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
private boolean injectionEnabled;
public static final String MIME_TYPE = "application/javascript;version=ECMAScript-2021";
@ -59,7 +70,18 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
@Override
public ScriptEngine createScriptEngine(String scriptType) {
OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
return new DebuggingGraalScriptEngine<>(engine);
return new DebuggingGraalScriptEngine<>(
new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null));
}
@Activate
protected void activate(BundleContext context, Map<String, ?> config) {
modified(config);
}
@Modified
protected void modified(Map<String, ?> config) {
Object injectionEnabled = config.get(CFG_INJECTION_ENABLED);
this.injectionEnabled = injectionEnabled == null || (Boolean) injectionEnabled;
}
}

View File

@ -15,22 +15,32 @@ package org.openhab.automation.jsscripting.internal;
import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AccessMode;
import java.nio.file.FileSystems;
import java.nio.file.LinkOption;
import java.nio.file.NoSuchFileException;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileAttribute;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.script.ScriptContext;
import javax.script.ScriptException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel;
import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
@ -43,32 +53,36 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
* GraalJS Script Engine implementation
*
* @author Jonathan Gilbert - Initial contribution
* @author Dan Cunningham - Script injections
*/
public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
private static final String GLOBAL_REQUIRE = "require(\"@jsscripting-globals\");";
private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
// final CommonJS search path for our library
private static final Path LOCAL_NODE_PATH = Paths.get("/node_modules");
// these fields start as null because they are populated on first use
private @NonNullByDefault({}) String engineIdentifier;
private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
private boolean initialized = false;
private String globalScript;
/**
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
* lifecycle and provides hooks for scripts to do so too.
*/
public OpenhabGraalJSScriptEngine() {
public OpenhabGraalJSScriptEngine(@Nullable String injectionCode) {
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
this.globalScript = GLOBAL_REQUIRE + (injectionCode != null ? injectionCode : "");
delegate = GraalJSScriptEngine.create(
Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
.build(),
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
.option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
.option("js.nashorn-compat", "true") // to ease
// migration
.option("js.nashorn-compat", "true") // to ease migration
.option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
// want ecma2021
.option("js.commonjs-require", "true") // enable CommonJS module support
@ -80,15 +94,52 @@ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngi
if (scriptDependencyListener != null) {
scriptDependencyListener.accept(path.toString());
}
if (path.toString().endsWith(".js")) {
SeekableByteChannel sbc = null;
if (path.startsWith(LOCAL_NODE_PATH)) {
InputStream is = getClass().getResourceAsStream(path.toString());
if (is == null) {
throw new IOException("Could not read " + path.toString());
}
sbc = new ReadOnlySeekableByteArrayChannel(is.readAllBytes());
} else {
sbc = super.newByteChannel(path, options, attrs);
}
return new PrefixedSeekableByteChannel(
("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
super.newByteChannel(path, options, attrs));
("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(), sbc);
} else {
return super.newByteChannel(path, options, attrs);
}
}
@Override
public void checkAccess(Path path, Set<? extends AccessMode> modes,
LinkOption... linkOptions) throws IOException {
if (path.startsWith(LOCAL_NODE_PATH)) {
if (getClass().getResource(path.toString()) == null) {
throw new NoSuchFileException(path.toString());
}
} else {
super.checkAccess(path, modes, linkOptions);
}
}
@Override
public Map<String, Object> readAttributes(Path path, String attributes,
LinkOption... options) throws IOException {
if (path.startsWith(LOCAL_NODE_PATH)) {
return Collections.singletonMap("isRegularFile", true);
}
return super.readAttributes(path, attributes, options);
}
@Override
public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException {
if (path.startsWith(LOCAL_NODE_PATH)) {
return path;
}
return super.toRealPath(path, linkOptions);
}
}));
}
@ -130,5 +181,11 @@ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngi
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
initialized = true;
try {
eval(globalScript);
} catch (ScriptException e) {
LOGGER.error("Could not inject global script", e);
}
}
}

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.fs;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SeekableByteChannel;
/**
* Simple wrapper around a byte array to provide a SeekableByteChannel for consumption
*
* @author Dan Cunningham - Initial contribution
*/
public class ReadOnlySeekableByteArrayChannel implements SeekableByteChannel {
private byte[] data;
private int position;
private boolean closed;
public ReadOnlySeekableByteArrayChannel(byte[] data) {
this.data = data;
}
@Override
public long position() {
return position;
}
@Override
public SeekableByteChannel position(long newPosition) throws IOException {
ensureOpen();
position = (int) Math.max(0, Math.min(newPosition, size()));
return this;
}
@Override
public long size() {
return data.length;
}
@Override
public int read(ByteBuffer buf) throws IOException {
ensureOpen();
int remaining = (int) size() - position;
if (remaining <= 0) {
return -1;
}
int readBytes = buf.remaining();
if (readBytes > remaining) {
readBytes = remaining;
}
buf.put(data, position, readBytes);
position += readBytes;
return readBytes;
}
@Override
public void close() {
closed = true;
}
@Override
public boolean isOpen() {
return !closed;
}
@Override
public int write(ByteBuffer b) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public SeekableByteChannel truncate(long newSize) {
throw new UnsupportedOperationException();
}
private void ensureOpen() throws ClosedChannelException {
if (!isOpen()) {
throw new ClosedChannelException();
}
}
}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.scope;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.openhab.core.automation.module.script.ScriptExtensionProvider;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
/**
* Base class to offer support for script extension providers
*
* @author Jonathan Gilbert - Initial contribution
*/
public abstract class AbstractScriptExtensionProvider implements ScriptExtensionProvider {
private Map<String, Function<String, Object>> types;
private Map<String, Map<String, Object>> idToTypes = new ConcurrentHashMap<>();
protected abstract String getPresetName();
protected abstract void initializeTypes(final BundleContext context);
protected void addType(String name, Function<String, Object> value) {
types.put(name, value);
}
@Activate
public void activate(final BundleContext context) {
types = new HashMap<>();
initializeTypes(context);
}
@Override
public Collection<String> getDefaultPresets() {
return Collections.emptyList();
}
@Override
public Collection<String> getPresets() {
return Collections.singleton(getPresetName());
}
@Override
public Collection<String> getTypes() {
return types.keySet();
}
@Override
public Object get(String scriptIdentifier, String type) throws IllegalArgumentException {
Map<String, Object> forScript = idToTypes.computeIfAbsent(scriptIdentifier, k -> new HashMap<>());
return forScript.computeIfAbsent(type, k -> types.get(k).apply(scriptIdentifier));
}
@Override
public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
if (getPresetName().equals(preset)) {
Map<String, Object> results = new HashMap<>(types.size());
for (String type : types.keySet()) {
results.put(type, get(scriptIdentifier, type));
}
return results;
}
return Collections.emptyMap();
}
@Override
public void unload(String scriptIdentifier) {
// ignore by default
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.scope;
//import com.oracle.truffle.js.runtime.java.adapter.JavaAdapterFactory;
/**
* Class utility to allow creation of 'extendable' classes with a classloader of the current bundle, rather than the
* classloader of the file being extended.
*
* @author Jonathan Gilbert - Initial contribution
*/
public class ClassExtender {
private ClassLoader classLoader = getClass().getClassLoader();
}

View File

@ -0,0 +1,64 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.scope;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Allows scripts to register for lifecycle events
*
* @author Jonathan Gilbert - Initial contribution
*/
public class Lifecycle implements ScriptDisposalAware {
private static final Logger logger = LoggerFactory.getLogger(Lifecycle.class);
public static final int DEFAULT_PRIORITY = 50;
private List<Hook> listeners = new ArrayList<>();
public void addDisposeHook(Consumer<Object> listener, int priority) {
addListener(listener, priority);
}
public void addDisposeHook(Consumer<Object> listener) {
addDisposeHook(listener, DEFAULT_PRIORITY);
}
private void addListener(Consumer<Object> listener, int priority) {
listeners.add(new Hook(priority, listener));
}
@Override
public void unload(String scriptIdentifier) {
try {
listeners.stream().sorted(Comparator.comparingInt(h -> h.priority))
.forEach(h -> h.fn.accept(scriptIdentifier));
} catch (RuntimeException ex) {
logger.warn("Script unloading halted due to exception in disposal: {}: {}", ex.getClass(), ex.getMessage());
}
}
private static class Hook {
public Hook(int priority, Consumer<Object> fn) {
this.priority = priority;
this.fn = fn;
}
int priority;
Consumer<Object> fn;
}
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.scope;
import org.openhab.core.automation.module.script.ScriptExtensionProvider;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Component;
/**
* ScriptExtensionProvider which provides various functions to help scripts to work with OSGi
*
* @author Jonathan Gilbert - Initial contribution
*/
@Component(immediate = true, service = ScriptExtensionProvider.class)
public class OSGiScriptExtensionProvider extends ScriptDisposalAwareScriptExtensionProvider {
@Override
protected String getPresetName() {
return "osgi";
}
@Override
protected void initializeTypes(final BundleContext context) {
ClassExtender classExtender = new ClassExtender();
addType("bundleContext", k -> context);
addType("lifecycle", k -> new Lifecycle());
addType("classutil", k -> classExtender);
}
}

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.scope;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Specifies that an object is aware of script disposal events
*
* @author Jonathan Gilbert - Initial contribution
*/
@NonNullByDefault
public interface ScriptDisposalAware {
/**
* Indicates that the script has been disposed
*
* @param scriptIdentifier the identifier for the script
*/
void unload(String scriptIdentifier);
}

View File

@ -0,0 +1,98 @@
/**
* Copyright (c) 2010-2021 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.automation.jsscripting.internal.scope;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.openhab.core.automation.module.script.ScriptExtensionProvider;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
/**
* Base class to offer support for script extension providers
*
* @author Jonathan Gilbert - Initial contribution
*/
public abstract class ScriptDisposalAwareScriptExtensionProvider
implements ScriptExtensionProvider, ScriptDisposalAware {
private Map<String, Function<String, Object>> types;
private Map<String, Map<String, Object>> idToTypes = new ConcurrentHashMap<>();
protected abstract String getPresetName();
protected abstract void initializeTypes(final BundleContext context);
protected void addType(String name, Function<String, Object> value) {
types.put(name, value);
}
@Activate
public void activate(final BundleContext context) {
types = new HashMap<>();
initializeTypes(context);
}
@Override
public Collection<String> getDefaultPresets() {
return Collections.emptyList();
}
@Override
public Collection<String> getPresets() {
return Collections.singleton(getPresetName());
}
@Override
public Collection<String> getTypes() {
return types.keySet();
}
@Override
public Object get(String scriptIdentifier, String type) throws IllegalArgumentException {
Map<String, Object> forScript = idToTypes.computeIfAbsent(scriptIdentifier, k -> new HashMap<>());
return forScript.computeIfAbsent(type, k -> types.get(k).apply(scriptIdentifier));
}
@Override
public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
if (getPresetName().equals(preset)) {
Map<String, Object> results = new HashMap<>(types.size());
for (String type : types.keySet()) {
results.put(type, get(scriptIdentifier, type));
}
return results;
}
return Collections.emptyMap();
}
@Override
public void unload(String scriptIdentifier) {
Map<String, Object> forScript = idToTypes.remove(scriptIdentifier);
if (forScript != null) {
for (Object o : forScript.values()) {
if (o instanceof ScriptDisposalAware) {
((ScriptDisposalAware) o).unload(scriptIdentifier);
}
}
}
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="automation:jsscripting">
<parameter name="injectionEnabled" type="boolean" required="true">
<label>Use Built-in Global Variables</label>
<description><![CDATA[ Import all variables from the OH scripting library into all rules for common services like items, things, actions, log, etc... <br>
If disabled, the OH scripting library can be imported manually using "<i>require('openhab')</i>"
]]></description>
<options>
<option value="true">Use Built-in Variables</option>
<option value="false">Do Not Use Built-in Variables</option>
</options>
<default>true</default>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@ -0,0 +1,204 @@
(function (global) {
'use strict';
const System = Java.type('java.lang.System');
const log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.automation.script");
const ScriptExecution = Java.type('org.openhab.core.model.script.actions.ScriptExecution');
const ZonedDateTime = Java.type('java.time.ZonedDateTime');
const formatRegExp = /%[sdj%]/g;
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 = {};
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>']));
}
}
}
};
function setTimeout(cb, delay) {
const args = Array.prototype.slice.call(arguments, 2);
return ScriptExecution.createTimerWithArgument(
ZonedDateTime.now().plusNanos(delay * 1000000),
args,
function (args) {
cb.apply(global, args);
}
);
}
function clearTimeout(timer) {
if (timer !== undefined && timer.isActive()) {
timer.cancel();
}
}
function setInterval(cb, delay) {
const args = Array.prototype.slice.call(arguments, 2);
const delayNanos = delay * 1000000
let timer = ScriptExecution.createTimerWithArgument(
ZonedDateTime.now().plusNanos(delayNanos),
args,
function (args) {
cb.apply(global, args);
if (!timer.isCancelled()) {
timer.reschedule(ZonedDateTime.now().plusNanos(delayNanos));
}
}
);
return timer;
}
function clearInterval(timer) {
clearTimeout(timer);
}
//Polyfil common functions onto the global object
globalThis.console = console;
globalThis.setTimeout = setTimeout;
globalThis.clearTimeout = clearTimeout;
globalThis.setInterval = setInterval;
globalThis.clearInterval = clearInterval;
//Support legacy NodeJS libraries
globalThis.global = globalThis;
globalThis.process = { env: { NODE_ENV: '' } };
})(this);