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:
Andrew Steinborn
2021-05-30 23:15:38 -04:00
parent 4a8be52c93
commit 6a6ca7a03e
39 changed files with 203 additions and 35 deletions

41
proxy-core/build.gradle Normal file
View File

@@ -0,0 +1,41 @@
plugins {
id 'java-library'
id 'maven-publish'
id 'checkstyle'
}
apply plugin: 'org.cadixdev.licenser'
apply from: '../gradle/checkstyle.gradle'
apply from: '../gradle/publish.gradle'
apply from: '../gradle/errorprone.gradle'
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
license {
header = project.rootProject.file('HEADER.txt')
}
dependencies {
implementation project(':velocity-api')
implementation project(':velocity-annotation-processor')
implementation "net.kyori:adventure-nbt:${adventureVersion}"
implementation "org.apache.logging.log4j:log4j-api:${log4jVersion}"
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}"
}
test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,48 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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.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();
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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);
}

View File

@@ -0,0 +1,93 @@
/*
* 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;
@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);
}
}

View File

@@ -0,0 +1,221 @@
/*
* 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 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;
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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;
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;
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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;
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;
}
}

View File

@@ -0,0 +1,78 @@
/*
* 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);
}
};
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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));
}
}
}

View File

@@ -0,0 +1,656 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,246 @@
/*
* 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.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 ProxyServer server;
public VelocityPluginManager(ProxyServer 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.");
}
}
}

View File

@@ -0,0 +1,63 @@
/*
* 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;
}

View File

@@ -0,0 +1,51 @@
/*
* 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");
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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
+ '}';
}
}

View File

@@ -0,0 +1,232 @@
/*
* 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.gson.Gson;
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.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 static final Gson PLUGIN_FILE_DESERIALIZER = new Gson();
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 PLUGIN_FILE_DESERIALIZER.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()
);
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,44 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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);
}
}

View File

@@ -0,0 +1,119 @@
/*
* 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
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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.api.proxy.ProxyServer;
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 = ProxyServer.class.getPackage();
return MoreObjects.firstNonNull(pkg.getImplementationTitle(), "Velocity");
}
@Override
public String version() {
final Package pkg = ProxyServer.class.getPackage();
return MoreObjects.firstNonNull(pkg.getImplementationVersion(), "<unknown>");
}
@Override
public List<String> authors() {
final Package pkg = ProxyServer.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;
}
}

View File

@@ -0,0 +1,232 @@
/*
* 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);
}
}

View File

@@ -0,0 +1,123 @@
/*
* 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()));
}
}

View File

@@ -0,0 +1,506 @@
/*
* 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) {
}
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,353 @@
/*
* 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();
}
}
}

View File

@@ -0,0 +1,30 @@
/*
* 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);
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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) {
}
}

View File

@@ -0,0 +1,106 @@
/*
* 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 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 com.velocitypowered.proxy.plugin.util.PluginDependencyUtils;
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
);
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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();
}
}

View File

@@ -0,0 +1,95 @@
/*
* 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.testutil;
import com.google.common.collect.ImmutableList;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginDescription;
import com.velocitypowered.api.plugin.PluginManager;
import java.nio.file.Path;
import java.util.Collection;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
public class FakePluginManager implements PluginManager {
public static final Object PLUGIN_A = new Object();
public static final Object PLUGIN_B = new Object();
private static final PluginContainer PC_A = new FakePluginContainer("a", PLUGIN_A);
private static final PluginContainer PC_B = new FakePluginContainer("b", PLUGIN_B);
@Override
public @Nullable PluginContainer fromInstance(@NonNull Object instance) {
if (instance == PLUGIN_A) {
return PC_A;
} else if (instance == PLUGIN_B) {
return PC_B;
} else {
return null;
}
}
@Override
public @Nullable PluginContainer getPlugin(@NonNull String id) {
switch (id) {
case "a":
return PC_A;
case "b":
return PC_B;
default:
return null;
}
}
@Override
public @NonNull Collection<PluginContainer> plugins() {
return ImmutableList.of(PC_A, PC_B);
}
@Override
public boolean isLoaded(@NonNull String id) {
return id.equals("a") || id.equals("b");
}
@Override
public void addToClasspath(@NonNull Object plugin, @NonNull Path path) {
throw new UnsupportedOperationException();
}
private static class FakePluginContainer implements PluginContainer {
private final String id;
private final Object instance;
private FakePluginContainer(String id, Object instance) {
this.id = id;
this.instance = instance;
}
@Override
public @NonNull PluginDescription description() {
return () -> id;
}
@Override
public Object instance() {
return instance;
}
}
}