From ee6a3650d886dca0e2f959852228b43666fb4ede Mon Sep 17 00:00:00 2001 From: daoge_cmd <3523206925@qq.com> Date: Tue, 7 Jan 2025 02:51:28 +0800 Subject: [PATCH] refactor: a big refactor --- .../allaymc/scriptpluginext/ScriptPlugin.java | 10 +++ .../ScriptPluginDescriptor.java | 9 +-- .../ScriptPluginExtension.java | 10 ++- .../ScriptPluginExtensionCommand.java | 53 ++++++++++++ .../allaymc/scriptpluginext/js/JSPlugin.java | 80 +++++++++++++++---- .../scriptpluginext/js/JSPluginLoader.java | 17 ++-- .../scriptpluginext/js/JSPluginSource.java | 46 +++++++++++ .../scriptpluginext/js/PackageJson.java | 64 +++++++++++++++ 8 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 src/main/java/org/allaymc/scriptpluginext/ScriptPlugin.java create mode 100644 src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtensionCommand.java create mode 100644 src/main/java/org/allaymc/scriptpluginext/js/JSPluginSource.java create mode 100644 src/main/java/org/allaymc/scriptpluginext/js/PackageJson.java diff --git a/src/main/java/org/allaymc/scriptpluginext/ScriptPlugin.java b/src/main/java/org/allaymc/scriptpluginext/ScriptPlugin.java new file mode 100644 index 0000000..a63a2b3 --- /dev/null +++ b/src/main/java/org/allaymc/scriptpluginext/ScriptPlugin.java @@ -0,0 +1,10 @@ +package org.allaymc.scriptpluginext; + +/** + * @author daoge_cmd + */ +public interface ScriptPlugin { + boolean canResetContext(); + + void resetContext(); +} diff --git a/src/main/java/org/allaymc/scriptpluginext/ScriptPluginDescriptor.java b/src/main/java/org/allaymc/scriptpluginext/ScriptPluginDescriptor.java index 9ece321..88d7a7c 100644 --- a/src/main/java/org/allaymc/scriptpluginext/ScriptPluginDescriptor.java +++ b/src/main/java/org/allaymc/scriptpluginext/ScriptPluginDescriptor.java @@ -1,13 +1,10 @@ package org.allaymc.scriptpluginext; -import lombok.Getter; -import org.allaymc.server.plugin.SimplePluginDescriptor; +import org.allaymc.api.plugin.PluginDescriptor; /** * @author daoge_cmd */ -@SuppressWarnings("FieldMayBeFinal") -@Getter -public class ScriptPluginDescriptor extends SimplePluginDescriptor { - private int debugPort = -1; +public interface ScriptPluginDescriptor extends PluginDescriptor { + int getDebugPort(); } diff --git a/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtension.java b/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtension.java index 1db5803..4aac095 100644 --- a/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtension.java +++ b/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtension.java @@ -1,6 +1,8 @@ package org.allaymc.scriptpluginext; +import org.allaymc.api.registry.Registries; import org.allaymc.scriptpluginext.js.JSPluginLoader; +import org.allaymc.scriptpluginext.js.JSPluginSource; import org.allaymc.server.extension.Extension; import org.allaymc.server.plugin.AllayPluginManager; @@ -10,6 +12,12 @@ public class ScriptPluginExtension extends Extension { @Override public void main(String[] args) { + AllayPluginManager.registerSource(new JSPluginSource()); AllayPluginManager.registerLoaderFactory(new JSPluginLoader.JsPluginLoaderFactory()); } -} + + @Override + public void afterServerStarted() { + Registries.COMMANDS.register(new ScriptPluginExtensionCommand()); + } +} \ No newline at end of file diff --git a/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtensionCommand.java b/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtensionCommand.java new file mode 100644 index 0000000..efbbf1b --- /dev/null +++ b/src/main/java/org/allaymc/scriptpluginext/ScriptPluginExtensionCommand.java @@ -0,0 +1,53 @@ +package org.allaymc.scriptpluginext; + +import org.allaymc.api.command.SimpleCommand; +import org.allaymc.api.command.tree.CommandContext; +import org.allaymc.api.command.tree.CommandTree; +import org.allaymc.api.plugin.PluginContainer; +import org.allaymc.api.server.Server; + +/** + * @author daoge_cmd + */ +public class ScriptPluginExtensionCommand extends SimpleCommand { + public ScriptPluginExtensionCommand() { + super("se", "The main command of the script plugin extension."); + } + + @Override + public void prepareCommandTree(CommandTree tree) { + tree.getRoot() + .key("resetctx") + .str("plugin") + .optional() + .exec(context -> { + String pluginName = context.getResult(1); + if (pluginName.isBlank()) { + // Reset ctx of all script plugins + Server.getInstance().getPluginManager().getEnabledPlugins().forEach((name, container) -> tryResetContextOf(context, name, container)); + } else { + var container = Server.getInstance().getPluginManager().getEnabledPlugin(pluginName); + if (container == null) { + context.addError("Plugin not found: " + pluginName); + return context.fail(); + } + + tryResetContextOf(context, pluginName, container); + } + context.addOutput("Done"); + return context.success(); + }); + } + + protected void tryResetContextOf(CommandContext context, String name, PluginContainer container) { + var plugin = container.plugin(); + if (plugin instanceof ScriptPlugin sp && sp.canResetContext()) { + context.addOutput("Resetting context of " + name); + try { + sp.resetContext(); + } catch (Throwable t) { + context.addError("Failed to reset context of " + name, t); + } + } + } +} diff --git a/src/main/java/org/allaymc/scriptpluginext/js/JSPlugin.java b/src/main/java/org/allaymc/scriptpluginext/js/JSPlugin.java index 4696d1c..74edd28 100644 --- a/src/main/java/org/allaymc/scriptpluginext/js/JSPlugin.java +++ b/src/main/java/org/allaymc/scriptpluginext/js/JSPlugin.java @@ -1,6 +1,7 @@ package org.allaymc.scriptpluginext.js; import lombok.SneakyThrows; +import org.allaymc.scriptpluginext.ScriptPlugin; import org.allaymc.scriptpluginext.ScriptPluginDescriptor; import org.allaymc.api.plugin.Plugin; import org.allaymc.api.plugin.PluginContainer; @@ -10,7 +11,7 @@ /** * @author daoge_cmd */ -public class JSPlugin extends Plugin { +public class JSPlugin extends Plugin implements ScriptPlugin { protected Context context; protected Value export; @@ -25,8 +26,7 @@ public void setPluginContainer(PluginContainer pluginContainer) { @SneakyThrows @Override public void onLoad() { - // ClassCastException won't happen - var chromeDebugPort = ((ScriptPluginDescriptor) pluginContainer.descriptor()).getDebugPort(); + var debugPort = ((ScriptPluginDescriptor) pluginContainer.descriptor()).getDebugPort(); var cbd = Context.newBuilder("js") .allowIO(IOAccess.ALL) .allowAllAccess(true) @@ -34,15 +34,33 @@ public void onLoad() { .allowHostClassLoading(true) .allowHostClassLookup(className -> true) .allowExperimentalOptions(true) - .option("js.esm-eval-returns-exports", "true"); - if (chromeDebugPort > 0) { - pluginLogger.info("Debug mode for javascript plugin {} is enabled. Port: {}", pluginContainer.descriptor().getName(), chromeDebugPort); + // Use strict mode by default + .option("js.strict", "true") + // The js.esm-eval-returns-exports option (false by default) can be used to expose + // the ES module namespace exported object to a Polyglot Context. This can be handy + // when an ES module is used directly from Java + .option("js.esm-eval-returns-exports", "true") + // Enable CommonJS experimental support. + .option("js.commonjs-require", "true") + // Directory where the NPM modules to be loaded are located. + .option("js.commonjs-require-cwd", pluginContainer.loader().getPluginPath().toString()); + // TODO: js.commonjs-core-modules-replacements + if (debugPort > 0) { + pluginLogger.info("Debug mode for javascript plugin {} is enabled. Port: {}", pluginContainer.descriptor().getName(), debugPort); // Debug mode is enabled - cbd.option("inspect", String.valueOf(chromeDebugPort)) + cbd.option("inspect", String.valueOf(debugPort)) + // The custom path that generates the connection URL .option("inspect.Path", pluginContainer.descriptor().getName()) - .option("inspect.Suspend", "true") - .option("inspect.Internal", "true") - .option("inspect.SourcePath", pluginContainer.loader().getPluginPath().toFile().getAbsolutePath()); + // The list of directories or ZIP/JAR files representing the source path. When the inspected + // application contains relative references to source files, their content is loaded from + // locations resolved with respect to this source path. It is useful during LLVM debugging, + // for instance. The paths are delimited by : on UNIX systems and by ; on MS Windows + .option("inspect.SourcePath", pluginContainer.loader().getPluginPath().toFile().getAbsolutePath()) + // Do not suspend on the first line of the application code + .option("inspect.Suspend", "false") + // When true, internal sources are inspected as well. Internal sources may provide + // language implementation details + .option("inspect.Internal", "true"); } context = cbd.build(); initGlobalMembers(); @@ -51,30 +69,43 @@ public void onLoad() { export = context.eval( Source.newBuilder("js", path.toFile()) .name(entranceJsFileName) + // ECMAScript modules can be loaded in a Context simply by evaluating the module sources. GraalJS loads + // ECMAScript modules based on their file extension. Therefore, any ECMAScript module should have file name + // extension .mjs. Alternatively, the module Source should have MIME type "application/javascript+module" .mimeType("application/javascript+module") .build() ); - tryCallJsFunction("onLoad"); + tryCallJSFunction("onLoad"); } @Override public void onEnable() { - tryCallJsFunction("onEnable"); + tryCallJSFunction("onEnable"); } @Override public void onDisable() { - tryCallJsFunction("onDisable"); + tryCallJSFunction("onDisable"); context.close(true); } @Override public boolean isReloadable() { - return true; + return tryCallJSFunction("isReloadable", false); } @Override public void reload() { + tryCallJSFunction("reload"); + } + + @Override + public boolean canResetContext() { + return tryCallJSFunction("canResetContext", false); + } + + @Override + public void resetContext() { onDisable(); onLoad(); onEnable(); @@ -82,14 +113,29 @@ public void reload() { protected void initGlobalMembers() { var binding = context.getBindings("js"); - binding.putMember("plugin", this); + binding.putMember("thisPlugin", this); + // Proxy the original "console" object, so when the js plug-in uses the + // "console" object, it will output information through log4j binding.putMember("console", proxyLogger); } - protected void tryCallJsFunction(String functionName) { - var func = export.getMember(functionName); + protected void tryCallJSFunction(String name) { + var func = export.getMember(name); if (func != null && func.canExecute()) { func.executeVoid(); } } + + protected T tryCallJSFunction(String name, T defaultValue) { + var func = export.getMember(name); + if (func != null && func.canExecute()) { + try { + return func.execute().asHostObject(); + } catch (Throwable ignore) { + return defaultValue; + } + } + + return defaultValue; + } } diff --git a/src/main/java/org/allaymc/scriptpluginext/js/JSPluginLoader.java b/src/main/java/org/allaymc/scriptpluginext/js/JSPluginLoader.java index 20316a3..a0558ea 100644 --- a/src/main/java/org/allaymc/scriptpluginext/js/JSPluginLoader.java +++ b/src/main/java/org/allaymc/scriptpluginext/js/JSPluginLoader.java @@ -3,16 +3,11 @@ import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.allaymc.scriptpluginext.ScriptPluginDescriptor; +import org.allaymc.api.plugin.*; import org.allaymc.scriptpluginext.ScriptPluginI18nLoader; import org.allaymc.api.i18n.I18n; -import org.allaymc.api.plugin.PluginContainer; -import org.allaymc.api.plugin.PluginDescriptor; -import org.allaymc.api.plugin.PluginException; -import org.allaymc.api.plugin.PluginLoader; import org.allaymc.api.utils.JSONUtils; import org.allaymc.server.i18n.AllayI18n; -import org.allaymc.server.plugin.DefaultPluginSource; import java.nio.file.Files; import java.nio.file.Path; @@ -35,7 +30,7 @@ public JSPluginLoader(Path pluginPath) { @SneakyThrows @Override public PluginDescriptor loadDescriptor() { - descriptor = JSONUtils.from(Files.newBufferedReader(pluginPath.resolve("plugin.json")), ScriptPluginDescriptor.class); + this.descriptor = JSONUtils.from(Files.newBufferedReader(pluginPath.resolve("package.json")), PackageJson.class); PluginDescriptor.checkDescriptorValid(descriptor); return descriptor; } @@ -53,7 +48,7 @@ public PluginContainer loadPlugin() { return PluginContainer.createPluginContainer( new JSPlugin(), descriptor, this, - DefaultPluginSource.getOrCreateDataFolder(descriptor.getName()) + JSPluginSource.getOrCreateDataFolder(descriptor.getName()) ); } @@ -61,7 +56,11 @@ public static class JsPluginLoaderFactory implements PluginLoader.Factory { @Override public boolean canLoad(Path pluginPath) { - return pluginPath.getFileName().toString().endsWith(".js") && Files.isDirectory(pluginPath); + var packageJsonPath = pluginPath.resolve("package.json"); + return pluginPath.getParent().endsWith(JSPluginSource.JS_PLUGIN_FOLDER) && + Files.isDirectory(pluginPath) && + Files.exists(packageJsonPath) && + Files.isRegularFile(packageJsonPath); } @Override diff --git a/src/main/java/org/allaymc/scriptpluginext/js/JSPluginSource.java b/src/main/java/org/allaymc/scriptpluginext/js/JSPluginSource.java new file mode 100644 index 0000000..c839832 --- /dev/null +++ b/src/main/java/org/allaymc/scriptpluginext/js/JSPluginSource.java @@ -0,0 +1,46 @@ +package org.allaymc.scriptpluginext.js; + +import lombok.SneakyThrows; +import org.allaymc.api.plugin.PluginSource; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author daoge_cmd + */ +public class JSPluginSource implements PluginSource { + + public static final Path JS_PLUGIN_FOLDER = Path.of("jsplugins"); + // Because the js plugin itself is also a folder, set the root of the data folder to another directory to avoid conflicts. + public static final Path JS_PLUGIN_DATA_FOLDER = Path.of("jsplugindata"); + + @SneakyThrows + public JSPluginSource() { + if (!Files.exists(JS_PLUGIN_FOLDER)) { + Files.createDirectory(JS_PLUGIN_FOLDER); + } + if (!Files.exists(JS_PLUGIN_DATA_FOLDER)) { + Files.createDirectory(JS_PLUGIN_DATA_FOLDER); + } + } + + @SneakyThrows + public static Path getOrCreateDataFolder(String pluginName) { + var dataFolder = JS_PLUGIN_DATA_FOLDER.resolve(pluginName); + if (!Files.exists(dataFolder)) { + Files.createDirectory(dataFolder); + } + return dataFolder; + } + + @SneakyThrows + @Override + public Set find() { + try (var stream = Files.list(JS_PLUGIN_FOLDER)) { + return stream.collect(Collectors.toSet()); + } + } +} diff --git a/src/main/java/org/allaymc/scriptpluginext/js/PackageJson.java b/src/main/java/org/allaymc/scriptpluginext/js/PackageJson.java new file mode 100644 index 0000000..a7489e0 --- /dev/null +++ b/src/main/java/org/allaymc/scriptpluginext/js/PackageJson.java @@ -0,0 +1,64 @@ +package org.allaymc.scriptpluginext.js; + +import lombok.Getter; +import org.allaymc.api.plugin.PluginDependency; +import org.allaymc.scriptpluginext.ScriptPluginDescriptor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author daoge_cmd + */ +public class PackageJson implements ScriptPluginDescriptor { + // "main" is optional in package.json, and in this case the default value will be "index.js" + private String main = "index.js"; + @Getter + private String name; + @Getter + private String description = ""; + @Getter + private String version; + // TODO: support another format used in "author" and "contributors" which is in + // json object, see https://dev.nodejs.cn/learn/the-package-json-guide/#author + private String author; + private List contributors = Collections.emptyList(); + private String homepage = ""; + // NOTICE: The following fields are introduced by us, they are not part of the specification + // Compared to "dependencies" or "devDependencies", "allayDependencies" specifies plugins that + // need to be installed together with the server. These plugins are not installed by npm (or + // other package managers), but should be installed manually by the user. The dependent plugin + // is not necessarily written in javascript, it may be in other languages such as java + private List allayDependencies = Collections.emptyList(); + // The debug port, optional. If specified, the plugin will be started in debug mode + private int allayDebugPort = -1; + + @Override + public String getEntrance() { + return main; + } + + @Override + public List getAuthors() { + var authors = new ArrayList(); + authors.add(author); + authors.addAll(contributors); + return authors; + } + + @Override + public String getWebsite() { + return homepage; + } + + @Override + public List getDependencies() { + return allayDependencies; + } + + @Override + public int getDebugPort() { + return allayDebugPort; + } +}