Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jsscripting] openhab-js integration #11656

Merged
merged 26 commits into from
Dec 13, 2021
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
377251a
Tell the script context to use the classloader of the current class.
digitaldan Oct 17, 2021
394de63
Protoype of merging OHJ with jsscripting
digitaldan Oct 20, 2021
18237a1
Load OH JS file directly from resource, use @oh namespace
digitaldan Oct 23, 2021
74be917
Remove debug statement
digitaldan Oct 23, 2021
57d4c80
add allItems
digitaldan Oct 25, 2021
ed51c5d
JS changes
digitaldan Nov 3, 2021
1a14a3d
Adds injection code and makes this a configurable service
digitaldan Nov 11, 2021
831bb7f
revert small typor
digitaldan Nov 11, 2021
2e0152d
Merge branch 'main' into jsscripting-ohj
digitaldan Nov 12, 2021
91e1033
adds global vars, cleans up local library loading
digitaldan Nov 19, 2021
f99e8af
Typo
digitaldan Nov 19, 2021
003f650
Use npm supplied `openhab` scripting library
digitaldan Nov 28, 2021
ba9c5a9
Update author headers
digitaldan Nov 28, 2021
fce7efc
Revert unattended change
digitaldan Nov 28, 2021
b622b44
adds setInterval and supports arguments for timers
digitaldan Dec 4, 2021
f299e2b
change console logging to "org.openhab.automation.script'
digitaldan Dec 4, 2021
7073421
Wrap logic in function for safety, apply callback in safer way
digitaldan Dec 4, 2021
2465d92
Change scripting logger name
digitaldan Dec 5, 2021
d149195
Add lifecyle and bundle context support, cleans up global scripting a…
digitaldan Dec 5, 2021
52dcb72
Bump openhab-js version
digitaldan Dec 5, 2021
d60e071
spotless
digitaldan Dec 5, 2021
e268143
Merge branch 'main' into jsscripting-ohj
digitaldan Dec 11, 2021
5d0ff0a
Merge branch 'main' into HEAD
digitaldan Dec 13, 2021
88a712c
remove unessesary binding provider classes, updated class docs
digitaldan Dec 13, 2021
4afd5c9
Bump openhab NPM version
digitaldan Dec 13, 2021
2054bca
Update bundles/org.openhab.automation.jsscripting/src/main/java/org/o…
kaikreuzer Dec 13, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions bundles/org.openhab.automation.jsscripting/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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.1</ohjs.version>
</properties>

<build>
Expand All @@ -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>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,31 @@
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.oracle.truffle.js.scriptengine.GraalJSEngineFactory;

/**
* 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our configuration pids follow the pattern org.openhab.<serviceid>.

+ "=org.openhab.automation.jsscripting")
@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(GraalJSScriptEngineFactory.class);
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
private boolean injectionEnabled;

@Override
public List<String> getScriptTypes() {
Expand All @@ -50,7 +64,18 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu

@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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,32 @@

import java.io.File;
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.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
import org.openhab.core.OpenHAB;
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
Expand All @@ -45,32 +55,38 @@
* 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__";
private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
"javascript", "personal");
// 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", MODULE_DIR).option("js.nashorn-compat", "true") // to ease
.option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to
// ease
// migration
.option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
// want ecma2021
Expand All @@ -83,15 +99,52 @@ public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> o
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);
}
}));
}

Expand Down Expand Up @@ -133,5 +186,11 @@ protected void beforeInvocation() {
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));

initialized = true;

try {
eval(globalScript);
} catch (ScriptException e) {
LOGGER.error("Could not inject gloabl script", e);
kaikreuzer marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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>
digitaldan marked this conversation as resolved.
Show resolved Hide resolved
<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>
Loading