@NonNullByDefault
public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
- private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
+ private static final String CFG_USE_INCLUDED_LIBRARY = "useIncludedLibrary";
private static final GraalJSEngineFactory factory = new GraalJSEngineFactory();
}
private boolean injectionEnabled = true;
+ private boolean useIncludedLibrary = true;
private final JSScriptServiceUtil jsScriptServiceUtil;
private final JSDependencyTracker jsDependencyTracker;
return null;
}
return new DebuggingGraalScriptEngine<>(
- new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null, jsScriptServiceUtil));
+ new OpenhabGraalJSScriptEngine(injectionEnabled, useIncludedLibrary, jsScriptServiceUtil));
}
@Override
@Modified
protected void modified(Map<String, ?> config) {
Object injectionEnabled = config.get(CFG_INJECTION_ENABLED);
- this.injectionEnabled = injectionEnabled == null || (Boolean) injectionEnabled;
+ this.injectionEnabled = injectionEnabled == null || (boolean) injectionEnabled;
+ Object useIncludedLibrary = config.get(CFG_USE_INCLUDED_LIBRARY);
+ this.useIncludedLibrary = useIncludedLibrary == null || (boolean) useIncludedLibrary;
}
}
* @author Dan Cunningham - Script injections
* @author Florian Hotze - Create lock object for multi-thread synchronization; Inject the {@link JSRuntimeFeatures}
* into the JS context; Fix memory leak caused by HostObject by making HostAccess reference static; Switch to
- * {@link Lock} for multi-thread synchronization
+ * {@link Lock} for multi-thread synchronization; globals & openhab-js injection code caching
*/
public class OpenhabGraalJSScriptEngine
extends InvocationInterceptingScriptEngineWithInvocableAndAutoCloseable<GraalJSScriptEngine> {
GLOBAL_SOURCE = Source.newBuilder("js", getFileAsReader("node_modules/@jsscripting-globals.js"),
"@jsscripting-globals.js").cached(true).build();
} catch (IOException e) {
- LOGGER.error("Failed to load global script", e);
+ throw new RuntimeException("Failed to load @jsscripting-globals.js", e);
}
}
+ private static Source OPENHAB_JS_SOURCE;
+
+ static {
+ try {
+ OPENHAB_JS_SOURCE = Source
+ .newBuilder("js", getFileAsReader("node_modules/@openhab-globals.js"), "@openhab-globals.js")
+ .cached(true).build();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load @openhab-globals.js", e);
+ }
+ }
+ private static String OPENHAB_JS_INJECTION_CODE = "Object.assign(this, require('openhab'));";
+
private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
/** Final CommonJS search path for our library */
private static final Path NODE_DIR = Paths.get("node_modules");
private @Nullable Consumer<String> scriptDependencyListener;
private boolean initialized = false;
- private final String injectionCode;
+ private final boolean injectionEnabled;
+ private final boolean useIncludedLibrary;
/**
* 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(@Nullable String injectionCode, JSScriptServiceUtil jsScriptServiceUtil) {
+ public OpenhabGraalJSScriptEngine(boolean injectionEnabled, boolean useIncludedLibrary,
+ JSScriptServiceUtil jsScriptServiceUtil) {
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
- this.injectionCode = (injectionCode != null ? injectionCode : "");
+ this.injectionEnabled = injectionEnabled;
+ this.useIncludedLibrary = useIncludedLibrary;
this.jsRuntimeFeatures = jsScriptServiceUtil.getJSRuntimeFeatures(lock);
LOGGER.debug("Initializing GraalJS script engine...");
ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
scriptExtensionAccessor, lock);
+ // Wrap the "require" function to also allow loading modules from the ScriptExtensionModuleProvider
Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
.locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
.map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
-
delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
- // Injections into the JS runtime
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
+
+ // Injections into the JS runtime
jsRuntimeFeatures.getFeatures().forEach((key, obj) -> {
LOGGER.debug("Injecting {} into the JS runtime...", key);
delegate.put(key, obj);
initialized = true;
try {
- LOGGER.debug("Evaluating global script...");
+ LOGGER.debug("Evaluating cached global script...");
delegate.getPolyglotContext().eval(GLOBAL_SOURCE);
- eval(injectionCode);
+ if (this.injectionEnabled) {
+ if (this.useIncludedLibrary) {
+ LOGGER.debug("Evaluating cached openhab-js injection...");
+ delegate.getPolyglotContext().eval(OPENHAB_JS_SOURCE);
+ } else {
+ LOGGER.debug("Evaluating openhab-js injection from the file system...");
+ eval(OPENHAB_JS_INJECTION_CODE);
+ }
+ }
LOGGER.debug("Successfully initialized GraalJS script engine.");
} catch (ScriptException e) {
LOGGER.error("Could not inject global script", e);
* @param fileName filename relative to the resources folder
* @return file as {@link InputStreamReader}
*/
- private static Reader getFileAsReader(String fileName) {
+ private static Reader getFileAsReader(String fileName) throws IOException {
InputStream ioStream = OpenhabGraalJSScriptEngine.class.getClassLoader().getResourceAsStream(fileName);
if (ioStream == null) {
- throw new IllegalArgumentException(fileName + " not found");
+ throw new IOException(fileName + " not found");
}
return new InputStreamReader(ioStream);
<config-description uri="automation:jsscripting">
<parameter name="injectionEnabled" type="boolean" required="true">
<label>Use Built-in Global Variables</label>
- <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><![CDATA[
+ Import all variables from the openHAB JavaScript library into all rules for common services like items, things, actions, log, etc... <br>
+ If disabled, the openHAB JavaScript library can be imported manually using "<i>require('openhab')</i>"
]]></description>
<options>
<option value="true">Use Built-in Variables</option>
</options>
<default>true</default>
</parameter>
+ <parameter name="useIncludedLibrary" type="boolean" required="true">
+ <label>Use Included openHAB JavaScript Library</label>
+ <description><![CDATA[
+ Use the included openHAB JavaScript library for optimal performance.<br>
+ Disable this option to allow loading the library from the local user configuration directory "automation/js/node_modules". Using a user provided version of the library may increase script loading times, especially on less powerful systems.
+ ]]></description>
+ <options>
+ <option value="true">Use Included Library</option>
+ <option value="false">Do Not Use Included Library</option>
+ </options>
+ <default>true</default>
+ </parameter>
</config-description>
</config-description:config-descriptions>
automation.config.jsscripting.injectionEnabled.label = Use Built-in Global Variables
-automation.config.jsscripting.injectionEnabled.description = 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>"
+automation.config.jsscripting.injectionEnabled.description = Import all variables from the openHAB JavaScript library into all rules for common services like items, things, actions, log, etc... <br> If disabled, the openHAB JavaScript library can be imported manually using "<i>require('openhab')</i>"
automation.config.jsscripting.injectionEnabled.option.true = Use Built-in Variables
automation.config.jsscripting.injectionEnabled.option.false = Do Not Use Built-in Variables
+automation.config.jsscripting.useIncludedLibrary.label = Use Included openHAB JavaScript Library
+automation.config.jsscripting.useIncludedLibrary.description = Use the included openHAB JavaScript library for optimal performance.<br> Disable this option to allow loading the library from the local user configuration directory "automation/js/node_modules". Using a user provided version of the library may increase script loading times, especially on less powerful systems.
+automation.config.jsscripting.useIncludedLibrary.option.true = Use Included Library
+automation.config.jsscripting.useIncludedLibrary.option.false = Do Not Use Included Library
# service