Skip to content

Commit

Permalink
Cleaned up
Browse files Browse the repository at this point in the history
  • Loading branch information
jpg0 committed Dec 14, 2020
1 parent 2cb72ec commit 08409e5
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 713 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,11 @@

package org.openhab.automation.module.script.graaljs.internal;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Optional;

import javax.script.*;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.graalvm.polyglot.PolyglotException;
import org.openhab.automation.module.script.graaljs.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -34,104 +27,21 @@
* @author Jonathan Gilbert - Initial contribution
*/
@NonNullByDefault
class DebuggingGraalScriptEngine {
class DebuggingGraalScriptEngine<T extends ScriptEngine & Invocable>
extends InvocationInterceptingScriptEngineWithInvocable<T> {

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

private ScriptEngine engine;

private DebuggingGraalScriptEngine(ScriptEngine engine) {
this.engine = engine;
}

@Nullable
private static Method EVAL_WITH_READER_METHOD;

static {
try {
EVAL_WITH_READER_METHOD = ScriptEngine.class.getDeclaredMethod("eval", Reader.class);
} catch (NoSuchMethodException e) {
logger.warn("Failed to load ScriptEngine.eval(Reader) method: {}", e.getMessage());
}
}

/**
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that logs
* PolyglotExceptions
* that are thrown from any 'eval' methods.
*
* @return a ScriptEngine which logs script exceptions
*/
static ScriptEngine create(ScriptEngine engine) {
return new DebuggingGraalScriptEngine(engine).createProxy();
}

private ScriptEngine createProxy() {
return (ScriptEngine) Proxy.newProxyInstance(ScriptEngine.class.getClassLoader(),
new Class<?>[] { ScriptEngine.class, Invocable.class }, (proxy, method, args) -> {
try {
if (method.getName().equals("eval")) {
injectFilename(method, args);
return withStackLogger(() -> method.invoke(engine, args));
} else if (method.getName().equals("invokeFunction")) {
return withStackLogger(() -> method.invoke(engine, args));
} else {
return method.invoke(engine, args);
}
} catch (InvocationTargetException ite) {
throw ite.getTargetException();
}
});
public DebuggingGraalScriptEngine(T delegate) {
super(delegate);
}

private void injectFilename(Method method, Object[] args) {
// if this is the eval(Reader) version, attempt inject the script name
if (method.equals(EVAL_WITH_READER_METHOD)) {
findPathForReader((Reader) args[0]).ifPresent(filename -> engine.getContext()
.setAttribute(ScriptEngine.FILENAME, filename, ScriptContext.ENGINE_SCOPE));
@Override
public ScriptException afterThrowsInvocation(ScriptException se) {
Throwable cause = se.getCause();
if (cause instanceof PolyglotException) {
stackLogger.error("Failed to execute script:", cause);
}
}

/**
* Logs error with JS stack trace if it's caused by a PolyglotException (e.g. the script caused the error)
*/
private Object withStackLogger(ObjectInvocable toExecute) throws InvocationTargetException, IllegalAccessException {
try {
return toExecute.invoke();
} catch (InvocationTargetException ite) {
Throwable cause = ite.getTargetException().getCause();
if (cause instanceof PolyglotException) {
stackLogger.error("Failed to execute script:", cause);
}
throw ite;
}
}

/**
* Nasty hack to opportunistically extract the path from the passed Reader. Not currently possible as insufficient
* information is passed. Not guaranteed to work, fragile and depends on caller implementation.
*/
private Optional<String> findPathForReader(Reader reader) {
try {
Field f = Reader.class.getDeclaredField("lock");
f.setAccessible(true);
BufferedInputStream bif = (BufferedInputStream) f.get(reader);
Field f2 = FilterInputStream.class.getDeclaredField("in");
f2.setAccessible(true);
FileInputStream fis = (FileInputStream) f2.get(f2.get(bif));
Field f3 = FileInputStream.class.getDeclaredField("path");
f3.setAccessible(true);
String fullpath = (String) f3.get(fis);
return Optional.of(new File(fullpath).getName());
} catch (Exception e) {
logger.warn("Failed to extract path for source file: {}", e.getMessage());
return Optional.empty();
}
}

@FunctionalInterface
private interface ObjectInvocable {
Object invoke() throws InvocationTargetException, IllegalAccessException;
return se;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import javax.script.ScriptEngine;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.automation.module.script.graaljs.internal.commonjs.graaljs.GraalJSCommonJSScriptEngine;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.osgi.service.component.annotations.Component;

Expand Down Expand Up @@ -50,7 +49,7 @@ public void scopeValues(ScriptEngine scriptEngine, Map<String, Object> scopeValu

@Override
public ScriptEngine createScriptEngine(String scriptType) {
ScriptEngine engine = new GraalJSCommonJSScriptEngine().createProxy();
return DebuggingGraalScriptEngine.create(engine);
OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
return new DebuggingGraalScriptEngine<>(engine);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,12 @@
* SPDX-License-Identifier: EPL-2.0
*/

package org.openhab.automation.module.script.graaljs.internal.commonjs.graaljs;
package org.openhab.automation.module.script.graaljs.internal;

import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileSystems;
import java.nio.file.OpenOption;
Expand All @@ -31,12 +29,14 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.graalvm.polyglot.Context;
import org.openhab.automation.module.script.graaljs.internal.commonjs.ScriptExtensionModuleProvider;
import org.openhab.automation.module.script.graaljs.internal.commonjs.graaljs.fs.DelegatingFileSystem;
import org.openhab.automation.module.script.graaljs.internal.commonjs.graaljs.fs.PrefixedSeekableByteChannel;
import org.openhab.automation.module.script.graaljs.internal.fs.DelegatingFileSystem;
import org.openhab.automation.module.script.graaljs.internal.fs.PrefixedSeekableByteChannel;
import org.openhab.automation.module.script.graaljs.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
import org.openhab.core.OpenHAB;
import org.openhab.core.automation.module.script.ScriptDependencyListener;
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

Expand All @@ -45,11 +45,14 @@
*
* @author Jonathan Gilbert - Initial contribution
*/
public class GraalJSCommonJSScriptEngine {
@NonNullByDefault
public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {

private final GraalJSScriptEngine engine;
private static final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);

private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
"javascript", "personal");

// these fields start as null because they are populated on first use
@NonNullByDefault({})
Expand All @@ -63,19 +66,20 @@ public class GraalJSCommonJSScriptEngine {
* 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 GraalJSCommonJSScriptEngine() {
this.engine = GraalJSScriptEngine.create(null,
public OpenhabGraalJSScriptEngine() {
super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
delegate = GraalJSScriptEngine.create(null,
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
.option("js.commonjs-require-cwd",
String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
"javascript", "personal"))
.option("js.nashorn-compat", "true") // to ease migration
.option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease
// migration
.option("js.commonjs-require", "true") // enable CommonJS module support
.fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
@Override
public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
FileAttribute<?>... attrs) throws IOException {
onLibLoaded(path.toString());
if (scriptDependencyListener != null) {
scriptDependencyListener.addDependency(path.toString());
}

if (path.toString().endsWith(".js")) {
return new PrefixedSeekableByteChannel(
Expand All @@ -88,11 +92,12 @@ public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> o
}));
}

private void initialize() {
@Override
protected void beforeInvocation() {
if (initialized)
return;

ScriptContext ctx = this.engine.getContext();
ScriptContext ctx = delegate.getContext();

// these are added post-construction, so we need to fetch them late
this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
Expand All @@ -108,41 +113,20 @@ private void initialize() {

scriptDependencyListener = (ScriptDependencyListener) ctx.getAttribute(CONTEXT_KEY_DEPENDENCY_LISTENER);
if (scriptDependencyListener == null) {
throw new IllegalStateException(
"Failed to retrieve script script dependency listener from engine bindings");
logger.warn(
"Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
}

ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
scriptExtensionAccessor);

Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
.locatorFor(engine.getPolyglotContext(), engineIdentifier).locateModule(moduleName).map(m -> (Object) m)
.orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
.locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
.map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));

this.engine.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
this.engine.put("require", wrapRequireFn.apply((Function<Object[], Object>) this.engine.get("require")));
delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));

initialized = true;
}

private void onLibLoaded(String libPath) {
scriptDependencyListener.addDependency(libPath);
}


public ScriptEngine createProxy() {
return (ScriptEngine) Proxy.newProxyInstance(ScriptEngine.class.getClassLoader(),
new Class<?>[] { ScriptEngine.class, Invocable.class }, (proxy, method, args) -> {

if (method.getName().equals("invokeFunction") || method.getName().equals("eval")) {
initialize();
}

try {
return method.invoke(engine, args);
} catch (InvocationTargetException ex) {
throw ex.getCause();
}
});
}
}

This file was deleted.

Loading

0 comments on commit 08409e5

Please sign in to comment.