]> git.basschouten.com Git - openhab-addons.git/blob
6621d5d1a157833287d13d1522530374022aaeff
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.automation.jsscripting.internal;
14
15 import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
16
17 import java.io.IOException;
18 import java.nio.channels.SeekableByteChannel;
19 import java.nio.file.FileSystems;
20 import java.nio.file.OpenOption;
21 import java.nio.file.Path;
22 import java.nio.file.attribute.FileAttribute;
23 import java.util.Set;
24 import java.util.function.Consumer;
25 import java.util.function.Function;
26
27 import javax.script.ScriptContext;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.graalvm.polyglot.Context;
31 import org.graalvm.polyglot.Engine;
32 import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
33 import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
34 import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
35 import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
36 import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
41
42 /**
43  * GraalJS Script Engine implementation
44  *
45  * @author Jonathan Gilbert - Initial contribution
46  */
47 public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
48
49     private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
50
51     private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
52
53     // these fields start as null because they are populated on first use
54     private @NonNullByDefault({}) String engineIdentifier;
55     private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
56
57     private boolean initialized = false;
58
59     /**
60      * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
61      * lifecycle and provides hooks for scripts to do so too.
62      */
63     public OpenhabGraalJSScriptEngine() {
64         super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
65         delegate = GraalJSScriptEngine.create(
66                 Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
67                         .build(),
68                 Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
69                         .option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
70                         .option("js.nashorn-compat", "true") // to ease
71                         // migration
72                         .option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
73                                                                  // want ecma2021
74                         .option("js.commonjs-require", "true") // enable CommonJS module support
75                         .hostClassLoader(getClass().getClassLoader())
76                         .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
77                             @Override
78                             public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
79                                     FileAttribute<?>... attrs) throws IOException {
80                                 if (scriptDependencyListener != null) {
81                                     scriptDependencyListener.accept(path.toString());
82                                 }
83
84                                 if (path.toString().endsWith(".js")) {
85                                     return new PrefixedSeekableByteChannel(
86                                             ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
87                                             super.newByteChannel(path, options, attrs));
88                                 } else {
89                                     return super.newByteChannel(path, options, attrs);
90                                 }
91                             }
92                         }));
93     }
94
95     @Override
96     protected void beforeInvocation() {
97         if (initialized) {
98             return;
99         }
100
101         ScriptContext ctx = delegate.getContext();
102
103         // these are added post-construction, so we need to fetch them late
104         this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
105         if (this.engineIdentifier == null) {
106             throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings");
107         }
108
109         ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
110                 .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
111         if (scriptExtensionAccessor == null) {
112             throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings");
113         }
114
115         scriptDependencyListener = (Consumer<String>) ctx
116                 .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
117         if (scriptDependencyListener == null) {
118             LOGGER.warn(
119                     "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
120         }
121
122         ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
123                 scriptExtensionAccessor);
124
125         Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
126                 .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
127                 .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
128
129         delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
130         delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
131
132         initialized = true;
133     }
134 }