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] ES6+ Support #8516

Merged
merged 3 commits into from
May 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

# Add-on maintainers:
/bundles/org.openhab.automation.groovyscripting/ @wborn
/bundles/org.openhab.automation.jsscripting/ @jpg0
/bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers
/bundles/org.openhab.automation.pidcontroller/ @fwolter
/bundles/org.openhab.binding.adorne/ @theiding
Expand Down
5 changes: 5 additions & 0 deletions bom/openhab-addons/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@
<artifactId>org.openhab.automation.pidcontroller</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.automation.jsscripting</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.adorne</artifactId>
Expand Down
13 changes: 13 additions & 0 deletions bundles/org.openhab.automation.jsscripting/NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.

* Project home: https://www.openhab.org

== Declared Project Licenses

This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.

== Source Code

https://github.com/openhab/openhab-addons
43 changes: 43 additions & 0 deletions bundles/org.openhab.automation.jsscripting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# JavaScript Scripting

This add-on provides support for JavaScript (ECMAScript 2021+) that can be used as a scripting language within automation rules.

## Creating JavaScript Scripts

When this add-on is installed, JavaScript script actions will be run by this add-on and allow ECMAScript 2021+ features.

Alternatively, you can create scripts in the `automation/jsr223` configuration directory.
If you create an empty file called `test.js`, you will see a log line with information similar to:

```text
... [INFO ] [.a.m.s.r.i.l.ScriptFileWatcher:150 ] - Loading script 'test.js'
```

To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to enable debug logging for the automation functionality:

```text
log:set DEBUG org.openhab.core.automation
```

For more information on the available APIs in scripts see the [JSR223 Scripting]({{base}}/configuration/jsr223.html) documentation.

## Script Examples

JavaScript scripts provide access to almost all the functionality in an openHAB runtime environment.
As a simple example, the following script logs "Hello, World!".
Note that `console.log` will usually not work since the output has no terminal to display the text.
The openHAB server uses the [SLF4J](https://www.slf4j.org/) library for logging.

```js
const LoggerFactory = Java.type('org.slf4j.LoggerFactory');

LoggerFactory.getLogger("org.openhab.core.automation.examples").info("Hello world!");
```

Depending on the openHAB logging configuration, you may need to prefix logger names with `org.openhab.core.automation` for them to show up in the log file (or you modify the logging configuration).

The script uses the [LoggerFactory](https://www.slf4j.org/apidocs/org/slf4j/Logger.html) to obtain a named logger and then logs a message like:

```text
... [INFO ] [.openhab.core.automation.examples:-2 ] - Hello world!
```
13 changes: 13 additions & 0 deletions bundles/org.openhab.automation.jsscripting/bnd.bnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Bundle-SymbolicName: ${project.artifactId}
DynamicImport-Package: *
Import-Package: org.openhab.core.automation.module.script,javax.management,javax.script,javax.xml.datatype,javax.xml.stream;version="[1.0,2)",org.osgi.framework;version="[1.8,2)",org.slf4j;version="[1.7,2)"
Require-Capability: osgi.extender;
filter:="(osgi.extender=osgi.serviceloader.processor)",
osgi.serviceloader;
filter:="(osgi.serviceloader=org.graalvm.polyglot.impl.AbstractPolyglotImpl)";
cardinality:=multiple

SPI-Provider: *
SPI-Consumer: *

-fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning
115 changes: 115 additions & 0 deletions bundles/org.openhab.automation.jsscripting/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.1.0-SNAPSHOT</version>
</parent>

<artifactId>org.openhab.automation.jsscripting</artifactId>

<name>openHAB Add-ons :: Bundles :: Automation :: JSScripting</name>

<properties>
<bnd.importpackage>
!sun.misc.*,
!sun.reflect.*,
!com.sun.management.*,
!jdk.internal.reflect.*,
!jdk.vm.ci.services
</bnd.importpackage>
Comment on lines +19 to +25
Copy link
Member

Choose a reason for hiding this comment

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

Is it possible to also add this to the bnd.bnd file?

<graal.version>20.1.0</graal.version>
<asm.version>6.2.1</asm.version>
<oh.version>${project.version}</oh.version>
</properties>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>embed-dependencies</id>
Copy link
Member

Choose a reason for hiding this comment

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

Did you look into embedding this content with BND?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have not, but I will do...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason that this is in here is because Graal uses ServiceLoaders to load its language definitions, and has one per language per jar. This means that for each jar there is a single META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider file which is used to inform the ServiceLoader. The problem we have is that we need two languages (javascript & tregex), but when the multiple jars are all expanded and added into this bundle, we end up with a single one of the above files (as they are present at the same path in the jar). The way that I have got around this is to provide my own version of this file, with both languages in it, and exclude the ones coming from the Graal jars (this declaration in the pom.xml just overrides the inherited one and instructs it to exclude this file).

It seems that there are a few options here:

  • Do what I've done so far
  • Attempt to merge the files from the distinct jars (I tried this, it was a lot of complexity wrestling with Maven)
  • Bundle the jars into META-INF/lib rather than repack them (I'm aware that this isn't recommended for openHAB)
  • Attempt to put my own file over the top of whatever gets included by inserted it at a later build phase. Maybe this is what you mean by using BND?

Copy link
Member

Choose a reason for hiding this comment

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

Sounds like something non-trivial to rework. So let's stick with what works. 😉

<goals>
<goal>unpack-dependencies</goal>
</goals>
<configuration>
<excludes>META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider</excludes> <!-- we'll provide this -->
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>org.graalvm.truffle</groupId>
<artifactId>truffle-api</artifactId>
<version>${graal.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>${graal.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-launcher</artifactId>
<version>${graal.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>${graal.version}</version>
</dependency>
<dependency>
<groupId>org.graalvm.regex</groupId>
<artifactId>regex</artifactId>
<version>${graal.version}</version>
</dependency>
<dependency> <!-- this must come AFTER the regex lib -->
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>${graal.version}</version>
</dependency>
<dependency>
<groupId>com.ibm.icu</groupId>
<artifactId>icu4j</artifactId>
<version>62.1</version>
</dependency>

<!-- include as version required is older than OH provides -->
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>${asm.version}</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-analysis</artifactId>
<version>${asm.version}</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>

<features name="org.openhab.automation.jsscripting-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>

<feature name="openhab-automation-jsscripting" description="JSScripting" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.jsscripting/${project.version}</bundle>
</feature>
</features>
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* 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;

import com.oracle.truffle.js.runtime.java.adapter.JavaAdapterFactory;

/**
* Class utility to allow creation of 'extendable' classes with a classloader of the GraalJS bundle, rather than the
* classloader of the file being extended.
*
* @author Jonathan Gilbert - Initial contribution
*/
public class ClassExtender {
private static ClassLoader classLoader = ClassExtender.class.getClassLoader();

public static Object extend(String className) {
try {
return extend(Class.forName(className));
} catch (ClassNotFoundException e) {
throw new RuntimeException("Cannot find class " + className, e);
}
}

public static Object extend(Class<?> clazz) {
return JavaAdapterFactory.getAdapterClassFor(clazz, null, classLoader);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* 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;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.graalvm.polyglot.PolyglotException;
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Wraps ScriptEngines provided by Graal to provide error messages and stack traces for scripts.
*
* @author Jonathan Gilbert - Initial contribution
*/
@NonNullByDefault
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable>
extends InvocationInterceptingScriptEngineWithInvocable<T> {

private static final Logger stackLogger = LoggerFactory.getLogger("org.openhab.automation.script.javascript.stack");

public DebuggingGraalScriptEngine(T delegate) {
super(delegate);
}

@Override
public ScriptException afterThrowsInvocation(ScriptException se) {
Throwable cause = se.getCause();
if (cause instanceof PolyglotException) {
stackLogger.error("Failed to execute script:", cause);
}
return se;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* 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;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.script.ScriptEngine;

import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.osgi.service.component.annotations.Component;

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

/**
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
*
* @author Jonathan Gilbert - Initial contribution
*/
@Component(service = ScriptEngineFactory.class)
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {

@Override
public List<String> getScriptTypes() {
List<String> scriptTypes = new ArrayList<>();
GraalJSEngineFactory graalJSEngineFactory = new GraalJSEngineFactory();

scriptTypes.addAll(graalJSEngineFactory.getMimeTypes());
scriptTypes.addAll(graalJSEngineFactory.getExtensions());

return Collections.unmodifiableList(scriptTypes);
}

@Override
public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValues) {
// noop; the are retrieved via modules, not injected
}

@Override
public ScriptEngine createScriptEngine(String scriptType) {
OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
return new DebuggingGraalScriptEngine<>(engine);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* 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;

import java.util.Optional;

import org.graalvm.polyglot.Value;

/**
* Locates modules from a module name
*
* @author Jonathan Gilbert - Initial contribution
*/
public interface ModuleLocator {
Optional<Value> locateModule(String name);
}
Loading