mirror of
https://github.com/PaperMC/Velocity.git
synced 2026-02-17 14:37:43 +01:00
Move mostly independent parts of the proxy to its own module
At this point, we have mostly connection/protocol handling and the "core proxy logic" left in the proxy module.
This commit is contained in:
@@ -55,9 +55,12 @@ tasks.withType(Checkstyle) {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly "com.github.spotbugs:spotbugs-annotations:4.1.2"
|
||||
|
||||
implementation project(':velocity-api')
|
||||
implementation project(':velocity-annotation-processor')
|
||||
implementation project(':velocity-native')
|
||||
implementation project(':velocity-proxy-core')
|
||||
|
||||
implementation "io.netty:netty-codec:${nettyVersion}"
|
||||
implementation "io.netty:netty-codec-haproxy:${nettyVersion}"
|
||||
@@ -67,7 +70,6 @@ dependencies {
|
||||
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-x86_64"
|
||||
implementation "io.netty:netty-transport-native-epoll:${nettyVersion}:linux-aarch_64"
|
||||
|
||||
implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}"
|
||||
implementation "org.apache.logging.log4j:log4j-core:${log4jVersion}"
|
||||
implementation "org.apache.logging.log4j:log4j-slf4j-impl:${log4jVersion}"
|
||||
implementation "org.apache.logging.log4j:log4j-iostreams:${log4jVersion}"
|
||||
@@ -79,7 +81,6 @@ dependencies {
|
||||
runtimeOnly 'com.lmax:disruptor:3.4.2' // Async loggers
|
||||
|
||||
implementation 'it.unimi.dsi:fastutil:8.4.1'
|
||||
implementation "net.kyori:adventure-nbt:${adventureVersion}"
|
||||
|
||||
implementation 'org.asynchttpclient:async-http-client:2.12.1'
|
||||
|
||||
@@ -87,12 +88,6 @@ dependencies {
|
||||
|
||||
implementation 'com.electronwill.night-config:toml:3.6.3'
|
||||
implementation 'org.bstats:bstats-base:2.2.0'
|
||||
implementation 'org.lanternpowered:lmbda:2.0.0'
|
||||
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine:2.8.8'
|
||||
implementation 'com.vdurmont:semver4j:3.1.0'
|
||||
|
||||
compileOnly 'com.github.spotbugs:spotbugs-annotations:4.1.2'
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-api:${junitVersion}"
|
||||
testImplementation "org.junit.jupiter:junit-jupiter-engine:${junitVersion}"
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.velocitypowered.api.command.CommandInvocation;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link CommandInvocation} implementations.
|
||||
*
|
||||
* @param <T> the type of the arguments
|
||||
*/
|
||||
abstract class AbstractCommandInvocation<T> implements CommandInvocation<T> {
|
||||
|
||||
private final CommandSource source;
|
||||
private final T arguments;
|
||||
|
||||
protected AbstractCommandInvocation(final CommandSource source, final T arguments) {
|
||||
this.source = Preconditions.checkNotNull(source, "source");
|
||||
this.arguments = Preconditions.checkNotNull(arguments, "arguments");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandSource source() {
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T arguments() {
|
||||
return arguments;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.velocitypowered.api.command.CommandInvocation;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
|
||||
/**
|
||||
* Creates command invocation contexts for the given {@link CommandSource}
|
||||
* and command line arguments.
|
||||
*
|
||||
* @param <I> the type of the built invocation
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CommandInvocationFactory<I extends CommandInvocation<?>> {
|
||||
|
||||
/**
|
||||
* Returns an invocation context for the given Brigadier context.
|
||||
*
|
||||
* @param context the command context
|
||||
* @return the built invocation context
|
||||
*/
|
||||
I create(final CommandContext<CommandSource> context);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.velocitypowered.api.command.BrigadierCommand;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandInvocation;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.InvocableCommand;
|
||||
import com.velocitypowered.api.command.RawCommand;
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.proxy.util.BrigadierUtils;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface CommandNodeFactory<T extends Command> {
|
||||
|
||||
InvocableCommandNodeFactory<SimpleCommand.Invocation> SIMPLE =
|
||||
new InvocableCommandNodeFactory<SimpleCommand.Invocation>() {
|
||||
@Override
|
||||
protected SimpleCommand.Invocation createInvocation(
|
||||
final CommandContext<CommandSource> context) {
|
||||
return VelocitySimpleCommandInvocation.FACTORY.create(context);
|
||||
}
|
||||
};
|
||||
|
||||
InvocableCommandNodeFactory<RawCommand.Invocation> RAW =
|
||||
new InvocableCommandNodeFactory<RawCommand.Invocation>() {
|
||||
@Override
|
||||
protected RawCommand.Invocation createInvocation(
|
||||
final CommandContext<CommandSource> context) {
|
||||
return VelocityRawCommandInvocation.FACTORY.create(context);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a Brigadier node for the execution of the given command.
|
||||
*
|
||||
* @param alias the command alias
|
||||
* @param command the command to execute
|
||||
* @return the command node
|
||||
*/
|
||||
LiteralCommandNode<CommandSource> create(String alias, T command);
|
||||
|
||||
abstract class InvocableCommandNodeFactory<I extends CommandInvocation<?>>
|
||||
implements CommandNodeFactory<InvocableCommand<I>> {
|
||||
|
||||
@Override
|
||||
public LiteralCommandNode<CommandSource> create(
|
||||
final String alias, final InvocableCommand<I> command) {
|
||||
return BrigadierUtils.buildRawArgumentsLiteral(alias,
|
||||
context -> {
|
||||
I invocation = createInvocation(context);
|
||||
if (!command.hasPermission(invocation)) {
|
||||
return BrigadierCommand.FORWARD;
|
||||
}
|
||||
command.execute(invocation);
|
||||
return 1;
|
||||
},
|
||||
(context, builder) -> {
|
||||
I invocation = createInvocation(context);
|
||||
|
||||
if (!command.hasPermission(invocation)) {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
return command.suggestAsync(invocation).thenApply(values -> {
|
||||
for (String value : values) {
|
||||
builder.suggest(value);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract I createInvocation(final CommandContext<CommandSource> context);
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.ParseResults;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestion;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.velocitypowered.api.command.BrigadierCommand;
|
||||
import com.velocitypowered.api.command.Command;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.command.CommandMeta;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.RawCommand;
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||
import com.velocitypowered.api.event.command.CommandExecuteEvent.CommandResult;
|
||||
import com.velocitypowered.api.event.command.CommandExecuteEventImpl;
|
||||
import com.velocitypowered.proxy.event.VelocityEventManager;
|
||||
import com.velocitypowered.proxy.util.BrigadierUtils;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.kyori.adventure.identity.Identity;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
|
||||
public class VelocityCommandManager implements CommandManager {
|
||||
|
||||
private final CommandDispatcher<CommandSource> dispatcher;
|
||||
private final VelocityEventManager eventManager;
|
||||
|
||||
public VelocityCommandManager(final VelocityEventManager eventManager) {
|
||||
this.eventManager = Preconditions.checkNotNull(eventManager);
|
||||
this.dispatcher = new CommandDispatcher<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMeta.Builder createMetaBuilder(final String alias) {
|
||||
Preconditions.checkNotNull(alias, "alias");
|
||||
return new VelocityCommandMeta.Builder(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMeta.Builder createMetaBuilder(final BrigadierCommand command) {
|
||||
Preconditions.checkNotNull(command, "command");
|
||||
return new VelocityCommandMeta.Builder(command.getNode().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(final BrigadierCommand command) {
|
||||
Preconditions.checkNotNull(command, "command");
|
||||
register(createMetaBuilder(command).build(), command);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(final CommandMeta meta, final Command command) {
|
||||
Preconditions.checkNotNull(meta, "meta");
|
||||
Preconditions.checkNotNull(command, "command");
|
||||
|
||||
Iterator<String> aliasIterator = meta.aliases().iterator();
|
||||
String primaryAlias = aliasIterator.next();
|
||||
|
||||
LiteralCommandNode<CommandSource> node = null;
|
||||
if (command instanceof BrigadierCommand) {
|
||||
node = ((BrigadierCommand) command).getNode();
|
||||
} else if (command instanceof SimpleCommand) {
|
||||
node = CommandNodeFactory.SIMPLE.create(primaryAlias, (SimpleCommand) command);
|
||||
} else if (command instanceof RawCommand) {
|
||||
node = CommandNodeFactory.RAW.create(primaryAlias, (RawCommand) command);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown command implementation for "
|
||||
+ command.getClass().getName());
|
||||
}
|
||||
|
||||
if (!(command instanceof BrigadierCommand)) {
|
||||
for (CommandNode<CommandSource> hint : meta.hints()) {
|
||||
node.addChild(BrigadierUtils.wrapForHinting(hint, node.getCommand()));
|
||||
}
|
||||
}
|
||||
|
||||
dispatcher.getRoot().addChild(node);
|
||||
while (aliasIterator.hasNext()) {
|
||||
String currentAlias = aliasIterator.next();
|
||||
CommandNode<CommandSource> existingNode = dispatcher.getRoot()
|
||||
.getChild(currentAlias.toLowerCase(Locale.ENGLISH));
|
||||
if (existingNode != null) {
|
||||
dispatcher.getRoot().getChildren().remove(existingNode);
|
||||
}
|
||||
dispatcher.getRoot().addChild(BrigadierUtils.buildRedirect(currentAlias, node));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister(final String alias) {
|
||||
Preconditions.checkNotNull(alias, "alias");
|
||||
dispatcher.getRoot().removeChildByName(alias.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires a {@link CommandExecuteEventImpl}.
|
||||
*
|
||||
* @param source the source to execute the command for
|
||||
* @param cmdLine the command to execute
|
||||
* @return the {@link CompletableFuture} of the event
|
||||
*/
|
||||
public CompletableFuture<CommandExecuteEvent> callCommandEvent(final CommandSource source,
|
||||
final String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
return eventManager.fire(new CommandExecuteEventImpl(source, cmdLine));
|
||||
}
|
||||
|
||||
private boolean executeImmediately0(final CommandSource source, final String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
ParseResults<CommandSource> results = parse(cmdLine, source, true);
|
||||
try {
|
||||
return dispatcher.execute(results) != BrigadierCommand.FORWARD;
|
||||
} catch (final CommandSyntaxException e) {
|
||||
boolean isSyntaxError = !e.getType().equals(
|
||||
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
|
||||
if (isSyntaxError) {
|
||||
source.sendMessage(Identity.nil(), Component.text(e.getMessage(), NamedTextColor.RED));
|
||||
// This is, of course, a lie, but the API will need to change...
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (final Throwable e) {
|
||||
// Ugly, ugly swallowing of everything Throwable, because plugins are naughty.
|
||||
throw new RuntimeException("Unable to invoke command " + cmdLine + " for " + source, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> execute(final CommandSource source, final String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
return callCommandEvent(source, cmdLine).thenApplyAsync(event -> {
|
||||
CommandResult commandResult = event.result();
|
||||
if (commandResult.isForwardToServer() || !commandResult.isAllowed()) {
|
||||
return false;
|
||||
}
|
||||
return executeImmediately0(source,
|
||||
MoreObjects.firstNonNull(commandResult.modifiedCommand(), event.rawCommand()));
|
||||
}, eventManager.getAsyncExecutor());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> executeImmediately(
|
||||
final CommandSource source, final String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> executeImmediately0(source, cmdLine), eventManager.getAsyncExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns suggestions to fill in the given command.
|
||||
*
|
||||
* @param source the source to execute the command for
|
||||
* @param cmdLine the partially completed command
|
||||
* @return a {@link CompletableFuture} eventually completed with a {@link List},
|
||||
* possibly empty
|
||||
*/
|
||||
public CompletableFuture<List<String>> offerSuggestions(final CommandSource source,
|
||||
final String cmdLine) {
|
||||
Preconditions.checkNotNull(source, "source");
|
||||
Preconditions.checkNotNull(cmdLine, "cmdLine");
|
||||
|
||||
ParseResults<CommandSource> parse = parse(cmdLine, source, false);
|
||||
return dispatcher.getCompletionSuggestions(parse)
|
||||
.thenApply(suggestions -> Lists.transform(suggestions.getList(), Suggestion::getText));
|
||||
}
|
||||
|
||||
private ParseResults<CommandSource> parse(final String cmdLine, final CommandSource source,
|
||||
final boolean trim) {
|
||||
String normalized = BrigadierUtils.normalizeInput(cmdLine, trim);
|
||||
return dispatcher.parse(normalized, source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given alias is registered on this manager.
|
||||
*
|
||||
* @param alias the command alias to check
|
||||
* @return {@code true} if the alias is registered
|
||||
*/
|
||||
@Override
|
||||
public boolean hasCommand(final String alias) {
|
||||
Preconditions.checkNotNull(alias, "alias");
|
||||
return dispatcher.getRoot().getChild(alias.toLowerCase(Locale.ENGLISH)) != null;
|
||||
}
|
||||
|
||||
public CommandDispatcher<CommandSource> getDispatcher() {
|
||||
return dispatcher;
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.velocitypowered.api.command.CommandMeta;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
final class VelocityCommandMeta implements CommandMeta {
|
||||
|
||||
static final class Builder implements CommandMeta.Builder {
|
||||
|
||||
private final ImmutableSet.Builder<String> aliases;
|
||||
private final ImmutableList.Builder<CommandNode<CommandSource>> hints;
|
||||
|
||||
public Builder(final String alias) {
|
||||
Preconditions.checkNotNull(alias, "alias");
|
||||
this.aliases = ImmutableSet.<String>builder()
|
||||
.add(alias.toLowerCase(Locale.ENGLISH));
|
||||
this.hints = ImmutableList.builder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMeta.Builder aliases(final String... aliases) {
|
||||
Preconditions.checkNotNull(aliases, "aliases");
|
||||
for (int i = 0, length = aliases.length; i < length; i++) {
|
||||
final String alias1 = aliases[i];
|
||||
Preconditions.checkNotNull(alias1, "alias at index %s", i);
|
||||
this.aliases.add(alias1.toLowerCase(Locale.ENGLISH));
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMeta.Builder hint(final CommandNode<CommandSource> node) {
|
||||
Preconditions.checkNotNull(node, "node");
|
||||
hints.add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMeta build() {
|
||||
return new VelocityCommandMeta(aliases.build(), hints.build());
|
||||
}
|
||||
}
|
||||
|
||||
private final Set<String> aliases;
|
||||
private final List<CommandNode<CommandSource>> hints;
|
||||
|
||||
private VelocityCommandMeta(
|
||||
final Set<String> aliases, final List<CommandNode<CommandSource>> hints) {
|
||||
this.aliases = aliases;
|
||||
this.hints = hints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> aliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<CommandNode<CommandSource>> hints() {
|
||||
return hints;
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.RawCommand;
|
||||
import com.velocitypowered.proxy.util.BrigadierUtils;
|
||||
|
||||
final class VelocityRawCommandInvocation extends AbstractCommandInvocation<String>
|
||||
implements RawCommand.Invocation {
|
||||
|
||||
static final Factory FACTORY = new Factory();
|
||||
|
||||
static class Factory implements CommandInvocationFactory<RawCommand.Invocation> {
|
||||
|
||||
@Override
|
||||
public RawCommand.Invocation create(final CommandContext<CommandSource> context) {
|
||||
return new VelocityRawCommandInvocation(
|
||||
context.getSource(),
|
||||
BrigadierUtils.getAlias(context),
|
||||
BrigadierUtils.getRawArguments(context));
|
||||
}
|
||||
}
|
||||
|
||||
private final String alias;
|
||||
|
||||
private VelocityRawCommandInvocation(final CommandSource source,
|
||||
final String alias, final String arguments) {
|
||||
super(source, arguments);
|
||||
this.alias = Preconditions.checkNotNull(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String alias() {
|
||||
return alias;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.proxy.util.BrigadierUtils;
|
||||
|
||||
final class VelocitySimpleCommandInvocation extends AbstractCommandInvocation<String[]>
|
||||
implements SimpleCommand.Invocation {
|
||||
|
||||
static final Factory FACTORY = new Factory();
|
||||
|
||||
static class Factory implements CommandInvocationFactory<SimpleCommand.Invocation> {
|
||||
|
||||
@Override
|
||||
public SimpleCommand.Invocation create(final CommandContext<CommandSource> context) {
|
||||
final String[] arguments = BrigadierUtils.getSplitArguments(context);
|
||||
final String alias = BrigadierUtils.getAlias(context);
|
||||
return new VelocitySimpleCommandInvocation(context.getSource(), alias, arguments);
|
||||
}
|
||||
}
|
||||
|
||||
private final String alias;
|
||||
|
||||
VelocitySimpleCommandInvocation(final CommandSource source, final String alias,
|
||||
final String[] arguments) {
|
||||
super(source, arguments);
|
||||
this.alias = alias;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String alias() {
|
||||
return this.alias;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ public class ServerCommand implements SimpleCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final SimpleCommand.Invocation invocation) {
|
||||
public void execute(final Invocation invocation) {
|
||||
final CommandSource source = invocation.source();
|
||||
final String[] args = invocation.arguments();
|
||||
|
||||
@@ -144,7 +144,7 @@ public class ServerCommand implements SimpleCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(final SimpleCommand.Invocation invocation) {
|
||||
public List<String> suggest(final Invocation invocation) {
|
||||
final String[] currentArgs = invocation.arguments();
|
||||
Stream<String> possibilities = server.registeredServers().stream()
|
||||
.map(rs -> rs.serverInfo().name());
|
||||
@@ -161,7 +161,7 @@ public class ServerCommand implements SimpleCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(final SimpleCommand.Invocation invocation) {
|
||||
public boolean hasPermission(final Invocation invocation) {
|
||||
return invocation.source().evaluatePermission("velocity.command.server") != Tristate.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@
|
||||
package com.velocitypowered.proxy.command.builtin;
|
||||
|
||||
import com.velocitypowered.api.command.RawCommand;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
|
||||
|
||||
public class ShutdownCommand implements RawCommand {
|
||||
|
||||
private final VelocityServer server;
|
||||
private final ProxyServer server;
|
||||
|
||||
public ShutdownCommand(VelocityServer server) {
|
||||
public ShutdownCommand(ProxyServer server) {
|
||||
this.server = server;
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@ public class ShutdownCommand implements RawCommand {
|
||||
public void execute(final Invocation invocation) {
|
||||
String reason = invocation.arguments();
|
||||
if (reason.isEmpty() || reason.trim().isEmpty()) {
|
||||
server.shutdown(true);
|
||||
server.shutdown();
|
||||
} else {
|
||||
server.shutdown(true, LegacyComponentSerializer.legacy('&').deserialize(reason));
|
||||
server.shutdown(LegacyComponentSerializer.legacy('&').deserialize(reason));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ public class VelocityCommand implements SimpleCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(final SimpleCommand.Invocation invocation) {
|
||||
public void execute(final Invocation invocation) {
|
||||
final CommandSource source = invocation.source();
|
||||
final String[] args = invocation.arguments();
|
||||
|
||||
@@ -121,7 +121,7 @@ public class VelocityCommand implements SimpleCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(final SimpleCommand.Invocation invocation) {
|
||||
public List<String> suggest(final Invocation invocation) {
|
||||
final CommandSource source = invocation.source();
|
||||
final String[] currentArgs = invocation.arguments();
|
||||
|
||||
@@ -151,7 +151,7 @@ public class VelocityCommand implements SimpleCommand {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(final SimpleCommand.Invocation invocation) {
|
||||
public boolean hasPermission(final Invocation invocation) {
|
||||
final CommandSource source = invocation.source();
|
||||
final String[] args = invocation.arguments();
|
||||
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.event;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.velocitypowered.api.event.Event;
|
||||
import com.velocitypowered.api.event.EventHandler;
|
||||
import com.velocitypowered.api.event.EventTask;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.lanternpowered.lmbda.LambdaFactory;
|
||||
import org.lanternpowered.lmbda.LambdaType;
|
||||
|
||||
final class CustomHandlerAdapter<F> {
|
||||
|
||||
final String name;
|
||||
private final Function<F, BiFunction<Object, Event, EventTask>> handlerBuilder;
|
||||
final Predicate<Method> filter;
|
||||
final BiConsumer<Method, List<String>> validator;
|
||||
private final LambdaType<F> functionType;
|
||||
private final MethodHandles.Lookup methodHandlesLookup;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
CustomHandlerAdapter(
|
||||
final String name,
|
||||
final Predicate<Method> filter,
|
||||
final BiConsumer<Method, List<String>> validator,
|
||||
final TypeToken<F> invokeFunctionType,
|
||||
final Function<F, BiFunction<Object, Event, EventTask>> handlerBuilder,
|
||||
final MethodHandles.Lookup methodHandlesLookup) {
|
||||
this.name = name;
|
||||
this.filter = filter;
|
||||
this.validator = validator;
|
||||
this.functionType = (LambdaType<F>) LambdaType.of(invokeFunctionType.getRawType());
|
||||
this.handlerBuilder = handlerBuilder;
|
||||
this.methodHandlesLookup = methodHandlesLookup;
|
||||
}
|
||||
|
||||
UntargetedEventHandler buildUntargetedHandler(final Method method)
|
||||
throws IllegalAccessException {
|
||||
final MethodHandle methodHandle = methodHandlesLookup.unreflect(method);
|
||||
final MethodHandles.Lookup defineLookup = MethodHandles.privateLookupIn(
|
||||
method.getDeclaringClass(), methodHandlesLookup);
|
||||
final LambdaType<F> lambdaType = functionType.defineClassesWith(defineLookup);
|
||||
final F invokeFunction = LambdaFactory.create(lambdaType, methodHandle);
|
||||
final BiFunction<Object, Event, EventTask> handlerFunction =
|
||||
handlerBuilder.apply(invokeFunction);
|
||||
return targetInstance -> new EventHandler<>() {
|
||||
|
||||
@Override
|
||||
public @Nullable EventTask execute(Event event) {
|
||||
return handlerFunction.apply(targetInstance, event);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.event;
|
||||
|
||||
import com.velocitypowered.api.event.Continuation;
|
||||
import com.velocitypowered.api.event.Event;
|
||||
import com.velocitypowered.api.event.EventHandler;
|
||||
import com.velocitypowered.api.event.EventTask;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public interface UntargetedEventHandler {
|
||||
|
||||
EventHandler<Event> buildHandler(Object targetInstance);
|
||||
|
||||
interface EventTaskHandler extends UntargetedEventHandler {
|
||||
|
||||
@Nullable EventTask execute(Object targetInstance, Event event);
|
||||
|
||||
@Override
|
||||
default EventHandler<Event> buildHandler(final Object targetInstance) {
|
||||
return event -> execute(targetInstance, event);
|
||||
}
|
||||
}
|
||||
|
||||
interface VoidHandler extends UntargetedEventHandler {
|
||||
|
||||
void execute(Object targetInstance, Object event);
|
||||
|
||||
@Override
|
||||
default EventHandler<Event> buildHandler(final Object targetInstance) {
|
||||
return event -> {
|
||||
execute(targetInstance, event);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface WithContinuationHandler extends UntargetedEventHandler {
|
||||
|
||||
void execute(Object targetInstance, Object event, Continuation continuation);
|
||||
|
||||
@Override
|
||||
default EventHandler<Event> buildHandler(final Object targetInstance) {
|
||||
return event -> EventTask.withContinuation(continuation ->
|
||||
execute(targetInstance, event, continuation));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,656 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.event;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.base.VerifyException;
|
||||
import com.google.common.collect.HashMultimap;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.event.Continuation;
|
||||
import com.velocitypowered.api.event.Event;
|
||||
import com.velocitypowered.api.event.EventHandler;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.event.EventTask;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.proxy.event.UntargetedEventHandler.EventTaskHandler;
|
||||
import com.velocitypowered.proxy.event.UntargetedEventHandler.VoidHandler;
|
||||
import com.velocitypowered.proxy.event.UntargetedEventHandler.WithContinuationHandler;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.VarHandle;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.lanternpowered.lmbda.LambdaFactory;
|
||||
import org.lanternpowered.lmbda.LambdaType;
|
||||
|
||||
public class VelocityEventManager implements EventManager {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(VelocityEventManager.class);
|
||||
|
||||
private static final MethodHandles.Lookup methodHandlesLookup = MethodHandles.lookup();
|
||||
private static final LambdaType<EventTaskHandler> untargetedEventTaskHandlerType =
|
||||
LambdaType.of(EventTaskHandler.class);
|
||||
private static final LambdaType<VoidHandler> untargetedVoidHandlerType =
|
||||
LambdaType.of(VoidHandler.class);
|
||||
private static final LambdaType<WithContinuationHandler> untargetedWithContinuationHandlerType =
|
||||
LambdaType.of(WithContinuationHandler.class);
|
||||
|
||||
private static final Comparator<HandlerRegistration> handlerComparator =
|
||||
Comparator.comparingInt(o -> o.order);
|
||||
|
||||
private final ExecutorService asyncExecutor;
|
||||
private final PluginManager pluginManager;
|
||||
|
||||
private final Multimap<Class<?>, HandlerRegistration> handlersByType = HashMultimap.create();
|
||||
private final LoadingCache<Class<?>, HandlersCache> handlersCache =
|
||||
Caffeine.newBuilder().build(this::bakeHandlers);
|
||||
|
||||
private final LoadingCache<Method, UntargetedEventHandler> untargetedMethodHandlers =
|
||||
Caffeine.newBuilder().weakValues().build(this::buildUntargetedMethodHandler);
|
||||
|
||||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||||
|
||||
private final List<CustomHandlerAdapter<?>> handlerAdapters = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Initializes the Velocity event manager.
|
||||
*
|
||||
* @param pluginManager a reference to the Velocity plugin manager
|
||||
*/
|
||||
public VelocityEventManager(final PluginManager pluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.asyncExecutor = Executors
|
||||
.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactoryBuilder()
|
||||
.setNameFormat("Velocity Async Event Executor - #%d").setDaemon(true).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new continuation adapter function.
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public <F> void registerHandlerAdapter(
|
||||
final String name,
|
||||
final Predicate<Method> filter,
|
||||
final BiConsumer<Method, List<String>> validator,
|
||||
final TypeToken<F> invokeFunctionType,
|
||||
final Function<F, BiFunction<Object, Event, EventTask>> handlerBuilder) {
|
||||
handlerAdapters.add(new CustomHandlerAdapter(name, filter, validator,
|
||||
invokeFunctionType, handlerBuilder, methodHandlesLookup));
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the registration of a single {@link EventHandler}.
|
||||
*/
|
||||
static final class HandlerRegistration {
|
||||
|
||||
final PluginContainer plugin;
|
||||
final short order;
|
||||
final Class<?> eventType;
|
||||
final EventHandler<Event> handler;
|
||||
final AsyncType asyncType;
|
||||
|
||||
/**
|
||||
* The instance of the {@link EventHandler} or the listener instance that was registered.
|
||||
*/
|
||||
final Object instance;
|
||||
|
||||
public HandlerRegistration(final PluginContainer plugin, final short order,
|
||||
final Class<?> eventType, final Object instance, final EventHandler<Event> handler,
|
||||
final AsyncType asyncType) {
|
||||
this.plugin = plugin;
|
||||
this.order = order;
|
||||
this.eventType = eventType;
|
||||
this.instance = instance;
|
||||
this.handler = handler;
|
||||
this.asyncType = asyncType;
|
||||
}
|
||||
}
|
||||
|
||||
enum AsyncType {
|
||||
/**
|
||||
* The complete event will be handled on an async thread.
|
||||
*/
|
||||
ALWAYS,
|
||||
/**
|
||||
* The event will initially start on the netty thread, and possibly
|
||||
* switch over to an async thread.
|
||||
*/
|
||||
SOMETIMES,
|
||||
/**
|
||||
* The event will never run async, everything is handled on
|
||||
* the netty thread.
|
||||
*/
|
||||
NEVER
|
||||
}
|
||||
|
||||
static final class HandlersCache {
|
||||
|
||||
final AsyncType asyncType;
|
||||
final HandlerRegistration[] handlers;
|
||||
|
||||
HandlersCache(final HandlerRegistration[] handlers, final AsyncType asyncType) {
|
||||
this.asyncType = asyncType;
|
||||
this.handlers = handlers;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Class<?>> getEventTypes(final Class<?> eventType) {
|
||||
return TypeToken.of(eventType).getTypes().rawTypes().stream()
|
||||
.filter(type -> type != Object.class)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private @Nullable HandlersCache bakeHandlers(final Class<?> eventType) {
|
||||
final List<HandlerRegistration> baked = new ArrayList<>();
|
||||
final List<Class<?>> types = getEventTypes(eventType);
|
||||
|
||||
lock.readLock().lock();
|
||||
try {
|
||||
for (final Class<?> type : types) {
|
||||
baked.addAll(handlersByType.get(type));
|
||||
}
|
||||
} finally {
|
||||
lock.readLock().unlock();
|
||||
}
|
||||
|
||||
if (baked.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
baked.sort(handlerComparator);
|
||||
|
||||
final AsyncType asyncType;
|
||||
if (baked.stream().anyMatch(reg -> reg.asyncType == AsyncType.ALWAYS)) {
|
||||
asyncType = AsyncType.ALWAYS;
|
||||
} else if (baked.stream().anyMatch(reg -> reg.asyncType == AsyncType.SOMETIMES)) {
|
||||
asyncType = AsyncType.SOMETIMES;
|
||||
} else {
|
||||
asyncType = AsyncType.NEVER;
|
||||
}
|
||||
|
||||
return new HandlersCache(baked.toArray(new HandlerRegistration[0]), asyncType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an {@link UntargetedEventHandler} for the given {@link Method}. This essentially
|
||||
* implements the {@link UntargetedEventHandler} (or the no async task variant) to invoke the
|
||||
* target method. The implemented class is defined in the same package as the declaring class.
|
||||
* The {@link UntargetedEventHandler} interface must be public so the implementation can access
|
||||
* it.
|
||||
*
|
||||
* @param method The method to generate an untargeted handler for
|
||||
* @return The untargeted handler
|
||||
*/
|
||||
private UntargetedEventHandler buildUntargetedMethodHandler(final Method method)
|
||||
throws IllegalAccessException {
|
||||
for (final CustomHandlerAdapter<?> handlerAdapter : handlerAdapters) {
|
||||
if (handlerAdapter.filter.test(method)) {
|
||||
return handlerAdapter.buildUntargetedHandler(method);
|
||||
}
|
||||
}
|
||||
final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(
|
||||
method.getDeclaringClass(), methodHandlesLookup);
|
||||
final MethodHandle methodHandle = lookup.unreflect(method);
|
||||
final LambdaType<? extends UntargetedEventHandler> type;
|
||||
if (EventTask.class.isAssignableFrom(method.getReturnType())) {
|
||||
type = untargetedEventTaskHandlerType;
|
||||
} else if (method.getParameterCount() == 2) {
|
||||
type = untargetedWithContinuationHandlerType;
|
||||
} else {
|
||||
type = untargetedVoidHandlerType;
|
||||
}
|
||||
return LambdaFactory.create(type.defineClassesWith(lookup), methodHandle);
|
||||
}
|
||||
|
||||
static final class MethodHandlerInfo {
|
||||
|
||||
final Method method;
|
||||
final AsyncType asyncType;
|
||||
final @Nullable Class<?> eventType;
|
||||
final short order;
|
||||
final @Nullable String errors;
|
||||
final @Nullable Class<?> continuationType;
|
||||
|
||||
private MethodHandlerInfo(final Method method, final AsyncType asyncType,
|
||||
final @Nullable Class<?> eventType, final short order, final @Nullable String errors,
|
||||
final @Nullable Class<?> continuationType) {
|
||||
this.method = method;
|
||||
this.asyncType = asyncType;
|
||||
this.eventType = eventType;
|
||||
this.order = order;
|
||||
this.errors = errors;
|
||||
this.continuationType = continuationType;
|
||||
}
|
||||
}
|
||||
|
||||
private void collectMethods(final Class<?> targetClass,
|
||||
final Map<String, MethodHandlerInfo> collected) {
|
||||
for (final Method method : targetClass.getDeclaredMethods()) {
|
||||
final Subscribe subscribe = method.getAnnotation(Subscribe.class);
|
||||
if (subscribe == null) {
|
||||
continue;
|
||||
}
|
||||
String key = method.getName()
|
||||
+ "("
|
||||
+ Arrays.stream(method.getParameterTypes())
|
||||
.map(Class::getName)
|
||||
.collect(Collectors.joining(","))
|
||||
+ ")";
|
||||
if (Modifier.isPrivate(method.getModifiers())) {
|
||||
key = targetClass.getName() + "$" + key;
|
||||
}
|
||||
if (collected.containsKey(key)) {
|
||||
continue;
|
||||
}
|
||||
final Set<String> errors = new HashSet<>();
|
||||
if (Modifier.isStatic(method.getModifiers())) {
|
||||
errors.add("method must not be static");
|
||||
}
|
||||
if (Modifier.isAbstract(method.getModifiers())) {
|
||||
errors.add("method must not be abstract");
|
||||
}
|
||||
Class<?> eventType = null;
|
||||
Class<?> continuationType = null;
|
||||
CustomHandlerAdapter<?> handlerAdapter = null;
|
||||
final int paramCount = method.getParameterCount();
|
||||
if (paramCount == 0) {
|
||||
errors.add("method must have at least one parameter which is the event");
|
||||
} else {
|
||||
final Class<?>[] parameterTypes = method.getParameterTypes();
|
||||
eventType = parameterTypes[0];
|
||||
if (!Event.class.isAssignableFrom(eventType)) {
|
||||
errors.add(String.format("first method parameter must be the event, %s is invalid",
|
||||
eventType.getName()));
|
||||
}
|
||||
for (final CustomHandlerAdapter<?> handlerAdapterCandidate : handlerAdapters) {
|
||||
if (handlerAdapterCandidate.filter.test(method)) {
|
||||
handlerAdapter = handlerAdapterCandidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (handlerAdapter != null) {
|
||||
final List<String> adapterErrors = new ArrayList<>();
|
||||
handlerAdapter.validator.accept(method, adapterErrors);
|
||||
if (!adapterErrors.isEmpty()) {
|
||||
errors.add(String.format("%s adapter errors: [%s]",
|
||||
handlerAdapter.name, String.join(", ", adapterErrors)));
|
||||
}
|
||||
} else if (paramCount == 2) {
|
||||
continuationType = parameterTypes[1];
|
||||
if (continuationType != Continuation.class) {
|
||||
errors.add(String.format("method is allowed to have a continuation as second parameter,"
|
||||
+ " but %s is invalid", continuationType.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
AsyncType asyncType = AsyncType.NEVER;
|
||||
if (handlerAdapter == null) {
|
||||
final Class<?> returnType = method.getReturnType();
|
||||
if (returnType != void.class && continuationType == Continuation.class) {
|
||||
errors.add("method return type must be void if a continuation parameter is provided");
|
||||
} else if (returnType != void.class && returnType != EventTask.class) {
|
||||
errors.add("method return type must be void, AsyncTask, "
|
||||
+ "AsyncTask.Basic or AsyncTask.WithContinuation");
|
||||
} else if (returnType == EventTask.class) {
|
||||
asyncType = AsyncType.SOMETIMES;
|
||||
}
|
||||
} else {
|
||||
asyncType = AsyncType.SOMETIMES;
|
||||
}
|
||||
if (subscribe.async()) {
|
||||
asyncType = AsyncType.ALWAYS;
|
||||
}
|
||||
final short order = subscribe.order();
|
||||
final String errorsJoined = errors.isEmpty() ? null : String.join(",", errors);
|
||||
collected.put(key, new MethodHandlerInfo(method, asyncType, eventType, order, errorsJoined, continuationType));
|
||||
}
|
||||
final Class<?> superclass = targetClass.getSuperclass();
|
||||
if (superclass != Object.class) {
|
||||
collectMethods(superclass, collected);
|
||||
}
|
||||
}
|
||||
|
||||
private void register(final List<HandlerRegistration> registrations) {
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
for (final HandlerRegistration registration : registrations) {
|
||||
handlersByType.put(registration.eventType, registration);
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
// Invalidate all the affected event subtypes
|
||||
handlersCache.invalidateAll(registrations.stream()
|
||||
.flatMap(registration -> getEventTypes(registration.eventType).stream())
|
||||
.distinct()
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(final Object plugin, final Object listener) {
|
||||
requireNonNull(listener, "listener");
|
||||
final PluginContainer pluginContainer = pluginManager.ensurePluginContainer(plugin);
|
||||
if (plugin == listener) {
|
||||
throw new IllegalArgumentException("The plugin main instance is automatically registered.");
|
||||
}
|
||||
registerInternally(pluginContainer, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends Event> void register(final Object plugin, final Class<E> eventClass,
|
||||
final short order, final EventHandler<E> handler) {
|
||||
final PluginContainer pluginContainer = pluginManager.ensurePluginContainer(plugin);
|
||||
requireNonNull(eventClass, "eventClass");
|
||||
requireNonNull(handler, "handler");
|
||||
|
||||
final HandlerRegistration registration = new HandlerRegistration(pluginContainer, order,
|
||||
eventClass, handler, (EventHandler<Event>) handler, AsyncType.SOMETIMES);
|
||||
register(Collections.singletonList(registration));
|
||||
}
|
||||
|
||||
public void registerInternally(final PluginContainer pluginContainer, final Object listener) {
|
||||
final Class<?> targetClass = listener.getClass();
|
||||
final Map<String, MethodHandlerInfo> collected = new HashMap<>();
|
||||
collectMethods(targetClass, collected);
|
||||
|
||||
final List<HandlerRegistration> registrations = new ArrayList<>();
|
||||
for (final MethodHandlerInfo info : collected.values()) {
|
||||
if (info.errors != null) {
|
||||
logger.info("Invalid listener method {} in {}: {}",
|
||||
info.method.getName(), info.method.getDeclaringClass().getName(), info.errors);
|
||||
continue;
|
||||
}
|
||||
final UntargetedEventHandler untargetedHandler = untargetedMethodHandlers.get(info.method);
|
||||
assert untargetedHandler != null;
|
||||
if (info.eventType == null) {
|
||||
throw new VerifyException("Event type is not present and there are no errors");
|
||||
}
|
||||
|
||||
final EventHandler<Event> handler = untargetedHandler.buildHandler(listener);
|
||||
registrations.add(new HandlerRegistration(pluginContainer, info.order,
|
||||
info.eventType, listener, handler, info.asyncType));
|
||||
}
|
||||
|
||||
register(registrations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListeners(final Object plugin) {
|
||||
final PluginContainer pluginContainer = pluginManager.ensurePluginContainer(plugin);
|
||||
unregisterIf(registration -> registration.plugin == pluginContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterListener(final Object plugin, final Object handler) {
|
||||
final PluginContainer pluginContainer = pluginManager.ensurePluginContainer(plugin);
|
||||
requireNonNull(handler, "handler");
|
||||
unregisterIf(registration ->
|
||||
registration.plugin == pluginContainer && registration.handler == handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Event> void unregister(final Object plugin, final EventHandler<E> handler) {
|
||||
unregisterListener(plugin, handler);
|
||||
}
|
||||
|
||||
private void unregisterIf(final Predicate<HandlerRegistration> predicate) {
|
||||
final List<HandlerRegistration> removed = new ArrayList<>();
|
||||
lock.writeLock().lock();
|
||||
try {
|
||||
final Iterator<HandlerRegistration> it = handlersByType.values().iterator();
|
||||
while (it.hasNext()) {
|
||||
final HandlerRegistration registration = it.next();
|
||||
if (predicate.test(registration)) {
|
||||
it.remove();
|
||||
removed.add(registration);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.writeLock().unlock();
|
||||
}
|
||||
|
||||
// Invalidate all the affected event subtypes
|
||||
handlersCache.invalidateAll(removed.stream()
|
||||
.flatMap(registration -> getEventTypes(registration.eventType).stream())
|
||||
.distinct()
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireAndForget(final Event event) {
|
||||
requireNonNull(event, "event");
|
||||
final HandlersCache handlersCache = this.handlersCache.get(event.getClass());
|
||||
if (handlersCache == null) {
|
||||
// Optimization: nobody's listening.
|
||||
return;
|
||||
}
|
||||
fire(null, event, handlersCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Event> CompletableFuture<E> fire(final E event) {
|
||||
requireNonNull(event, "event");
|
||||
final HandlersCache handlersCache = this.handlersCache.get(event.getClass());
|
||||
if (handlersCache == null) {
|
||||
// Optimization: nobody's listening.
|
||||
return CompletableFuture.completedFuture(event);
|
||||
}
|
||||
final CompletableFuture<E> future = new CompletableFuture<>();
|
||||
fire(future, event, handlersCache);
|
||||
return future;
|
||||
}
|
||||
|
||||
private <E extends Event> void fire(final @Nullable CompletableFuture<E> future,
|
||||
final E event, final HandlersCache handlersCache) {
|
||||
if (handlersCache.asyncType == AsyncType.ALWAYS) {
|
||||
// We already know that the event needs to be handled async, so
|
||||
// execute it asynchronously from the start
|
||||
asyncExecutor.execute(() -> fire(future, event, 0, true, handlersCache.handlers));
|
||||
} else {
|
||||
fire(future, event, 0, false, handlersCache.handlers);
|
||||
}
|
||||
}
|
||||
|
||||
private static final int TASK_STATE_DEFAULT = 0;
|
||||
private static final int TASK_STATE_EXECUTING = 1;
|
||||
private static final int TASK_STATE_CONTINUE_IMMEDIATELY = 2;
|
||||
|
||||
private static final VarHandle CONTINUATION_TASK_RESUMED;
|
||||
private static final VarHandle CONTINUATION_TASK_STATE;
|
||||
|
||||
static {
|
||||
try {
|
||||
CONTINUATION_TASK_RESUMED = MethodHandles.lookup()
|
||||
.findVarHandle(ContinuationTask.class, "resumed", boolean.class);
|
||||
CONTINUATION_TASK_STATE = MethodHandles.lookup()
|
||||
.findVarHandle(ContinuationTask.class, "state", int.class);
|
||||
} catch (final ReflectiveOperationException e) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
final class ContinuationTask<E extends Event> implements Continuation, Runnable {
|
||||
|
||||
private final EventTask task;
|
||||
private final int index;
|
||||
private final HandlerRegistration[] registrations;
|
||||
private final @Nullable CompletableFuture<E> future;
|
||||
private final boolean currentlyAsync;
|
||||
private final E event;
|
||||
|
||||
// This field is modified via a VarHandle, so this field is used and cannot be final.
|
||||
@SuppressWarnings({"UnusedVariable", "FieldMayBeFinal", "FieldCanBeLocal"})
|
||||
private volatile int state = TASK_STATE_DEFAULT;
|
||||
|
||||
// This field is modified via a VarHandle, so this field is used and cannot be final.
|
||||
@SuppressWarnings({"UnusedVariable", "FieldMayBeFinal"})
|
||||
private volatile boolean resumed = false;
|
||||
|
||||
private ContinuationTask(
|
||||
final EventTask task,
|
||||
final HandlerRegistration[] registrations,
|
||||
final @Nullable CompletableFuture<E> future,
|
||||
final E event,
|
||||
final int index,
|
||||
final boolean currentlyAsync) {
|
||||
this.task = task;
|
||||
this.registrations = registrations;
|
||||
this.future = future;
|
||||
this.event = event;
|
||||
this.index = index;
|
||||
this.currentlyAsync = currentlyAsync;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (execute()) {
|
||||
fire(future, event, index + 1, currentlyAsync, registrations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the task and returns whether the next one should be executed
|
||||
* immediately after this one without scheduling.
|
||||
*/
|
||||
boolean execute() {
|
||||
state = TASK_STATE_EXECUTING;
|
||||
try {
|
||||
task.execute(this);
|
||||
} catch (final Throwable t) {
|
||||
// validateOnlyOnce false here so don't get an exception if the
|
||||
// continuation was resumed before
|
||||
resume(t, false);
|
||||
}
|
||||
return !CONTINUATION_TASK_STATE.compareAndSet(
|
||||
this, TASK_STATE_EXECUTING, TASK_STATE_DEFAULT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
resume(null, true);
|
||||
}
|
||||
|
||||
void resume(final @Nullable Throwable exception, final boolean validateOnlyOnce) {
|
||||
final boolean changed = CONTINUATION_TASK_RESUMED.compareAndSet(this, false, true);
|
||||
// Only allow the continuation to be resumed once
|
||||
if (!changed && validateOnlyOnce) {
|
||||
throw new IllegalStateException("The continuation can only be resumed once.");
|
||||
}
|
||||
final HandlerRegistration registration = registrations[index];
|
||||
if (exception != null) {
|
||||
logHandlerException(registration, exception);
|
||||
}
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
if (index + 1 == registrations.length) {
|
||||
// Optimization: don't schedule a task just to complete the future
|
||||
if (future != null) {
|
||||
future.complete(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!CONTINUATION_TASK_STATE.compareAndSet(
|
||||
this, TASK_STATE_EXECUTING, TASK_STATE_CONTINUE_IMMEDIATELY)) {
|
||||
asyncExecutor.execute(() -> fire(future, event, index + 1, true, registrations));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWithException(final Throwable exception) {
|
||||
resume(requireNonNull(exception, "exception"), true);
|
||||
}
|
||||
}
|
||||
|
||||
private <E extends Event> void fire(final @Nullable CompletableFuture<E> future, final E event,
|
||||
final int offset, final boolean currentlyAsync, final HandlerRegistration[] registrations) {
|
||||
for (int i = offset; i < registrations.length; i++) {
|
||||
final HandlerRegistration registration = registrations[i];
|
||||
try {
|
||||
final EventTask eventTask = registration.handler.execute(event);
|
||||
if (eventTask == null) {
|
||||
continue;
|
||||
}
|
||||
final ContinuationTask<E> continuationTask = new ContinuationTask<>(eventTask,
|
||||
registrations, future, event, i, currentlyAsync);
|
||||
if (currentlyAsync || !eventTask.requiresAsync()) {
|
||||
if (continuationTask.execute()) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
asyncExecutor.execute(continuationTask);
|
||||
}
|
||||
// fire will continue in another thread once the async task is
|
||||
// executed and the continuation is resumed
|
||||
return;
|
||||
} catch (final Throwable t) {
|
||||
logHandlerException(registration, t);
|
||||
}
|
||||
}
|
||||
if (future != null) {
|
||||
future.complete(event);
|
||||
}
|
||||
}
|
||||
|
||||
private static void logHandlerException(
|
||||
final HandlerRegistration registration, final Throwable t) {
|
||||
logger.error("Couldn't pass {} to {}", registration.eventType.getSimpleName(),
|
||||
registration.plugin.description().id(), t);
|
||||
}
|
||||
|
||||
public boolean shutdown() throws InterruptedException {
|
||||
asyncExecutor.shutdown();
|
||||
return asyncExecutor.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public ExecutorService getAsyncExecutor() {
|
||||
return asyncExecutor;
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
public class PluginClassLoader extends URLClassLoader {
|
||||
|
||||
private static final Set<PluginClassLoader> loaders = new CopyOnWriteArraySet<>();
|
||||
|
||||
static {
|
||||
ClassLoader.registerAsParallelCapable();
|
||||
}
|
||||
|
||||
private final PluginDescription description;
|
||||
|
||||
public PluginClassLoader(URL[] urls, ClassLoader parent, PluginDescription description) {
|
||||
super(urls, parent);
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public void addToClassloaders() {
|
||||
loaders.add(this);
|
||||
}
|
||||
|
||||
void addPath(Path path) {
|
||||
try {
|
||||
addURL(path.toUri().toURL());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
loaders.remove(this);
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
return findClass0(name, true);
|
||||
}
|
||||
|
||||
private boolean isKtLanguagePlugin() {
|
||||
return description.id().equals("velocity-language-kotlin");
|
||||
}
|
||||
|
||||
private Class<?> findClass0(String name, boolean checkOther)
|
||||
throws ClassNotFoundException {
|
||||
if (name.startsWith("com.velocitypowered") && !isKtLanguagePlugin()) {
|
||||
throw new ClassNotFoundException();
|
||||
}
|
||||
|
||||
try {
|
||||
return super.findClass(name);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// Ignored: we'll try others
|
||||
}
|
||||
|
||||
if (checkOther) {
|
||||
for (PluginClassLoader loader : loaders) {
|
||||
if (loader != this) {
|
||||
try {
|
||||
return loader.findClass0(name, false);
|
||||
} catch (ClassNotFoundException ignored) {
|
||||
// We're trying others, safe to ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "plugin " + this.description.name();
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.name.Names;
|
||||
import com.vdurmont.semver4j.Semver;
|
||||
import com.vdurmont.semver4j.Semver.SemverType;
|
||||
import com.vdurmont.semver4j.SemverException;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.event.EventManager;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer;
|
||||
import com.velocitypowered.proxy.plugin.loader.jvm.JvmPluginLoader;
|
||||
import com.velocitypowered.proxy.plugin.util.PluginDependencyUtils;
|
||||
import com.velocitypowered.proxy.plugin.util.ProxyPluginContainer;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class VelocityPluginManager implements PluginManager {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(VelocityPluginManager.class);
|
||||
|
||||
private final Map<String, PluginContainer> plugins = new LinkedHashMap<>();
|
||||
private final IdentityHashMap<Object, PluginContainer> pluginInstances = new IdentityHashMap<>();
|
||||
private final VelocityServer server;
|
||||
|
||||
public VelocityPluginManager(VelocityServer server) {
|
||||
this.server = checkNotNull(server, "server");
|
||||
|
||||
// Register ourselves as a plugin
|
||||
this.registerPlugin(ProxyPluginContainer.VELOCITY);
|
||||
}
|
||||
|
||||
private void registerPlugin(PluginContainer plugin) {
|
||||
plugins.put(plugin.description().id(), plugin);
|
||||
Object instance = plugin.instance();
|
||||
if (instance != null) {
|
||||
pluginInstances.put(instance, plugin);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all plugins from the specified {@code directory}.
|
||||
* @param directory the directory to load from
|
||||
* @throws IOException if we could not open the directory
|
||||
*/
|
||||
@SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_WOULD_HAVE_BEEN_A_NPE",
|
||||
justification = "I looked carefully and there's no way SpotBugs is right.")
|
||||
public void loadPlugins(Path directory) throws IOException {
|
||||
checkNotNull(directory, "directory");
|
||||
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
|
||||
|
||||
List<PluginDescription> found = new ArrayList<>();
|
||||
JvmPluginLoader loader = new JvmPluginLoader(server, directory);
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory,
|
||||
p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
|
||||
for (Path path : stream) {
|
||||
try {
|
||||
found.addAll(loader.loadPluginCandidates(path));
|
||||
} catch (Exception e) {
|
||||
logger.error("Unable to load plugin {}", path, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found.isEmpty()) {
|
||||
// No plugins found
|
||||
return;
|
||||
}
|
||||
|
||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
|
||||
|
||||
Map<String, PluginContainer> loadedPluginsById = new HashMap<>(this.plugins);
|
||||
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
||||
// Now load the plugins
|
||||
pluginLoad:
|
||||
for (PluginDescription candidate : sortedPlugins) {
|
||||
// Verify dependencies
|
||||
for (PluginDependency dependency : candidate.dependencies()) {
|
||||
final PluginContainer dependencyContainer = loadedPluginsById.get(dependency.id());
|
||||
if (dependencyContainer == null) {
|
||||
if (dependency.optional()) {
|
||||
logger.warn("Plugin {} has an optional dependency {} that is not available",
|
||||
candidate.id(), dependency.id());
|
||||
} else {
|
||||
logger.error("Can't load plugin {} due to missing dependency {}",
|
||||
candidate.id(), dependency.id());
|
||||
continue pluginLoad;
|
||||
}
|
||||
} else {
|
||||
String requiredRange = dependency.version();
|
||||
if (!requiredRange.isEmpty()) {
|
||||
try {
|
||||
Semver dependencyCandidateVersion = new Semver(
|
||||
dependencyContainer.description().version(), SemverType.NPM);
|
||||
if (!dependencyCandidateVersion.satisfies(requiredRange)) {
|
||||
if (!dependency.optional()) {
|
||||
logger.error(
|
||||
"Can't load plugin {} due to incompatible dependency {} {} (you have {})",
|
||||
candidate.id(), dependency.id(), requiredRange,
|
||||
dependencyContainer.description().version());
|
||||
continue pluginLoad;
|
||||
} else {
|
||||
logger.warn(
|
||||
"Plugin {} has an optional dependency on {} {}, but you have {}",
|
||||
candidate.id(), dependency.id(), requiredRange,
|
||||
dependencyContainer.description().version());
|
||||
}
|
||||
}
|
||||
} catch (SemverException exception) {
|
||||
logger.warn("Can't check dependency of {} for the proper version of {},"
|
||||
+ " assuming they are compatible", candidate.id(), dependency.id());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
PluginDescription realPlugin = loader.materializePlugin(candidate);
|
||||
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
||||
pluginContainers.put(container, loader.createModule(container));
|
||||
loadedPluginsById.put(candidate.id(), container);
|
||||
} catch (Exception e) {
|
||||
logger.error("Can't create module for plugin {}", candidate.id(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// Make a global Guice module that with common bindings for every plugin
|
||||
AbstractModule commonModule = new AbstractModule() {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(ProxyServer.class).toInstance(server);
|
||||
bind(PluginManager.class).toInstance(server.pluginManager());
|
||||
bind(EventManager.class).toInstance(server.eventManager());
|
||||
bind(CommandManager.class).toInstance(server.commandManager());
|
||||
for (PluginContainer container : pluginContainers.keySet()) {
|
||||
bind(PluginContainer.class)
|
||||
.annotatedWith(Names.named(container.description().id()))
|
||||
.toInstance(container);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (Map.Entry<PluginContainer, Module> plugin : pluginContainers.entrySet()) {
|
||||
PluginContainer container = plugin.getKey();
|
||||
PluginDescription description = container.description();
|
||||
|
||||
try {
|
||||
loader.createPlugin(container, plugin.getValue(), commonModule);
|
||||
} catch (Exception e) {
|
||||
logger.error("Can't create plugin {}", description.id(), e);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.info("Loaded plugin {} {} by {}", description.id(), MoreObjects.firstNonNull(
|
||||
description.version(), "<UNKNOWN>"), Joiner.on(", ").join(description.authors()));
|
||||
registerPlugin(container);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PluginContainer fromInstance(Object instance) {
|
||||
checkNotNull(instance, "instance");
|
||||
|
||||
if (instance instanceof PluginContainer) {
|
||||
return (PluginContainer) instance;
|
||||
}
|
||||
|
||||
return pluginInstances.get(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PluginContainer getPlugin(String id) {
|
||||
checkNotNull(id, "id");
|
||||
return plugins.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PluginContainer> plugins() {
|
||||
return Collections.unmodifiableCollection(plugins.values());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded(String id) {
|
||||
return plugins.containsKey(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToClasspath(Object plugin, Path path) {
|
||||
checkNotNull(plugin, "instance");
|
||||
checkNotNull(path, "path");
|
||||
PluginContainer optContainer = fromInstance(plugin);
|
||||
if (optContainer == null) {
|
||||
throw new IllegalArgumentException("plugin is not loaded");
|
||||
}
|
||||
|
||||
ClassLoader pluginClassloader = plugin.getClass().getClassLoader();
|
||||
if (pluginClassloader instanceof PluginClassLoader) {
|
||||
((PluginClassLoader) pluginClassloader).addPath(path);
|
||||
} else {
|
||||
throw new UnsupportedOperationException(
|
||||
"Operation is not supported on non-Java Velocity plugins.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.google.inject.Module;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* This interface is used for loading plugins.
|
||||
*/
|
||||
public interface PluginLoader {
|
||||
|
||||
Collection<PluginDescription> loadPluginCandidates(Path source) throws Exception;
|
||||
|
||||
PluginDescription materializePlugin(PluginDescription source) throws Exception;
|
||||
|
||||
/**
|
||||
* Creates a {@link Module} for the provided {@link PluginContainer}
|
||||
* and verifies that the container's {@link PluginDescription} is correct.
|
||||
*
|
||||
* <p>Does not create an instance of the plugin.</p>
|
||||
*
|
||||
* @param container the plugin container
|
||||
* @return the module containing bindings specific to this plugin
|
||||
* @throws IllegalArgumentException If the {@link PluginDescription}
|
||||
* is missing the path
|
||||
*/
|
||||
Module createModule(PluginContainer container) throws Exception;
|
||||
|
||||
/**
|
||||
* Creates an instance of the plugin as specified by the
|
||||
* plugin's main class found in the {@link PluginDescription}.
|
||||
*
|
||||
* <p>The provided {@link Module modules} are used to create an
|
||||
* injector which is then used to create the plugin instance.</p>
|
||||
*
|
||||
* <p>The plugin instance is set in the provided {@link PluginContainer}.</p>
|
||||
*
|
||||
* @param container the plugin container
|
||||
* @param modules the modules to be used when creating this plugin's injector
|
||||
* @throws IllegalStateException If the plugin instance could not be
|
||||
* created from the provided modules
|
||||
*/
|
||||
void createPlugin(PluginContainer container, Module... modules) throws Exception;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class VelocityPluginContainer implements PluginContainer {
|
||||
|
||||
private final PluginDescription description;
|
||||
private @MonotonicNonNull Object instance;
|
||||
|
||||
public VelocityPluginContainer(PluginDescription description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginDescription description() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object instance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void setInstance(Object instance) {
|
||||
if (this.instance == null) {
|
||||
this.instance = instance;
|
||||
} else {
|
||||
throw new IllegalStateException("Plugin instance already set");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class VelocityPluginDescription implements PluginDescription {
|
||||
|
||||
private final String id;
|
||||
private final @Nullable String name;
|
||||
private final String version;
|
||||
private final @Nullable String description;
|
||||
private final @Nullable String url;
|
||||
private final List<String> authors;
|
||||
private final Map<String, PluginDependency> dependencies;
|
||||
private final @Nullable Path source;
|
||||
|
||||
/**
|
||||
* Creates a new plugin description.
|
||||
* @param id the ID
|
||||
* @param name the name of the plugin
|
||||
* @param version the plugin version
|
||||
* @param description a description of the plugin
|
||||
* @param url the website for the plugin
|
||||
* @param authors the authors of this plugin
|
||||
* @param dependencies the dependencies for this plugin
|
||||
* @param source the original source for the plugin
|
||||
*/
|
||||
public VelocityPluginDescription(String id, @Nullable String name, String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Path source) {
|
||||
this.id = checkNotNull(id, "id");
|
||||
this.name = Strings.emptyToNull(name);
|
||||
this.version = checkNotNull(version, "version");
|
||||
this.description = Strings.emptyToNull(description);
|
||||
this.url = Strings.emptyToNull(url);
|
||||
this.authors = authors == null ? ImmutableList.of() : ImmutableList.copyOf(authors);
|
||||
this.dependencies = Maps.uniqueIndex(dependencies, d -> d == null ? null : d.id());
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name == null ? id : name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String description() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String url() {
|
||||
return url;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> authors() {
|
||||
return authors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PluginDependency> dependencies() {
|
||||
return dependencies.values();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PluginDependency getDependency(String id) {
|
||||
return dependencies.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Path file() {
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VelocityPluginDescription{"
|
||||
+ "id='" + id + '\''
|
||||
+ ", name='" + name + '\''
|
||||
+ ", version='" + version + '\''
|
||||
+ ", description='" + description + '\''
|
||||
+ ", url='" + url + '\''
|
||||
+ ", authors=" + authors
|
||||
+ ", dependencies=" + dependencies
|
||||
+ ", source=" + source
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@@ -1,230 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader.jvm;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Module;
|
||||
import com.velocitypowered.annotationprocessor.SerializedPluginDescription;
|
||||
import com.velocitypowered.api.plugin.InvalidPluginException;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import com.velocitypowered.proxy.plugin.PluginClassLoader;
|
||||
import com.velocitypowered.proxy.plugin.loader.PluginLoader;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginContainer;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
|
||||
import io.leangen.geantyref.TypeToken;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarInputStream;
|
||||
|
||||
public class JvmPluginLoader implements PluginLoader {
|
||||
|
||||
private final Path baseDirectory;
|
||||
private final Map<URI, PluginClassLoader> classLoaders = new HashMap<>();
|
||||
|
||||
public JvmPluginLoader(ProxyServer server, Path baseDirectory) {
|
||||
this.baseDirectory = baseDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PluginDescription> loadPluginCandidates(Path source) throws Exception {
|
||||
List<SerializedPluginDescription> serialized = getSerializedPluginInfo(source);
|
||||
if (serialized.isEmpty()) {
|
||||
throw new InvalidPluginException("Did not find a valid velocity-plugin-info.json.");
|
||||
}
|
||||
|
||||
List<PluginDescription> candidates = new ArrayList<>();
|
||||
for (SerializedPluginDescription description : serialized) {
|
||||
candidates.add(createCandidateDescription(description, source));
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginDescription materializePlugin(PluginDescription source) throws Exception {
|
||||
if (!(source instanceof JvmVelocityPluginDescriptionCandidate)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the JVM plugin loader");
|
||||
}
|
||||
|
||||
Path jarFilePath = source.file();
|
||||
if (jarFilePath == null) {
|
||||
throw new IllegalStateException("JAR path not provided.");
|
||||
}
|
||||
|
||||
URI pluginJarUri = jarFilePath.toUri();
|
||||
URL pluginJarUrl = pluginJarUri.toURL();
|
||||
PluginClassLoader loader = this.classLoaders.computeIfAbsent(pluginJarUri, (uri) -> {
|
||||
PluginClassLoader classLoader = AccessController.doPrivileged(
|
||||
(PrivilegedAction<PluginClassLoader>) () -> new PluginClassLoader(new URL[]{pluginJarUrl},
|
||||
JvmPluginLoader.class.getClassLoader(), source));
|
||||
classLoader.addToClassloaders();
|
||||
return classLoader;
|
||||
});
|
||||
|
||||
JvmVelocityPluginDescriptionCandidate candidate =
|
||||
(JvmVelocityPluginDescriptionCandidate) source;
|
||||
Class mainClass = loader.loadClass(candidate.getMainClass());
|
||||
return createDescription(candidate, mainClass);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Module createModule(PluginContainer container) throws Exception {
|
||||
PluginDescription description = container.description();
|
||||
if (!(description instanceof JvmVelocityPluginDescription)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the JVM plugin loader");
|
||||
}
|
||||
|
||||
JvmVelocityPluginDescription javaDescription = (JvmVelocityPluginDescription) description;
|
||||
Path source = javaDescription.file();
|
||||
|
||||
if (source == null) {
|
||||
throw new IllegalArgumentException("No path in plugin description");
|
||||
}
|
||||
|
||||
return new VelocityPluginModule(javaDescription, container, baseDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createPlugin(PluginContainer container, Module... modules) {
|
||||
if (!(container instanceof VelocityPluginContainer)) {
|
||||
throw new IllegalArgumentException("Container provided isn't of the Java plugin loader");
|
||||
}
|
||||
PluginDescription description = container.description();
|
||||
if (!(description instanceof JvmVelocityPluginDescription)) {
|
||||
throw new IllegalArgumentException("Description provided isn't of the Java plugin loader");
|
||||
}
|
||||
|
||||
Injector injector = Guice.createInjector(modules);
|
||||
Object instance = injector
|
||||
.getInstance(((JvmVelocityPluginDescription) description).getMainClass());
|
||||
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException(
|
||||
"Got nothing from injector for plugin " + description.id());
|
||||
}
|
||||
|
||||
((VelocityPluginContainer) container).setInstance(instance);
|
||||
}
|
||||
|
||||
private List<SerializedPluginDescription> getSerializedPluginInfo(Path source)
|
||||
throws Exception {
|
||||
boolean foundOldVelocityPlugin = false;
|
||||
boolean foundBungeeBukkitPluginFile = false;
|
||||
try (JarInputStream in = new JarInputStream(
|
||||
new BufferedInputStream(Files.newInputStream(source)))) {
|
||||
JarEntry entry;
|
||||
while ((entry = in.getNextJarEntry()) != null) {
|
||||
if (entry.getName().equals("velocity-plugin-info.json")) {
|
||||
try (Reader pluginInfoReader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
|
||||
return VelocityServer.GENERAL_GSON.fromJson(pluginInfoReader,
|
||||
new TypeToken<List<SerializedPluginDescription>>() {}.getType());
|
||||
}
|
||||
}
|
||||
|
||||
if (entry.getName().equals("velocity-plugin.json")) {
|
||||
foundOldVelocityPlugin = true;
|
||||
}
|
||||
|
||||
if (entry.getName().equals("plugin.yml") || entry.getName().equals("bungee.yml")) {
|
||||
foundBungeeBukkitPluginFile = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundOldVelocityPlugin) {
|
||||
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
|
||||
+ "be developed for an older version of Velocity. Please obtain a newer version of the "
|
||||
+ "plugin.");
|
||||
}
|
||||
|
||||
if (foundBungeeBukkitPluginFile) {
|
||||
throw new InvalidPluginException("The plugin file " + source.getFileName() + " appears to "
|
||||
+ "be a Bukkit or BungeeCord plugin. Velocity does not support Bukkit or BungeeCord "
|
||||
+ "plugins.");
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private VelocityPluginDescription createCandidateDescription(
|
||||
SerializedPluginDescription description,
|
||||
Path source) {
|
||||
Set<PluginDependency> dependencies = new HashSet<>();
|
||||
|
||||
for (SerializedPluginDescription.Dependency dependency : description.getDependencies()) {
|
||||
dependencies.add(toDependencyMeta(dependency));
|
||||
}
|
||||
|
||||
return new JvmVelocityPluginDescriptionCandidate(
|
||||
description.getId(),
|
||||
description.getName(),
|
||||
description.getVersion(),
|
||||
description.getDescription(),
|
||||
description.getUrl(),
|
||||
description.getAuthors(),
|
||||
dependencies,
|
||||
source,
|
||||
description.getMain()
|
||||
);
|
||||
}
|
||||
|
||||
private VelocityPluginDescription createDescription(
|
||||
JvmVelocityPluginDescriptionCandidate description,
|
||||
Class mainClass) {
|
||||
return new JvmVelocityPluginDescription(
|
||||
description.id(),
|
||||
description.name(),
|
||||
description.version(),
|
||||
description.description(),
|
||||
description.url(),
|
||||
description.authors(),
|
||||
description.dependencies(),
|
||||
description.file(),
|
||||
mainClass
|
||||
);
|
||||
}
|
||||
|
||||
private static PluginDependency toDependencyMeta(
|
||||
SerializedPluginDescription.Dependency dependency) {
|
||||
return new PluginDependency(
|
||||
dependency.getId(),
|
||||
dependency.getVersion(),
|
||||
dependency.isOptional()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader.jvm;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
class JvmVelocityPluginDescription extends VelocityPluginDescription {
|
||||
|
||||
private final Class<?> mainClass;
|
||||
|
||||
JvmVelocityPluginDescription(String id, @Nullable String name, String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies,
|
||||
@Nullable Path source, Class<?> mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
Class<?> getMainClass() {
|
||||
return mainClass;
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader.jvm;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
class JvmVelocityPluginDescriptionCandidate extends VelocityPluginDescription {
|
||||
|
||||
private final String mainClass;
|
||||
|
||||
JvmVelocityPluginDescriptionCandidate(String id, @Nullable String name, String version,
|
||||
@Nullable String description, @Nullable String url,
|
||||
@Nullable List<String> authors, Collection<PluginDependency> dependencies, Path source,
|
||||
String mainClass) {
|
||||
super(id, name, version, description, url, authors, dependencies, source);
|
||||
this.mainClass = checkNotNull(mainClass);
|
||||
}
|
||||
|
||||
String getMainClass() {
|
||||
return mainClass;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.loader.jvm;
|
||||
|
||||
import com.google.inject.Binder;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.Scopes;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import java.nio.file.Path;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class VelocityPluginModule implements Module {
|
||||
|
||||
private final JvmVelocityPluginDescription description;
|
||||
private final PluginContainer pluginContainer;
|
||||
private final Path basePluginPath;
|
||||
|
||||
VelocityPluginModule(JvmVelocityPluginDescription description,
|
||||
PluginContainer pluginContainer, Path basePluginPath) {
|
||||
this.description = description;
|
||||
this.pluginContainer = pluginContainer;
|
||||
this.basePluginPath = basePluginPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(Binder binder) {
|
||||
binder.bind(description.getMainClass()).in(Scopes.SINGLETON);
|
||||
|
||||
binder.bind(Logger.class).toInstance(LoggerFactory.getLogger(description.id()));
|
||||
binder.bind(Path.class).annotatedWith(DataDirectory.class)
|
||||
.toInstance(basePluginPath.resolve(description.id()));
|
||||
binder.bind(PluginDescription.class).toInstance(description);
|
||||
binder.bind(PluginContainer.class).toInstance(pluginContainer);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.graph.Graph;
|
||||
import com.google.common.graph.GraphBuilder;
|
||||
import com.google.common.graph.MutableGraph;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class PluginDependencyUtils {
|
||||
|
||||
private PluginDependencyUtils() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to topographically sort all plugins for the proxy to load by dependencies using
|
||||
* a depth-first search.
|
||||
*
|
||||
* @param candidates the plugins to sort
|
||||
* @return the sorted list of plugins
|
||||
* @throws IllegalStateException if there is a circular loop in the dependency graph
|
||||
*/
|
||||
public static List<PluginDescription> sortCandidates(List<PluginDescription> candidates) {
|
||||
List<PluginDescription> sortedCandidates = new ArrayList<>(candidates);
|
||||
sortedCandidates.sort(Comparator.comparing(PluginDescription::id));
|
||||
|
||||
// Create a graph and populate it with plugin dependencies. Specifically, each graph has plugin
|
||||
// nodes, and edges that represent the dependencies that plugin relies on. Non-existent plugins
|
||||
// are ignored.
|
||||
MutableGraph<PluginDescription> graph = GraphBuilder.directed()
|
||||
.allowsSelfLoops(false)
|
||||
.expectedNodeCount(sortedCandidates.size())
|
||||
.build();
|
||||
Map<String, PluginDescription> candidateMap = Maps.uniqueIndex(sortedCandidates,
|
||||
PluginDescription::id);
|
||||
|
||||
for (PluginDescription description : sortedCandidates) {
|
||||
graph.addNode(description);
|
||||
|
||||
for (PluginDependency dependency : description.dependencies()) {
|
||||
PluginDescription in = candidateMap.get(dependency.id());
|
||||
|
||||
if (in != null) {
|
||||
graph.putEdge(description, in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we do the depth-first search.
|
||||
List<PluginDescription> sorted = new ArrayList<>();
|
||||
Map<PluginDescription, Mark> marks = new HashMap<>();
|
||||
|
||||
for (PluginDescription node : graph.nodes()) {
|
||||
visitNode(graph, node, marks, sorted, new ArrayDeque<>());
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
private static void visitNode(Graph<PluginDescription> dependencyGraph, PluginDescription node,
|
||||
Map<PluginDescription, Mark> marks, List<PluginDescription> sorted,
|
||||
Deque<PluginDescription> currentIteration) {
|
||||
Mark mark = marks.getOrDefault(node, Mark.NOT_VISITED);
|
||||
if (mark == Mark.PERMANENT) {
|
||||
return;
|
||||
} else if (mark == Mark.TEMPORARY) {
|
||||
// A circular dependency has been detected.
|
||||
currentIteration.addLast(node);
|
||||
StringBuilder loopGraph = new StringBuilder();
|
||||
for (PluginDescription description : currentIteration) {
|
||||
loopGraph.append(description.id());
|
||||
loopGraph.append(" -> ");
|
||||
}
|
||||
loopGraph.setLength(loopGraph.length() - 4);
|
||||
throw new IllegalStateException("Circular dependency detected: " + loopGraph.toString());
|
||||
}
|
||||
|
||||
currentIteration.addLast(node);
|
||||
marks.put(node, Mark.TEMPORARY);
|
||||
for (PluginDescription edge : dependencyGraph.successors(node)) {
|
||||
visitNode(dependencyGraph, edge, marks, sorted, currentIteration);
|
||||
}
|
||||
|
||||
marks.put(node, Mark.PERMANENT);
|
||||
currentIteration.removeLast();
|
||||
sorted.add(node);
|
||||
}
|
||||
|
||||
private enum Mark {
|
||||
NOT_VISITED,
|
||||
TEMPORARY,
|
||||
PERMANENT
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import com.google.common.base.MoreObjects;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.proxy.VelocityServer;
|
||||
import java.util.List;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class ProxyPluginContainer implements PluginContainer {
|
||||
|
||||
public static final PluginContainer VELOCITY = new ProxyPluginContainer();
|
||||
|
||||
private final PluginDescription description = new PluginDescription() {
|
||||
@Override
|
||||
public String id() {
|
||||
return "velocity";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
final Package pkg = VelocityServer.class.getPackage();
|
||||
return MoreObjects.firstNonNull(pkg.getImplementationTitle(), "Velocity");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
final Package pkg = VelocityServer.class.getPackage();
|
||||
return MoreObjects.firstNonNull(pkg.getImplementationVersion(), "<unknown>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> authors() {
|
||||
final Package pkg = VelocityServer.class.getPackage();
|
||||
final String vendor = MoreObjects.firstNonNull(pkg.getImplementationVendor(),
|
||||
"Velocity Contributors");
|
||||
return List.of(vendor);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public PluginDescription description() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object instance() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.Scheduler;
|
||||
import com.velocitypowered.api.scheduler.TaskStatus;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class VelocityScheduler implements Scheduler {
|
||||
|
||||
private final PluginManager pluginManager;
|
||||
private final ExecutorService taskService;
|
||||
private final ScheduledExecutorService timerExecutionService;
|
||||
private final Multimap<Object, ScheduledTask> tasksByPlugin = Multimaps.synchronizedMultimap(
|
||||
Multimaps.newSetMultimap(new IdentityHashMap<>(), HashSet::new));
|
||||
|
||||
/**
|
||||
* Initalizes the scheduler.
|
||||
*
|
||||
* @param pluginManager the Velocity plugin manager
|
||||
*/
|
||||
public VelocityScheduler(PluginManager pluginManager) {
|
||||
this.pluginManager = pluginManager;
|
||||
this.taskService = Executors.newCachedThreadPool(new ThreadFactoryBuilder().setDaemon(true)
|
||||
.setNameFormat("Velocity Task Scheduler - #%d").build());
|
||||
this.timerExecutionService = Executors
|
||||
.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder().setDaemon(true)
|
||||
.setNameFormat("Velocity Task Scheduler Timer").build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder buildTask(Object plugin, Runnable runnable) {
|
||||
checkNotNull(plugin, "plugin");
|
||||
checkNotNull(runnable, "runnable");
|
||||
checkArgument(pluginManager.fromInstance(plugin) != null, "plugin is not registered");
|
||||
return new TaskBuilderImpl(plugin, runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the Velocity scheduler.
|
||||
* @return {@code true} if all tasks finished, {@code false} otherwise
|
||||
* @throws InterruptedException if the current thread was interrupted
|
||||
*/
|
||||
public boolean shutdown() throws InterruptedException {
|
||||
Collection<ScheduledTask> terminating;
|
||||
synchronized (tasksByPlugin) {
|
||||
terminating = ImmutableList.copyOf(tasksByPlugin.values());
|
||||
}
|
||||
for (ScheduledTask task : terminating) {
|
||||
task.cancel();
|
||||
}
|
||||
timerExecutionService.shutdown();
|
||||
taskService.shutdown();
|
||||
return taskService.awaitTermination(10, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private class TaskBuilderImpl implements TaskBuilder {
|
||||
|
||||
private final Object plugin;
|
||||
private final Runnable runnable;
|
||||
private long delay; // ms
|
||||
private long repeat; // ms
|
||||
|
||||
private TaskBuilderImpl(Object plugin, Runnable runnable) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder delay(long time, TimeUnit unit) {
|
||||
this.delay = unit.toMillis(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder repeat(long time, TimeUnit unit) {
|
||||
this.repeat = unit.toMillis(time);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder clearDelay() {
|
||||
this.delay = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskBuilder clearRepeat() {
|
||||
this.repeat = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScheduledTask schedule() {
|
||||
VelocityTask task = new VelocityTask(plugin, runnable, delay, repeat);
|
||||
tasksByPlugin.put(plugin, task);
|
||||
task.schedule();
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
||||
private class VelocityTask implements Runnable, ScheduledTask {
|
||||
|
||||
private final Object plugin;
|
||||
private final Runnable runnable;
|
||||
private final long delay;
|
||||
private final long repeat;
|
||||
private @Nullable ScheduledFuture<?> future;
|
||||
private volatile @Nullable Thread currentTaskThread;
|
||||
|
||||
private VelocityTask(Object plugin, Runnable runnable, long delay, long repeat) {
|
||||
this.plugin = plugin;
|
||||
this.runnable = runnable;
|
||||
this.delay = delay;
|
||||
this.repeat = repeat;
|
||||
}
|
||||
|
||||
void schedule() {
|
||||
if (repeat == 0) {
|
||||
this.future = timerExecutionService.schedule(this, delay, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
this.future = timerExecutionService
|
||||
.scheduleAtFixedRate(this, delay, repeat, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object plugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TaskStatus status() {
|
||||
if (future == null) {
|
||||
return TaskStatus.SCHEDULED;
|
||||
}
|
||||
|
||||
if (future.isCancelled()) {
|
||||
return TaskStatus.CANCELLED;
|
||||
}
|
||||
|
||||
if (future.isDone()) {
|
||||
return TaskStatus.FINISHED;
|
||||
}
|
||||
|
||||
return TaskStatus.SCHEDULED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (future != null) {
|
||||
future.cancel(false);
|
||||
|
||||
Thread cur = currentTaskThread;
|
||||
if (cur != null) {
|
||||
cur.interrupt();
|
||||
}
|
||||
|
||||
onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
taskService.execute(() -> {
|
||||
currentTaskThread = Thread.currentThread();
|
||||
try {
|
||||
runnable.run();
|
||||
} catch (Throwable e) {
|
||||
//noinspection ConstantConditions
|
||||
if (e instanceof InterruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
} else {
|
||||
PluginDescription description = pluginManager.ensurePluginContainer(plugin)
|
||||
.description();
|
||||
Log.logger.error("Exception in task {} by plugin {}", runnable,
|
||||
description.name(), e);
|
||||
}
|
||||
} finally {
|
||||
if (repeat == 0) {
|
||||
onFinish();
|
||||
}
|
||||
currentTaskThread = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onFinish() {
|
||||
tasksByPlugin.remove(plugin, this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Log {
|
||||
|
||||
private static final Logger logger = LogManager.getLogger(VelocityTask.class);
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.util;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.suggestion.SuggestionProvider;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import java.util.Locale;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
/**
|
||||
* Provides utilities for working with Brigadier commands.
|
||||
*/
|
||||
public final class BrigadierUtils {
|
||||
|
||||
private static final Splitter SPACE_SPLITTER = Splitter.on(' ');
|
||||
|
||||
/**
|
||||
* Returns a literal node that redirects its execution to
|
||||
* the given destination node.
|
||||
*
|
||||
* @param alias the command alias
|
||||
* @param destination the destination node
|
||||
* @return the built node
|
||||
*/
|
||||
public static LiteralCommandNode<CommandSource> buildRedirect(
|
||||
final String alias, final LiteralCommandNode<CommandSource> destination) {
|
||||
// Redirects only work for nodes with children, but break the top argument-less command.
|
||||
// Manually adding the root command after setting the redirect doesn't fix it.
|
||||
// See https://github.com/Mojang/brigadier/issues/46). Manually clone the node instead.
|
||||
LiteralArgumentBuilder<CommandSource> builder = LiteralArgumentBuilder
|
||||
.<CommandSource>literal(alias.toLowerCase(Locale.ENGLISH))
|
||||
.requires(destination.getRequirement())
|
||||
.forward(
|
||||
destination.getRedirect(), destination.getRedirectModifier(), destination.isFork())
|
||||
.executes(destination.getCommand());
|
||||
for (CommandNode<CommandSource> child : destination.getChildren()) {
|
||||
builder.then(child);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a literal node that optionally accepts arguments
|
||||
* as a raw {@link String}.
|
||||
*
|
||||
* @param alias the literal alias
|
||||
* @param brigadierCommand the command to execute
|
||||
* @param suggestionProvider the suggestion provider
|
||||
* @return the built node
|
||||
*/
|
||||
public static LiteralCommandNode<CommandSource> buildRawArgumentsLiteral(
|
||||
final String alias, final Command<CommandSource> brigadierCommand,
|
||||
SuggestionProvider<CommandSource> suggestionProvider) {
|
||||
return LiteralArgumentBuilder
|
||||
.<CommandSource>literal(alias.toLowerCase(Locale.ENGLISH))
|
||||
.then(RequiredArgumentBuilder
|
||||
.<CommandSource, String>argument("arguments", StringArgumentType.greedyString())
|
||||
.suggests(suggestionProvider)
|
||||
.executes(brigadierCommand))
|
||||
.executes(brigadierCommand)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the used command alias.
|
||||
*
|
||||
* @param context the command context
|
||||
* @return the parsed command alias
|
||||
*/
|
||||
public static String getAlias(final CommandContext<CommandSource> context) {
|
||||
return context.getNodes().get(0).getNode().getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the raw {@link String} arguments of a command execution.
|
||||
*
|
||||
* @param context the command context
|
||||
* @return the parsed arguments
|
||||
*/
|
||||
public static String getRawArguments(final CommandContext<CommandSource> context) {
|
||||
String cmdLine = context.getInput();
|
||||
int firstSpace = cmdLine.indexOf(' ');
|
||||
if (firstSpace == -1) {
|
||||
return "";
|
||||
}
|
||||
return cmdLine.substring(firstSpace + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the splitted arguments of a command node built with
|
||||
* {@link #buildRawArgumentsLiteral(String, Command, SuggestionProvider)}.
|
||||
*
|
||||
* @param context the command context
|
||||
* @return the parsed arguments
|
||||
*/
|
||||
public static String[] getSplitArguments(final CommandContext<CommandSource> context) {
|
||||
String line = getRawArguments(context);
|
||||
if (line.isEmpty()) {
|
||||
return new String[0];
|
||||
}
|
||||
return SPACE_SPLITTER.splitToList(line).toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the normalized representation of the given command input.
|
||||
*
|
||||
* @param cmdLine the command input
|
||||
* @param trim whether to trim argument-less inputs
|
||||
* @return the normalized command
|
||||
*/
|
||||
public static String normalizeInput(final String cmdLine, final boolean trim) {
|
||||
// Command aliases are case insensitive, but Brigadier isn't
|
||||
String command = trim ? cmdLine.trim() : cmdLine;
|
||||
int firstSpace = command.indexOf(' ');
|
||||
if (firstSpace != -1) {
|
||||
return command.substring(0, firstSpace).toLowerCase(Locale.ENGLISH)
|
||||
+ command.substring(firstSpace);
|
||||
}
|
||||
return command.toLowerCase(Locale.ENGLISH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the given command node prior for hinting metadata to
|
||||
* a {@link com.velocitypowered.api.command.Command}.
|
||||
*
|
||||
* @param node the command node to be wrapped
|
||||
* @param command the command to execute
|
||||
* @return the wrapped command node
|
||||
*/
|
||||
public static CommandNode<CommandSource> wrapForHinting(
|
||||
final CommandNode<CommandSource> node, final @Nullable Command<CommandSource> command) {
|
||||
Preconditions.checkNotNull(node, "node");
|
||||
ArgumentBuilder<CommandSource, ?> builder = node.createBuilder();
|
||||
builder.executes(command);
|
||||
for (CommandNode<CommandSource> child : node.getChildren()) {
|
||||
builder.then(wrapForHinting(child, command));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private BrigadierUtils() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.util.adventure;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.kyori.adventure.nbt.CompoundBinaryTag;
|
||||
import net.kyori.adventure.nbt.TagStringIO;
|
||||
import net.kyori.adventure.nbt.api.BinaryTagHolder;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent.ShowEntity;
|
||||
import net.kyori.adventure.text.event.HoverEvent.ShowItem;
|
||||
import net.kyori.adventure.text.serializer.gson.LegacyHoverEventSerializer;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
|
||||
import net.kyori.adventure.util.Codec.Decoder;
|
||||
import net.kyori.adventure.util.Codec.Encoder;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
/**
|
||||
* An implementation of {@link LegacyHoverEventSerializer} that implements the interface in the
|
||||
* most literal, albeit "incompatible" way possible.
|
||||
*/
|
||||
public class VelocityLegacyHoverEventSerializer implements LegacyHoverEventSerializer {
|
||||
|
||||
public static final LegacyHoverEventSerializer INSTANCE =
|
||||
new VelocityLegacyHoverEventSerializer();
|
||||
|
||||
private VelocityLegacyHoverEventSerializer() {
|
||||
|
||||
}
|
||||
|
||||
private static Key legacyIdToFakeKey(byte id) {
|
||||
return Key.key("velocity", "legacy_hover/id_" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HoverEvent.@NonNull ShowItem deserializeShowItem(@NonNull Component input)
|
||||
throws IOException {
|
||||
String snbt = PlainComponentSerializer.plain().serialize(input);
|
||||
CompoundBinaryTag item = TagStringIO.get().asCompound(snbt);
|
||||
|
||||
Key key;
|
||||
String idIfString = item.getString("id", "");
|
||||
if (idIfString.isEmpty()) {
|
||||
key = legacyIdToFakeKey(item.getByte("id"));
|
||||
} else {
|
||||
key = Key.key(idIfString);
|
||||
}
|
||||
|
||||
byte count = item.getByte("Count", (byte) 1);
|
||||
return ShowItem.of(key, count, BinaryTagHolder.of(snbt));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HoverEvent.@NonNull ShowEntity deserializeShowEntity(@NonNull Component input,
|
||||
Decoder<Component, String, ? extends RuntimeException> componentDecoder) throws IOException {
|
||||
String snbt = PlainComponentSerializer.plain().serialize(input);
|
||||
CompoundBinaryTag item = TagStringIO.get().asCompound(snbt);
|
||||
|
||||
Component name;
|
||||
try {
|
||||
name = componentDecoder.decode(item.getString("name"));
|
||||
} catch (Exception e) {
|
||||
name = Component.text(item.getString("name"));
|
||||
}
|
||||
|
||||
return ShowEntity.of(Key.key(item.getString("type")),
|
||||
UUID.fromString(item.getString("id")),
|
||||
name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Component serializeShowItem(HoverEvent.@NonNull ShowItem input)
|
||||
throws IOException {
|
||||
final CompoundBinaryTag.Builder builder = CompoundBinaryTag.builder()
|
||||
.putByte("Count", (byte) input.count());
|
||||
|
||||
String keyAsString = input.item().asString();
|
||||
if (keyAsString.startsWith("velocity:legacy_hover/id_")) {
|
||||
builder.putByte("id", Byte.parseByte(keyAsString
|
||||
.substring("velocity:legacy_hover/id_".length())));
|
||||
} else {
|
||||
builder.putString("id", keyAsString);
|
||||
}
|
||||
|
||||
BinaryTagHolder nbt = input.nbt();
|
||||
if (nbt != null) {
|
||||
builder.put("tag", TagStringIO.get().asCompound(nbt.string()));
|
||||
}
|
||||
|
||||
return Component.text(TagStringIO.get().asString(builder.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Component serializeShowEntity(HoverEvent.@NonNull ShowEntity input,
|
||||
Encoder<Component, String, ? extends RuntimeException> componentEncoder) throws IOException {
|
||||
CompoundBinaryTag.Builder tag = CompoundBinaryTag.builder()
|
||||
.putString("id", input.id().toString())
|
||||
.putString("type", input.type().asString());
|
||||
Component name = input.name();
|
||||
if (name != null) {
|
||||
tag.putString("name", componentEncoder.encode(name));
|
||||
}
|
||||
return Component.text(TagStringIO.get().asString(tag.build()));
|
||||
}
|
||||
}
|
||||
@@ -1,506 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.mojang.brigadier.arguments.IntegerArgumentType;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.tree.ArgumentCommandNode;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import com.mojang.brigadier.tree.LiteralCommandNode;
|
||||
import com.velocitypowered.api.command.BrigadierCommand;
|
||||
import com.velocitypowered.api.command.CommandMeta;
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.RawCommand;
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.proxy.event.MockEventManager;
|
||||
import com.velocitypowered.proxy.event.VelocityEventManager;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class CommandManagerTests {
|
||||
|
||||
private static final VelocityEventManager EVENT_MANAGER = new MockEventManager();
|
||||
|
||||
static {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
try {
|
||||
EVENT_MANAGER.shutdown();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
static VelocityCommandManager createManager() {
|
||||
return new VelocityCommandManager(EVENT_MANAGER);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstruction() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
assertFalse(manager.hasCommand("foo"));
|
||||
assertTrue(manager.getDispatcher().getRoot().getChildren().isEmpty());
|
||||
assertFalse(manager.execute(MockCommandSource.INSTANCE, "foo").join());
|
||||
assertFalse(manager.executeImmediately(MockCommandSource.INSTANCE, "bar").join());
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "").join().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBrigadierRegister() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
LiteralCommandNode<CommandSource> node = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("foo")
|
||||
.build();
|
||||
BrigadierCommand command = new BrigadierCommand(node);
|
||||
manager.register(command);
|
||||
|
||||
assertEquals(node, command.getNode());
|
||||
assertTrue(manager.hasCommand("fOo"));
|
||||
|
||||
LiteralCommandNode<CommandSource> barNode = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("bar")
|
||||
.build();
|
||||
BrigadierCommand aliasesCommand = new BrigadierCommand(barNode);
|
||||
CommandMeta meta = manager.createMetaBuilder(aliasesCommand)
|
||||
.aliases("baZ")
|
||||
.build();
|
||||
|
||||
assertEquals(ImmutableSet.of("bar", "baz"), meta.aliases());
|
||||
assertTrue(meta.hints().isEmpty());
|
||||
manager.register(meta, aliasesCommand);
|
||||
assertTrue(manager.hasCommand("bAr"));
|
||||
assertTrue(manager.hasCommand("Baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleRegister() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
SimpleCommand command = new NoopSimpleCommand();
|
||||
|
||||
manager.register("Foo", command);
|
||||
assertTrue(manager.hasCommand("foO"));
|
||||
manager.unregister("fOo");
|
||||
assertFalse(manager.hasCommand("foo"));
|
||||
assertFalse(manager.execute(MockCommandSource.INSTANCE, "foo").join());
|
||||
|
||||
manager.register("foo", command, "bAr", "BAZ");
|
||||
assertTrue(manager.hasCommand("bar"));
|
||||
assertTrue(manager.hasCommand("bAz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRawRegister() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
RawCommand command = new NoopRawCommand();
|
||||
|
||||
manager.register("foO", command, "BAR");
|
||||
assertTrue(manager.hasCommand("fOo"));
|
||||
assertTrue(manager.hasCommand("bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBrigadierExecute() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
AtomicBoolean executed = new AtomicBoolean(false);
|
||||
AtomicBoolean checkedRequires = new AtomicBoolean(false);
|
||||
LiteralCommandNode<CommandSource> node = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("buy")
|
||||
.executes(context -> {
|
||||
assertEquals(MockCommandSource.INSTANCE, context.getSource());
|
||||
assertEquals("buy", context.getInput());
|
||||
executed.set(true);
|
||||
return 1;
|
||||
})
|
||||
.build();
|
||||
CommandNode<CommandSource> quantityNode = RequiredArgumentBuilder
|
||||
.<CommandSource, Integer>argument("quantity", IntegerArgumentType.integer(12, 16))
|
||||
.requires(source -> {
|
||||
assertEquals(MockCommandSource.INSTANCE, source);
|
||||
checkedRequires.set(true);
|
||||
return true;
|
||||
})
|
||||
.executes(context -> {
|
||||
int argument = IntegerArgumentType.getInteger(context, "quantity");
|
||||
assertEquals(14, argument);
|
||||
executed.set(true);
|
||||
return 1;
|
||||
})
|
||||
.build();
|
||||
CommandNode<CommandSource> productNode = RequiredArgumentBuilder
|
||||
.<CommandSource, String>argument("product", StringArgumentType.string())
|
||||
.requires(source -> {
|
||||
checkedRequires.set(true);
|
||||
return false;
|
||||
})
|
||||
.executes(context -> fail("was executed"))
|
||||
.build();
|
||||
quantityNode.addChild(productNode);
|
||||
node.addChild(quantityNode);
|
||||
manager.register(new BrigadierCommand(node));
|
||||
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "buy ").join());
|
||||
assertTrue(executed.compareAndSet(true, false), "was executed");
|
||||
assertTrue(manager.executeImmediately(MockCommandSource.INSTANCE, "buy 14").join());
|
||||
assertTrue(checkedRequires.compareAndSet(true, false));
|
||||
assertTrue(executed.get());
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "buy 9").join(),
|
||||
"Invalid arg returns false");
|
||||
assertTrue(manager.executeImmediately(MockCommandSource.INSTANCE, "buy 12 bananas")
|
||||
.join());
|
||||
assertTrue(checkedRequires.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSimpleExecute() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
AtomicBoolean executed = new AtomicBoolean(false);
|
||||
SimpleCommand command = invocation -> {
|
||||
assertEquals(MockCommandSource.INSTANCE, invocation.source());
|
||||
assertArrayEquals(new String[] {"bar", "254"}, invocation.arguments());
|
||||
executed.set(true);
|
||||
};
|
||||
manager.register("foo", command);
|
||||
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "foo bar 254").join());
|
||||
assertTrue(executed.get());
|
||||
|
||||
SimpleCommand noPermsCommand = new SimpleCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
fail("was executed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(final Invocation invocation) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
manager.register("dangerous", noPermsCommand, "veryDangerous");
|
||||
assertFalse(manager.execute(MockCommandSource.INSTANCE, "dangerous").join());
|
||||
assertFalse(manager.executeImmediately(MockCommandSource.INSTANCE, "verydangerous 123")
|
||||
.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRawExecute() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
AtomicBoolean executed = new AtomicBoolean(false);
|
||||
RawCommand command = new RawCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
assertEquals(MockCommandSource.INSTANCE, invocation.source());
|
||||
assertEquals("lobby 23", invocation.arguments());
|
||||
executed.set(true);
|
||||
}
|
||||
};
|
||||
manager.register("sendMe", command);
|
||||
|
||||
assertTrue(manager.executeImmediately(MockCommandSource.INSTANCE, "sendMe lobby 23")
|
||||
.join());
|
||||
assertTrue(executed.compareAndSet(true, false));
|
||||
|
||||
RawCommand noArgsCommand = new RawCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
assertEquals("", invocation.arguments());
|
||||
executed.set(true);
|
||||
}
|
||||
};
|
||||
manager.register("noargs", noArgsCommand);
|
||||
|
||||
assertTrue(manager.executeImmediately(MockCommandSource.INSTANCE, "noargs").join());
|
||||
assertTrue(executed.get());
|
||||
assertTrue(manager.executeImmediately(MockCommandSource.INSTANCE, "noargs ").join());
|
||||
|
||||
RawCommand noPermsCommand = new RawCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
fail("was executed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(final Invocation invocation) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
manager.register("sendThem", noPermsCommand);
|
||||
assertFalse(manager.executeImmediately(MockCommandSource.INSTANCE, "sendThem foo")
|
||||
.join());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuggestions() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
|
||||
LiteralCommandNode<CommandSource> brigadierNode = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("brigadier")
|
||||
.build();
|
||||
CommandNode<CommandSource> nameNode = RequiredArgumentBuilder
|
||||
.<CommandSource, String>argument("name", StringArgumentType.string())
|
||||
.build();
|
||||
CommandNode<CommandSource> numberNode = RequiredArgumentBuilder
|
||||
.<CommandSource, Integer>argument("quantity", IntegerArgumentType.integer())
|
||||
.suggests((context, builder) -> builder.suggest(2).suggest(3).buildFuture())
|
||||
.build();
|
||||
nameNode.addChild(numberNode);
|
||||
brigadierNode.addChild(nameNode);
|
||||
manager.register(new BrigadierCommand(brigadierNode));
|
||||
|
||||
SimpleCommand simpleCommand = new SimpleCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
fail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(final Invocation invocation) {
|
||||
switch (invocation.arguments().length) {
|
||||
case 0:
|
||||
return ImmutableList.of("foo", "bar");
|
||||
case 1:
|
||||
return ImmutableList.of("123");
|
||||
default:
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
};
|
||||
manager.register("simple", simpleCommand);
|
||||
|
||||
RawCommand rawCommand = new RawCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
fail();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(final Invocation invocation) {
|
||||
switch (invocation.arguments()) {
|
||||
case "":
|
||||
return ImmutableList.of("foo", "baz");
|
||||
case "foo ":
|
||||
return ImmutableList.of("2", "3", "5", "7");
|
||||
case "bar ":
|
||||
return ImmutableList.of("11", "13", "17");
|
||||
default:
|
||||
return ImmutableList.of();
|
||||
}
|
||||
}
|
||||
};
|
||||
manager.register("raw", rawCommand);
|
||||
|
||||
assertEquals(
|
||||
ImmutableList.of("brigadier", "raw", "simple"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "").join(),
|
||||
"literals are in alphabetical order");
|
||||
assertEquals(
|
||||
ImmutableList.of("brigadier"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "briga").join());
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier")
|
||||
.join().isEmpty());
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier ")
|
||||
.join().isEmpty());
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier foo")
|
||||
.join().isEmpty());
|
||||
assertEquals(
|
||||
ImmutableList.of("2", "3"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "brigadier foo ").join());
|
||||
assertEquals(
|
||||
ImmutableList.of("bar", "foo"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "simple ").join());
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "simple")
|
||||
.join().isEmpty());
|
||||
assertEquals(
|
||||
ImmutableList.of("123"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "simPle foo").join());
|
||||
assertEquals(
|
||||
ImmutableList.of("baz", "foo"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "raw ").join());
|
||||
assertEquals(
|
||||
ImmutableList.of("2", "3", "5", "7"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "raw foo ").join());
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "raw foo")
|
||||
.join().isEmpty());
|
||||
assertEquals(
|
||||
ImmutableList.of("11", "13", "17"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "rAW bar ").join());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBrigadierSuggestionPermissions() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
LiteralCommandNode<CommandSource> manageNode = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("manage")
|
||||
.requires(source -> false)
|
||||
.build();
|
||||
CommandNode<CommandSource> idNode = RequiredArgumentBuilder
|
||||
.<CommandSource, Integer>argument("id", IntegerArgumentType.integer(0))
|
||||
.suggests((context, builder) -> fail("called suggestion builder"))
|
||||
.build();
|
||||
manageNode.addChild(idNode);
|
||||
manager.register(new BrigadierCommand(manageNode));
|
||||
|
||||
// Brigadier doesn't call the children predicate when requesting suggestions.
|
||||
// However, it won't query children if the source doesn't pass the parent
|
||||
// #requires predicate.
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "manage ")
|
||||
.join().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testHinting() {
|
||||
VelocityCommandManager manager = createManager();
|
||||
AtomicBoolean executed = new AtomicBoolean(false);
|
||||
AtomicBoolean calledSuggestionProvider = new AtomicBoolean(false);
|
||||
AtomicReference<String> expectedArgs = new AtomicReference<>();
|
||||
RawCommand command = new RawCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
assertEquals(expectedArgs.get(), invocation.arguments());
|
||||
executed.set(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(final Invocation invocation) {
|
||||
return ImmutableList.of("raw");
|
||||
}
|
||||
};
|
||||
|
||||
CommandNode<CommandSource> barHint = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("bar")
|
||||
.executes(context -> fail("hints don't get executed"))
|
||||
.build();
|
||||
ArgumentCommandNode<CommandSource, Integer> numberArg = RequiredArgumentBuilder
|
||||
.<CommandSource, Integer>argument("number", IntegerArgumentType.integer())
|
||||
.suggests((context, builder) -> {
|
||||
calledSuggestionProvider.set(true);
|
||||
return builder.suggest("456").buildFuture();
|
||||
})
|
||||
.build();
|
||||
barHint.addChild(numberArg);
|
||||
CommandNode<CommandSource> bazHint = LiteralArgumentBuilder
|
||||
.<CommandSource>literal("baz")
|
||||
.build();
|
||||
CommandMeta meta = manager.createMetaBuilder("foo")
|
||||
.aliases("foo2")
|
||||
.hint(barHint)
|
||||
.hint(bazHint)
|
||||
.build();
|
||||
manager.register(meta, command);
|
||||
|
||||
expectedArgs.set("notBarOrBaz");
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "foo notBarOrBaz").join());
|
||||
assertTrue(executed.compareAndSet(true, false));
|
||||
expectedArgs.set("anotherArg 123");
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "Foo2 anotherArg 123").join());
|
||||
assertTrue(executed.compareAndSet(true, false));
|
||||
expectedArgs.set("bar");
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "foo bar").join());
|
||||
assertTrue(executed.compareAndSet(true, false));
|
||||
expectedArgs.set("bar 123");
|
||||
assertTrue(manager.execute(MockCommandSource.INSTANCE, "foo2 bar 123").join());
|
||||
assertTrue(executed.compareAndSet(true, false));
|
||||
|
||||
assertEquals(ImmutableList.of("bar", "baz", "raw"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo ").join());
|
||||
assertFalse(calledSuggestionProvider.get());
|
||||
assertEquals(ImmutableList.of("456"),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo bar ").join());
|
||||
assertTrue(calledSuggestionProvider.compareAndSet(true, false));
|
||||
assertEquals(ImmutableList.of(),
|
||||
manager.offerSuggestions(MockCommandSource.INSTANCE, "foo2 baz ").join());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuggestionPermissions() throws ExecutionException, InterruptedException {
|
||||
VelocityCommandManager manager = createManager();
|
||||
RawCommand rawCommand = new RawCommand() {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
fail("The Command should not be executed while testing suggestions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Invocation invocation) {
|
||||
return invocation.arguments().length() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(final Invocation invocation) {
|
||||
return ImmutableList.of("suggestion");
|
||||
}
|
||||
};
|
||||
|
||||
manager.register("foo", rawCommand);
|
||||
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo").get().isEmpty());
|
||||
assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "foo bar").get().isEmpty());
|
||||
|
||||
SimpleCommand oldCommand = new SimpleCommand() {
|
||||
@Override
|
||||
public void execute(Invocation invocation) {
|
||||
fail("The Command should not be executed while testing suggestions");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Invocation invocation) {
|
||||
return invocation.arguments().length > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(Invocation invocation) {
|
||||
return ImmutableList.of("suggestion");
|
||||
}
|
||||
};
|
||||
|
||||
manager.register("bar", oldCommand);
|
||||
|
||||
assertTrue(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar").get().isEmpty());
|
||||
assertFalse(manager.offerSuggestions(MockCommandSource.INSTANCE, "bar foo").get().isEmpty());
|
||||
}
|
||||
|
||||
static class NoopSimpleCommand implements SimpleCommand {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
static class NoopRawCommand implements RawCommand {
|
||||
@Override
|
||||
public void execute(final Invocation invocation) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.permission.Tristate;
|
||||
|
||||
public class MockCommandSource implements CommandSource {
|
||||
|
||||
public static final CommandSource INSTANCE = new MockCommandSource();
|
||||
|
||||
@Override
|
||||
public Tristate evaluatePermission(final String permission) {
|
||||
return Tristate.UNDEFINED;
|
||||
}
|
||||
}
|
||||
@@ -1,353 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.event;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.velocitypowered.api.event.Continuation;
|
||||
import com.velocitypowered.api.event.Event;
|
||||
import com.velocitypowered.api.event.EventTask;
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.proxy.testutil.FakePluginManager;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInstance;
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class EventTest {
|
||||
|
||||
private final VelocityEventManager eventManager =
|
||||
new VelocityEventManager(new FakePluginManager());
|
||||
|
||||
@AfterAll
|
||||
void shutdown() throws Exception {
|
||||
eventManager.shutdown();
|
||||
}
|
||||
|
||||
static final class TestEvent implements Event {
|
||||
}
|
||||
|
||||
static void assertAsyncThread(final Thread thread) {
|
||||
assertTrue(thread.getName().contains("Velocity Async Event Executor"));
|
||||
}
|
||||
|
||||
static void assertSyncThread(final Thread thread) {
|
||||
assertEquals(Thread.currentThread(), thread);
|
||||
}
|
||||
|
||||
private void handleMethodListener(final Object listener) throws Exception {
|
||||
eventManager.register(FakePluginManager.PLUGIN_A, listener);
|
||||
try {
|
||||
eventManager.fire(new TestEvent()).get();
|
||||
} finally {
|
||||
eventManager.unregisterListeners(FakePluginManager.PLUGIN_A);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAlwaysSync() throws Exception {
|
||||
final AlwaysSyncListener listener = new AlwaysSyncListener();
|
||||
handleMethodListener(listener);
|
||||
assertSyncThread(listener.thread);
|
||||
assertEquals(1, listener.result);
|
||||
}
|
||||
|
||||
static final class AlwaysSyncListener {
|
||||
|
||||
@MonotonicNonNull Thread thread;
|
||||
int result;
|
||||
|
||||
@Subscribe
|
||||
void sync(TestEvent event) {
|
||||
result++;
|
||||
thread = Thread.currentThread();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAlwaysAsync() throws Exception {
|
||||
final AlwaysAsyncListener listener = new AlwaysAsyncListener();
|
||||
handleMethodListener(listener);
|
||||
assertAsyncThread(listener.threadA);
|
||||
assertAsyncThread(listener.threadB);
|
||||
assertAsyncThread(listener.threadC);
|
||||
assertEquals(3, listener.result);
|
||||
}
|
||||
|
||||
static final class AlwaysAsyncListener {
|
||||
|
||||
@MonotonicNonNull Thread threadA;
|
||||
@MonotonicNonNull Thread threadB;
|
||||
@MonotonicNonNull Thread threadC;
|
||||
int result;
|
||||
|
||||
@Subscribe(async = true)
|
||||
void firstAsync(TestEvent event) {
|
||||
result++;
|
||||
threadA = Thread.currentThread();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
EventTask secondAsync(TestEvent event) {
|
||||
threadB = Thread.currentThread();
|
||||
return EventTask.async(() -> result++);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
void thirdAsync(TestEvent event) {
|
||||
result++;
|
||||
threadC = Thread.currentThread();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSometimesAsync() throws Exception {
|
||||
final SometimesAsyncListener listener = new SometimesAsyncListener();
|
||||
handleMethodListener(listener);
|
||||
assertSyncThread(listener.threadA);
|
||||
assertSyncThread(listener.threadB);
|
||||
assertAsyncThread(listener.threadC);
|
||||
assertAsyncThread(listener.threadD);
|
||||
assertEquals(3, listener.result);
|
||||
}
|
||||
|
||||
static final class SometimesAsyncListener {
|
||||
|
||||
@MonotonicNonNull Thread threadA;
|
||||
@MonotonicNonNull Thread threadB;
|
||||
@MonotonicNonNull Thread threadC;
|
||||
@MonotonicNonNull Thread threadD;
|
||||
int result;
|
||||
|
||||
@Subscribe(order = PostOrder.EARLY)
|
||||
void notAsync(TestEvent event) {
|
||||
result++;
|
||||
threadA = Thread.currentThread();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
EventTask notAsyncUntilTask(TestEvent event) {
|
||||
threadB = Thread.currentThread();
|
||||
return EventTask.async(() -> {
|
||||
threadC = Thread.currentThread();
|
||||
result++;
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LATE)
|
||||
void stillAsyncAfterTask(TestEvent event) {
|
||||
threadD = Thread.currentThread();
|
||||
result++;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContinuation() throws Exception {
|
||||
final ContinuationListener listener = new ContinuationListener();
|
||||
handleMethodListener(listener);
|
||||
assertSyncThread(listener.threadA);
|
||||
assertSyncThread(listener.threadB);
|
||||
assertAsyncThread(listener.threadC);
|
||||
assertEquals(2, listener.value.get());
|
||||
}
|
||||
|
||||
static final class ContinuationListener {
|
||||
|
||||
@MonotonicNonNull Thread threadA;
|
||||
@MonotonicNonNull Thread threadB;
|
||||
@MonotonicNonNull Thread threadC;
|
||||
|
||||
final AtomicInteger value = new AtomicInteger();
|
||||
|
||||
@Subscribe(order = PostOrder.EARLY)
|
||||
EventTask continuation(TestEvent event) {
|
||||
threadA = Thread.currentThread();
|
||||
return EventTask.withContinuation(continuation -> {
|
||||
value.incrementAndGet();
|
||||
threadB = Thread.currentThread();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
value.incrementAndGet();
|
||||
continuation.resume();
|
||||
}).start();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LATE)
|
||||
void afterContinuation(TestEvent event) {
|
||||
threadC = Thread.currentThread();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testResumeContinuationImmediately() throws Exception {
|
||||
final ResumeContinuationImmediatelyListener listener = new ResumeContinuationImmediatelyListener();
|
||||
handleMethodListener(listener);
|
||||
assertSyncThread(listener.threadA);
|
||||
assertSyncThread(listener.threadB);
|
||||
assertSyncThread(listener.threadC);
|
||||
assertEquals(2, listener.result);
|
||||
}
|
||||
|
||||
static final class ResumeContinuationImmediatelyListener {
|
||||
|
||||
@MonotonicNonNull Thread threadA;
|
||||
@MonotonicNonNull Thread threadB;
|
||||
@MonotonicNonNull Thread threadC;
|
||||
int result;
|
||||
|
||||
@Subscribe(order = PostOrder.EARLY)
|
||||
EventTask continuation(TestEvent event) {
|
||||
threadA = Thread.currentThread();
|
||||
return EventTask.withContinuation(continuation -> {
|
||||
threadB = Thread.currentThread();
|
||||
result++;
|
||||
continuation.resume();
|
||||
});
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LATE)
|
||||
void afterContinuation(TestEvent event) {
|
||||
threadC = Thread.currentThread();
|
||||
result++;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContinuationParameter() throws Exception {
|
||||
final ContinuationParameterListener listener = new ContinuationParameterListener();
|
||||
handleMethodListener(listener);
|
||||
assertSyncThread(listener.threadA);
|
||||
assertSyncThread(listener.threadB);
|
||||
assertAsyncThread(listener.threadC);
|
||||
assertEquals(3, listener.result.get());
|
||||
}
|
||||
|
||||
static final class ContinuationParameterListener {
|
||||
|
||||
@MonotonicNonNull Thread threadA;
|
||||
@MonotonicNonNull Thread threadB;
|
||||
@MonotonicNonNull Thread threadC;
|
||||
|
||||
final AtomicInteger result = new AtomicInteger();
|
||||
|
||||
@Subscribe
|
||||
void resume(TestEvent event, Continuation continuation) {
|
||||
threadA = Thread.currentThread();
|
||||
result.incrementAndGet();
|
||||
continuation.resume();
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LATE)
|
||||
void resumeFromCustomThread(TestEvent event, Continuation continuation) {
|
||||
threadB = Thread.currentThread();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
result.incrementAndGet();
|
||||
continuation.resume();
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Subscribe(order = PostOrder.LAST)
|
||||
void afterCustomThread(TestEvent event, Continuation continuation) {
|
||||
threadC = Thread.currentThread();
|
||||
result.incrementAndGet();
|
||||
continuation.resume();
|
||||
}
|
||||
}
|
||||
|
||||
interface FancyContinuation {
|
||||
|
||||
void resume();
|
||||
|
||||
void resumeWithError(Exception exception);
|
||||
}
|
||||
|
||||
private static final class FancyContinuationImpl implements FancyContinuation {
|
||||
|
||||
private final Continuation continuation;
|
||||
|
||||
private FancyContinuationImpl(final Continuation continuation) {
|
||||
this.continuation = continuation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
continuation.resume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resumeWithError(final Exception exception) {
|
||||
continuation.resumeWithException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
interface TriConsumer<A, B, C> {
|
||||
|
||||
void accept(A a, B b, C c);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFancyContinuationParameter() throws Exception {
|
||||
eventManager.registerHandlerAdapter(
|
||||
"fancy",
|
||||
method -> method.getParameterCount() > 1
|
||||
&& method.getParameterTypes()[1] == FancyContinuation.class,
|
||||
(method, errors) -> {
|
||||
if (method.getReturnType() != void.class) {
|
||||
errors.add("method return type must be void");
|
||||
}
|
||||
if (method.getParameterCount() != 2) {
|
||||
errors.add("method must have exactly two parameters, the first is the event and "
|
||||
+ "the second is the fancy continuation");
|
||||
}
|
||||
},
|
||||
new TypeToken<TriConsumer<Object, Event, FancyContinuation>>() {},
|
||||
invokeFunction -> (instance, event) ->
|
||||
EventTask.withContinuation(continuation ->
|
||||
invokeFunction.accept(instance, event, new FancyContinuationImpl(continuation))
|
||||
));
|
||||
final FancyContinuationListener listener = new FancyContinuationListener();
|
||||
handleMethodListener(listener);
|
||||
assertEquals(1, listener.result);
|
||||
}
|
||||
|
||||
static final class FancyContinuationListener {
|
||||
|
||||
int result;
|
||||
|
||||
@Subscribe
|
||||
void continuation(TestEvent event, FancyContinuation continuation) {
|
||||
result++;
|
||||
continuation.resume();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.event;
|
||||
|
||||
import com.velocitypowered.proxy.plugin.MockPluginManager;
|
||||
|
||||
/**
|
||||
* A mock {@link VelocityEventManager}. Must be shutdown after use!
|
||||
*/
|
||||
public class MockEventManager extends VelocityEventManager {
|
||||
|
||||
public MockEventManager() {
|
||||
super(MockPluginManager.INSTANCE);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.PluginManager;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
|
||||
public class MockPluginManager implements PluginManager {
|
||||
|
||||
public static final PluginManager INSTANCE = new MockPluginManager();
|
||||
|
||||
@Override
|
||||
public @Nullable PluginContainer fromInstance(final Object instance) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable PluginContainer getPlugin(final String id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PluginContainer> plugins() {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoaded(final String id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addToClasspath(final Object plugin, final Path path) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.plugin.util;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.velocitypowered.api.plugin.PluginDescription;
|
||||
import com.velocitypowered.api.plugin.meta.PluginDependency;
|
||||
import com.velocitypowered.proxy.plugin.loader.VelocityPluginDescription;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PluginDependencyUtilsTest {
|
||||
|
||||
private static final PluginDescription NO_DEPENDENCY = testDescription("trivial");
|
||||
private static final PluginDescription NO_DEPENDENCY_2 = testDescription("trivial2");
|
||||
private static final PluginDescription HAS_DEPENDENCY_1 = testDescription("dependent1",
|
||||
new PluginDependency("trivial", "", false));
|
||||
private static final PluginDescription HAS_DEPENDENCY_2 = testDescription("dependent2",
|
||||
new PluginDependency("dependent1", "", false));
|
||||
private static final PluginDescription HAS_DEPENDENCY_3 = testDescription("dependent3",
|
||||
new PluginDependency("trivial", "", false));
|
||||
|
||||
private static final PluginDescription CIRCULAR_DEPENDENCY_1 = testDescription("circle",
|
||||
new PluginDependency("oval", "", false));
|
||||
private static final PluginDescription CIRCULAR_DEPENDENCY_2 = testDescription("oval",
|
||||
new PluginDependency("circle", "", false));
|
||||
|
||||
@Test
|
||||
void sortCandidatesTrivial() throws Exception {
|
||||
List<PluginDescription> descriptionList = new ArrayList<>();
|
||||
assertEquals(descriptionList, PluginDependencyUtils.sortCandidates(descriptionList));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesSingleton() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(NO_DEPENDENCY);
|
||||
assertEquals(plugins, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesBasicDependency() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(HAS_DEPENDENCY_1, NO_DEPENDENCY);
|
||||
List<PluginDescription> expected = ImmutableList.of(NO_DEPENDENCY, HAS_DEPENDENCY_1);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesNestedDependency() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(HAS_DEPENDENCY_1, HAS_DEPENDENCY_2,
|
||||
NO_DEPENDENCY);
|
||||
List<PluginDescription> expected = ImmutableList.of(NO_DEPENDENCY, HAS_DEPENDENCY_1,
|
||||
HAS_DEPENDENCY_2);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesTypical() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(HAS_DEPENDENCY_2, NO_DEPENDENCY_2,
|
||||
HAS_DEPENDENCY_1, NO_DEPENDENCY);
|
||||
List<PluginDescription> expected = ImmutableList.of(NO_DEPENDENCY, HAS_DEPENDENCY_1,
|
||||
HAS_DEPENDENCY_2, NO_DEPENDENCY_2);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesMultiplePluginsDependentOnOne() throws Exception {
|
||||
List<PluginDescription> plugins = ImmutableList.of(HAS_DEPENDENCY_3, HAS_DEPENDENCY_1,
|
||||
NO_DEPENDENCY);
|
||||
List<PluginDescription> expected = ImmutableList.of(NO_DEPENDENCY, HAS_DEPENDENCY_1,
|
||||
HAS_DEPENDENCY_3);
|
||||
assertEquals(expected, PluginDependencyUtils.sortCandidates(plugins));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sortCandidatesCircularDependency() throws Exception {
|
||||
List<PluginDescription> descs = ImmutableList.of(CIRCULAR_DEPENDENCY_1, CIRCULAR_DEPENDENCY_2);
|
||||
assertThrows(IllegalStateException.class, () -> PluginDependencyUtils.sortCandidates(descs));
|
||||
}
|
||||
|
||||
private static PluginDescription testDescription(String id, PluginDependency... dependencies) {
|
||||
return new VelocityPluginDescription(
|
||||
id, "tuxed", "0.1", null, null, ImmutableList.of(),
|
||||
ImmutableList.copyOf(dependencies), null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Velocity Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.velocitypowered.proxy.scheduler;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import com.velocitypowered.api.scheduler.ScheduledTask;
|
||||
import com.velocitypowered.api.scheduler.TaskStatus;
|
||||
import com.velocitypowered.proxy.testutil.FakePluginManager;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class VelocitySchedulerTest {
|
||||
// TODO: The timings here will be inaccurate on slow systems.
|
||||
|
||||
@Test
|
||||
void buildTask() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager());
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown)
|
||||
.schedule();
|
||||
latch.await();
|
||||
assertEquals(TaskStatus.FINISHED, task.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelWorks() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager());
|
||||
AtomicInteger i = new AtomicInteger(3);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, i::decrementAndGet)
|
||||
.delay(100, TimeUnit.SECONDS)
|
||||
.schedule();
|
||||
task.cancel();
|
||||
Thread.sleep(200);
|
||||
assertEquals(3, i.get());
|
||||
assertEquals(TaskStatus.CANCELLED, task.status());
|
||||
}
|
||||
|
||||
@Test
|
||||
void repeatTaskWorks() throws Exception {
|
||||
VelocityScheduler scheduler = new VelocityScheduler(new FakePluginManager());
|
||||
CountDownLatch latch = new CountDownLatch(3);
|
||||
ScheduledTask task = scheduler.buildTask(FakePluginManager.PLUGIN_A, latch::countDown)
|
||||
.delay(100, TimeUnit.MILLISECONDS)
|
||||
.repeat(100, TimeUnit.MILLISECONDS)
|
||||
.schedule();
|
||||
latch.await();
|
||||
task.cancel();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user