]> git.basschouten.com Git - openhab-addons.git/blob
9f29b0d99d2c768072a1ec1e4ec6c725f3e91f72
[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
14 package org.openhab.automation.jsscripting.internal;
15
16 import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
17
18 import java.io.File;
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;
25 import java.util.Set;
26 import java.util.function.Consumer;
27 import java.util.function.Function;
28
29 import javax.script.ScriptContext;
30
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;
41
42 import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
43
44 /**
45  * GraalJS Script Engine implementation
46  *
47  * @author Jonathan Gilbert - Initial contribution
48  */
49 public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
50
51     private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
52
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");
56
57     // these fields start as null because they are populated on first use
58     private @NonNullByDefault({}) String engineIdentifier;
59     private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
60
61     private boolean initialized = false;
62
63     /**
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.
66      */
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")
71                         .build(),
72                 Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
73                         .option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease
74                                                                                                            // migration
75                         .option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
76                                                                  // want ecma2021
77                         .option("js.commonjs-require", "true") // enable CommonJS module support
78                         .hostClassLoader(getClass().getClassLoader())
79                         .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
80                             @Override
81                             public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
82                                     FileAttribute<?>... attrs) throws IOException {
83                                 if (scriptDependencyListener != null) {
84                                     scriptDependencyListener.accept(path.toString());
85                                 }
86
87                                 if (path.toString().endsWith(".js")) {
88                                     return new PrefixedSeekableByteChannel(
89                                             ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
90                                             super.newByteChannel(path, options, attrs));
91                                 } else {
92                                     return super.newByteChannel(path, options, attrs);
93                                 }
94                             }
95                         }));
96     }
97
98     @Override
99     protected void beforeInvocation() {
100         if (initialized) {
101             return;
102         }
103
104         ScriptContext ctx = delegate.getContext();
105
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");
110         }
111
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");
116         }
117
118         scriptDependencyListener = (Consumer<String>) ctx
119                 .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
120         if (scriptDependencyListener == null) {
121             LOGGER.warn(
122                     "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
123         }
124
125         ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
126                 scriptExtensionAccessor);
127
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 }));
131
132         delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
133         delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
134
135         initialized = true;
136     }
137 }