<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>
</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>
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";
@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;
}
}
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;
* 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
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);
+ }
}));
}
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);
+ }
}
}
--- /dev/null
+/**
+ * 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();
+ }
+ }
+}
--- /dev/null
+/**
+ * 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
+ }
+}
--- /dev/null
+/**
+ * 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();
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+<?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>
--- /dev/null
+
+(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);