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

add-on javascript context #3554

Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.client.MapToolVariableResolver;
import net.rptools.maptool.client.script.javascript.JSArray;
import net.rptools.maptool.client.script.javascript.JSContext;
import net.rptools.maptool.client.script.javascript.JSObject;
import net.rptools.maptool.client.script.javascript.JSScriptEngine;
import net.rptools.maptool.client.script.javascript.api.MapToolJSAPIInterface;
Expand All @@ -52,13 +53,14 @@ public class MacroJavaScriptBridge extends AbstractFunction implements DefinesSp

private MacroJavaScriptBridge() {
super(
1,
0,
UNLIMITED_PARAMETERS,
"js.eval",
"js.evalNS",
"js.evalURI",
"js.removeNS",
"js.createNS");
"js.createNS",
"js.listNS");
}

public static MacroJavaScriptBridge getInstance() {
Expand All @@ -72,6 +74,20 @@ public Object childEvaluate(
variableResolver = (MapToolVariableResolver) resolver;
String contextName = null;

if ("js.listNS".equalsIgnoreCase(functionName)) {
JsonArray array = new JsonArray();
JSScriptEngine.getContexts().stream()
.sorted(Comparator.comparing(JSContext::name))
.forEach(
c -> {
JsonObject obj = new JsonObject();
obj.addProperty("name", c.name());
obj.addProperty("trusted", c.trusted());
array.add(obj);
});
return array;
}

if ("js.evalNS".equalsIgnoreCase(functionName) || "js.evalURI".equalsIgnoreCase(functionName)) {
if (args.size() < 2) {
throw new ParameterException(String.format(NOT_ENOUGH_PARAM, functionName, 2, args.size()));
Expand All @@ -80,11 +96,17 @@ public Object childEvaluate(
}

if ("js.removeNS".equalsIgnoreCase(functionName)) {
if (args.size() < 1) {
throw new ParameterException(String.format(NOT_ENOUGH_PARAM, functionName, 2, args.size()));
}
contextName = (String) args.remove(0);
JSScriptEngine.removeContext(contextName, MapTool.getParser().isMacroTrusted());
return "removed";
}
if ("js.createNS".equalsIgnoreCase(functionName)) {
if (args.size() < 1) {
throw new ParameterException(String.format(NOT_ENOUGH_PARAM, functionName, 2, args.size()));
}
contextName = (String) args.remove(0);
boolean makeTrusted = MapTool.getParser().isMacroTrusted();
if (args.size() > 0) {
Expand All @@ -97,6 +119,9 @@ public Object childEvaluate(

String script;
if ("js.evalURI".equalsIgnoreCase(functionName)) {
if (args.size() < 1) {
throw new ParameterException(String.format(NOT_ENOUGH_PARAM, functionName, 2, args.size()));
}
URL url;
try {
url = new URL(args.get(0).toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,5 @@
import org.graalvm.polyglot.*;
import org.graalvm.polyglot.HostAccess.*;

public class JSContext {
public boolean isTrusted;
public Context context;
public String name;

public JSContext(boolean t, Context c, String name) {
this.isTrusted = t;
this.context = c;
this.name = name;
}
}
public record JSContext(boolean trusted, Context context, String name) {}
;
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@ public class JSScriptEngine {
private static final JSScriptEngine jsScriptEngine = new JSScriptEngine();
private static final Logger log = LogManager.getLogger(JSScriptEngine.class);
private static final Map<String, JSContext> contexts = new HashMap<String, JSContext>();
private static final Map<String, JSContext> addOnContexts = new HashMap<String, JSContext>();
private static final Stack<JSContext> contextStack = new Stack<>();

public static JSContext getCurrentContext() {
return contextStack.peek();
}

public static boolean inTrustedContext() {
if (jsScriptEngine.contextStack.empty()) {
if (JSScriptEngine.contextStack.empty()) {
return false;
}
return jsScriptEngine.contextStack.peek().isTrusted;
return JSScriptEngine.contextStack.peek().trusted();
}

private void registerAPIObject(Value bindings, MapToolJSAPIInterface apiObj) {
Expand Down Expand Up @@ -91,7 +92,7 @@ public static void removeContext(String name, boolean trusted) throws ParserExce
return;
}
JSContext c = contexts.get(name);
if (c == null || c.isTrusted) {
if (c == null || c.trusted()) {
throw new ParserException(I18N.getText("macro.function.general.noPermJS", name));
}
contexts.remove(name);
Expand All @@ -101,6 +102,29 @@ public static void removeContext(String name, boolean trusted) throws ParserExce
public static void resetContexts() {
JSMacro.clear();
contexts.clear();
addOnContexts.clear();
}

public static JSContext registerAddOnContext(String name) {
JSContext c = new JSContext(true, jsScriptEngine.makeContext(), name);
addOnContexts.put(name, c);
return c;
}

public static void removeAddOnContext(String name) {
addOnContexts.remove(name);
}

public static boolean hasContext(String name) {
return contexts.containsKey(name);
}

public static boolean hasAddOnContext(String name) {
return addOnContexts.containsKey(name);
}

public static Set<JSContext> getContexts() {
return new HashSet<>(contexts.values());
}

public Context makeContext() {
Expand Down Expand Up @@ -130,23 +154,37 @@ public static JSScriptEngine getJSScriptEngine() {

public Value evalScript(String contextName, String script)
throws ScriptException, ParserException {
return evalScript(contextName, script, MapTool.getParser().isMacroTrusted());
}

public Value evalScript(String contextName, String script, boolean trusted)
throws ScriptException, ParserException {
if (contextName == null) {
return evalAnonymous(script);
}
JSContext jc = contexts.get(contextName);
if (jc == null) {
jc =
registerContext(
contextName,
MapTool.getParser().isMacroTrusted(),
MapTool.getParser().isMacroTrusted());
jc = registerContext(contextName, trusted, trusted);
}
if (jc.isTrusted && !MapTool.getParser().isMacroTrusted()) {
throw new ParserException(I18N.getText("macro.function.general.noPermJS", contextName));

return evalScript(jc, script, trusted);
}

public Value evalAddOnScript(String contextName, String script, boolean trusted)
throws ScriptException, ParserException {
JSContext jc = addOnContexts.get(contextName);
return evalScript(jc, script, trusted);
}

public Value evalScript(JSContext context, String script, boolean trusted)
throws ScriptException, ParserException {

if (context.trusted() && !trusted) {
throw new ParserException(I18N.getText("macro.function.general.noPermJS", context.name()));
}
contextStack.push(jc);
contextStack.push(context);
try {
return jc.context.eval("js", script);
return context.context().eval("js", script);
} finally {
contextStack.pop();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public String asString() {
if (undefined) {
throw InvalidDataOperation.createUndefined(name);
} else {
throw InvalidDataOperation.createInvalidConversion(DataType.ASSET, DataType.DOUBLE);
return asset.toString();
}
}

Expand Down
6 changes: 6 additions & 0 deletions src/main/java/net/rptools/maptool/model/library/Library.java
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,10 @@ public interface Library {
* @return the {@link Asset} for the library license file if it has one.
*/
CompletableFuture<Optional<Asset>> getLicenseAsset();

/**
* Clean up any resources used by the library, This should be called when the library is no longer
* needed.
*/
void cleanup();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import javax.script.ScriptException;
import net.rptools.lib.MD5Key;
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.client.MapToolMacroContext;
import net.rptools.maptool.client.MapToolVariableResolver;
import net.rptools.maptool.client.script.javascript.JSScriptEngine;
import net.rptools.maptool.language.I18N;
import net.rptools.maptool.model.Asset;
import net.rptools.maptool.model.Asset.Type;
Expand All @@ -49,6 +51,7 @@
import net.rptools.maptool.model.library.proto.AddOnLibraryEventsDto;
import net.rptools.maptool.model.library.proto.MTScriptPropertiesDto;
import net.rptools.maptool.util.threads.ThreadExecutionHelper;
import net.rptools.parser.ParserException;
import org.javatuples.Pair;

/** Class that implements add-on libraries. */
Expand All @@ -60,6 +63,9 @@ public class AddOnLibrary implements Library {
/** The name of the event for initialization. */
private static final String INIT_EVENT = "onInit";

/** The prefix for the name of the JavaScript context for this addon. */
private static final String JS_CONTEXT_PREFIX = "addon:";

/** Record used to store information about the MacrScript functions for this library. */
private record MTScript(String path, boolean autoExecute, String description, MD5Key md5Key) {}

Expand Down Expand Up @@ -121,11 +127,17 @@ private record MTScript(String path, boolean autoExecute, String description, MD
private final Map<String, String> legacyEventNameMap = new HashMap<>();

/** The mapping between MTScript function paths and non legacy events. */
private final Map<String, String> eventNameMap = new HashMap<>();
private final Map<String, String> mtScriptEventNameMap = new HashMap<>();

/** The mapping between JavaScript script paths and non legacy events. */
private final Map<String, String> jsEventNameMap = new HashMap<>();

/** The ID of the asset for the whole of the add-on Library. */
private final MD5Key assetKey;

/** The name of the JavaScript context for the add on library. */
private final String jsContextName;

/**
* Class used to represent Drop In Libraries.
*
Expand Down Expand Up @@ -172,7 +184,11 @@ private AddOnLibrary(

eventsDto.getEventsList().stream()
.filter(e -> !e.getMts().isEmpty())
.forEach(e -> eventNameMap.put(e.getName(), e.getMts()));
.forEach(e -> mtScriptEventNameMap.put(e.getName(), e.getMts()));

eventsDto.getEventsList().stream()
.filter(e -> !e.getJs().isEmpty())
.forEach(e -> jsEventNameMap.put(e.getName(), e.getJs()));

eventsDto.getLegacyEventsList().stream()
.filter(e -> !e.getMts().isEmpty())
Expand Down Expand Up @@ -201,6 +217,8 @@ private AddOnLibrary(

licenseFile = dto.getLicenseFile();
readMeFile = dto.getReadMeFile();

jsContextName = JS_CONTEXT_PREFIX + namespace;
}

/**
Expand Down Expand Up @@ -357,6 +375,14 @@ public CompletableFuture<Optional<Asset>> getLicenseAsset() {
}
}

@Override
public void cleanup() {
// Remove any existing JavaScript context if it exists
if (JSScriptEngine.hasAddOnContext(jsContextName)) {
JSScriptEngine.removeAddOnContext(jsContextName);
}
}

@Override
public CompletableFuture<String> getVersion() {
return CompletableFuture.completedFuture(version);
Expand Down Expand Up @@ -485,14 +511,27 @@ void initialize() {
data.needsInitialization()
.thenAccept(
needInit -> {
// First remove any existing JavaScript context if it exists
if (JSScriptEngine.hasAddOnContext(jsContextName)) {
JSScriptEngine.removeAddOnContext(jsContextName);
}
// Then create a new JavaScript context for the add-on library
JSScriptEngine.registerAddOnContext(jsContextName);
if (needInit) {
if (eventNameMap.containsKey(FIRST_INIT_EVENT)) {
callMTSFunction(eventNameMap.get(FIRST_INIT_EVENT)).join();
if (jsEventNameMap.containsKey(FIRST_INIT_EVENT)) {
runJS(jsEventNameMap.get(FIRST_INIT_EVENT));
}
if (mtScriptEventNameMap.containsKey(FIRST_INIT_EVENT)) {
callMTSFunction(mtScriptEventNameMap.get(FIRST_INIT_EVENT))
.join();
data.setNeedsToBeInitialized(false).join();
}
}
if (eventNameMap.containsKey(INIT_EVENT)) {
callMTSFunction(eventNameMap.get(INIT_EVENT)).join();
if (jsEventNameMap.containsKey(INIT_EVENT)) {
runJS(jsEventNameMap.get(INIT_EVENT));
}
if (mtScriptEventNameMap.containsKey(INIT_EVENT)) {
callMTSFunction(mtScriptEventNameMap.get(INIT_EVENT)).join();
}
});
});
Expand All @@ -516,6 +555,26 @@ private CompletableFuture<Void> callMTSFunction(String name) {
});
}

/**
* Run the specified JavaScript code from the add-on library in the add-on library's JavaScript
* context.
*
* @param file the JavaScript file withing the file to run.
*/
private void runJS(String file) {
readFile(file)
.thenAccept(
script -> {
try {
JSScriptEngine.getJSScriptEngine()
.evalScript(jsContextName, script.asAsset().getDataAsString(), true);
} catch (ParserException | ScriptException e) {
throw new RuntimeException(e);
}
})
.join();
}

CompletableFuture<DataValue> readFile(String path) {
return CompletableFuture.supplyAsync(
() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ public void registerLibrary(AddOnLibrary library) {
public void deregisterLibrary(String namespace) {
var removed = namespaceLibraryMap.remove(namespace.toLowerCase());
if (removed != null) {
removed.cleanup();
new MapToolEventBus()
.getMainEventBus()
.post(new AddOnsRemovedEvent(Set.of(removed.getLibraryInfo().join())));
Expand Down Expand Up @@ -172,6 +173,9 @@ public void removeAllLibraries() {

if (libs.size() > 0) {
new MapToolEventBus().getMainEventBus().post(new AddOnsRemovedEvent(libs));
for (var library : namespaceLibraryMap.values()) {
library.cleanup();
}
namespaceLibraryMap.clear();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,11 @@ public CompletableFuture<Optional<Asset>> getLicenseAsset() {
return CompletableFuture.completedFuture(Optional.empty());
}

@Override
public void cleanup() {
// No cleanup needed
}

/**
* @param id the id of the token Lib:Token to get.
* @return the Token for the library.
Expand Down