2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
14 package org.openhab.automation.jsscripting.internal;
16 import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
19 import java.io.IOException;
20 import java.nio.channels.SeekableByteChannel;
21 import java.nio.file.FileSystems;
22 import java.nio.file.OpenOption;
23 import java.nio.file.Path;
24 import java.nio.file.attribute.FileAttribute;
26 import java.util.function.Consumer;
27 import java.util.function.Function;
29 import javax.script.ScriptContext;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.graalvm.polyglot.Context;
33 import org.graalvm.polyglot.Engine;
34 import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
35 import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
36 import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
37 import org.openhab.core.OpenHAB;
38 import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
39 import org.slf4j.Logger;
40 import org.slf4j.LoggerFactory;
42 import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
45 * GraalJS Script Engine implementation
47 * @author Jonathan Gilbert - Initial contribution
49 public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
51 private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
53 private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
54 private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
55 "javascript", "personal");
57 // these fields start as null because they are populated on first use
58 private @NonNullByDefault({}) String engineIdentifier;
59 private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
61 private boolean initialized = false;
64 * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
65 * lifecycle and provides hooks for scripts to do so too.
67 public OpenhabGraalJSScriptEngine() {
68 super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
69 delegate = GraalJSScriptEngine.create(
70 Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
72 Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
73 .option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease
75 .option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
77 .option("js.commonjs-require", "true") // enable CommonJS module support
78 .hostClassLoader(getClass().getClassLoader())
79 .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
81 public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
82 FileAttribute<?>... attrs) throws IOException {
83 if (scriptDependencyListener != null) {
84 scriptDependencyListener.accept(path.toString());
87 if (path.toString().endsWith(".js")) {
88 return new PrefixedSeekableByteChannel(
89 ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
90 super.newByteChannel(path, options, attrs));
92 return super.newByteChannel(path, options, attrs);
99 protected void beforeInvocation() {
104 ScriptContext ctx = delegate.getContext();
106 // these are added post-construction, so we need to fetch them late
107 this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
108 if (this.engineIdentifier == null) {
109 throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings");
112 ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
113 .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
114 if (scriptExtensionAccessor == null) {
115 throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings");
118 scriptDependencyListener = (Consumer<String>) ctx
119 .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
120 if (scriptDependencyListener == null) {
122 "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
125 ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
126 scriptExtensionAccessor);
128 Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
129 .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
130 .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
132 delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
133 delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));