Fix some concurrency issues with login handling
This commit is contained in:
@@ -22,6 +22,8 @@
|
||||
|
||||
package me.lucko.luckperms.bungee;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import me.lucko.luckperms.api.Contexts;
|
||||
import me.lucko.luckperms.api.caching.UserData;
|
||||
import me.lucko.luckperms.api.context.MutableContextSet;
|
||||
@@ -30,7 +32,6 @@ import me.lucko.luckperms.common.constants.Message;
|
||||
import me.lucko.luckperms.common.core.UuidCache;
|
||||
import me.lucko.luckperms.common.core.model.User;
|
||||
import me.lucko.luckperms.common.defaults.Rule;
|
||||
import me.lucko.luckperms.common.utils.AbstractListener;
|
||||
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import net.md_5.bungee.api.connection.PendingConnection;
|
||||
@@ -44,62 +45,61 @@ import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class BungeeListener extends AbstractListener implements Listener {
|
||||
private static final TextComponent WARN_MESSAGE = new TextComponent(Message.LOADING_ERROR.toString());
|
||||
@RequiredArgsConstructor
|
||||
public class BungeeListener implements Listener {
|
||||
private final LPBungeePlugin plugin;
|
||||
|
||||
BungeeListener(LPBungeePlugin plugin) {
|
||||
super(plugin);
|
||||
this.plugin = plugin;
|
||||
}
|
||||
private final Set<UUID> deniedLogin = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onPlayerPermissionCheck(PermissionCheckEvent e) {
|
||||
if (!(e.getSender() instanceof ProxiedPlayer)) {
|
||||
e.setHasPermission(true);
|
||||
return;
|
||||
}
|
||||
|
||||
final ProxiedPlayer player = ((ProxiedPlayer) e.getSender());
|
||||
|
||||
User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId()));
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserData userData = user.getUserData();
|
||||
if (userData == null) {
|
||||
plugin.getLog().warn("Player " + player.getName() + " does not have any user data setup.");
|
||||
return;
|
||||
}
|
||||
|
||||
Contexts contexts = new Contexts(
|
||||
plugin.getContextManager().getApplicableContext(player),
|
||||
plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_PERMS),
|
||||
plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_WORLD_PERMS),
|
||||
true,
|
||||
plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_GROUPS),
|
||||
plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_WORLD_GROUPS),
|
||||
false
|
||||
);
|
||||
|
||||
e.setHasPermission(userData.getPermissionData(contexts).getPermissionValue(e.getPermission()).asBoolean());
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
@EventHandler(priority = EventPriority.LOW)
|
||||
public void onPlayerLogin(LoginEvent e) {
|
||||
/* Delay the login here, as we want to cache UUID data before the player is connected to a backend bukkit server.
|
||||
/* Called when the player first attempts a connection with the server.
|
||||
Listening on LOW priority to allow plugins to modify username / UUID data here. (auth plugins)
|
||||
|
||||
Delay the login here, as we want to cache UUID data before the player is connected to a backend bukkit server.
|
||||
This means that a player will have the same UUID across the network, even if parts of the network are running in
|
||||
Offline mode. */
|
||||
|
||||
// registers the plugins intent to modify this events state going forward.
|
||||
// this will prevent the event from completing until we're finished handling.
|
||||
e.registerIntent(plugin);
|
||||
|
||||
final long startTime = System.currentTimeMillis();
|
||||
final PendingConnection c = e.getConnection();
|
||||
|
||||
/* either the plugin hasn't finished starting yet, or there was an issue connecting to the DB, performing file i/o, etc.
|
||||
as this is bungeecord, we will still allow the login, as players can't really do much harm without permissions data.
|
||||
the proxy will just fallback to using the config file perms. */
|
||||
if (!plugin.getStorage().isAcceptingLogins()) {
|
||||
// log that the user tried to login, but was denied at this stage.
|
||||
deniedLogin.add(c.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
/* another plugin (or the proxy itself) has cancelled this connection already */
|
||||
if (e.isCancelled()) {
|
||||
plugin.getLog().warn("Connection from " + c.getUniqueId() + " was already denied. No permissions data will be loaded.");
|
||||
deniedLogin.add(c.getUniqueId());
|
||||
return;
|
||||
}
|
||||
|
||||
/* Actually process the login for the connection.
|
||||
We do this here to delay the login until the data is ready.
|
||||
If the login gets cancelled later on, then this will be cleaned up.
|
||||
|
||||
This includes:
|
||||
- loading uuid data
|
||||
- loading permissions
|
||||
- creating a user instance in the UserManager for this connection.
|
||||
- setting up cached data. */
|
||||
plugin.doAsync(() -> {
|
||||
final long startTime = System.currentTimeMillis();
|
||||
final UuidCache cache = plugin.getUuidCache();
|
||||
final PendingConnection c = e.getConnection();
|
||||
|
||||
if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUIDS)) {
|
||||
UUID uuid = plugin.getStorage().getUUID(c.getName()).join();
|
||||
@@ -109,6 +109,8 @@ public class BungeeListener extends AbstractListener implements Listener {
|
||||
// No previous data for this player
|
||||
plugin.getApiProvider().getEventFactory().handleUserFirstLogin(c.getUniqueId(), c.getName());
|
||||
cache.addToCache(c.getUniqueId(), c.getUniqueId());
|
||||
|
||||
// Join this call, as we want this to be set for when the player connects to the backend.
|
||||
plugin.getStorage().force().saveUUIDData(c.getName(), c.getUniqueId()).join();
|
||||
}
|
||||
} else {
|
||||
@@ -118,12 +120,14 @@ public class BungeeListener extends AbstractListener implements Listener {
|
||||
}
|
||||
|
||||
// Online mode, no cache needed. This is just for name -> uuid lookup.
|
||||
// Again, join this call so the data is available for the backend.
|
||||
plugin.getStorage().force().saveUUIDData(c.getName(), c.getUniqueId()).join();
|
||||
}
|
||||
|
||||
// We have to make a new user on this thread whilst the connection is being held, or we get concurrency issues as the Bukkit server
|
||||
// and the BungeeCord server try to make a new user at the same time.
|
||||
/* We have to make a new user on this thread whilst the connection is being held, or we get concurrency issues
|
||||
as the Bukkit server and the BungeeCord server try to make a new user at the same time. */
|
||||
plugin.getStorage().force().loadUser(cache.getUUID(c.getUniqueId()), c.getName()).join();
|
||||
|
||||
User user = plugin.getUserManager().get(cache.getUUID(c.getUniqueId()));
|
||||
if (user == null) {
|
||||
plugin.getLog().warn("Failed to load user: " + c.getName());
|
||||
@@ -148,7 +152,13 @@ public class BungeeListener extends AbstractListener implements Listener {
|
||||
if (time >= 1000) {
|
||||
plugin.getLog().warn("Processing login for " + c.getName() + " took " + time + "ms.");
|
||||
}
|
||||
|
||||
// finally, complete out intent to modify state, so the proxy can continue handling the connection.
|
||||
e.completeIntent(plugin);
|
||||
|
||||
// schedule a cleanup of the users data in a few seconds.
|
||||
// this should cover the eventuality that the login fails.
|
||||
plugin.getUserManager().scheduleUnload(c.getUniqueId());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -158,17 +168,57 @@ public class BungeeListener extends AbstractListener implements Listener {
|
||||
final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(e.getPlayer().getUniqueId()));
|
||||
|
||||
if (user == null) {
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> player.sendMessage(WARN_MESSAGE), 3, TimeUnit.SECONDS);
|
||||
plugin.getProxy().getScheduler().schedule(plugin, () -> {
|
||||
if (!player.isConnected()) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.sendMessage(new TextComponent(Message.LOADING_ERROR.asString(plugin.getLocaleManager())));
|
||||
}, 3, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait until the last priority to unload, so plugins can still perform permission checks on this event
|
||||
@EventHandler(priority = EventPriority.HIGHEST)
|
||||
public void onPlayerQuit(PlayerDisconnectEvent e) {
|
||||
onLeave(e.getPlayer().getUniqueId());
|
||||
// Request that the users data is unloaded.
|
||||
plugin.getUserManager().scheduleUnload(e.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
// We don't preprocess all servers, so we may have to do it here.
|
||||
@EventHandler(priority = EventPriority.HIGH)
|
||||
public void onPlayerPermissionCheck(PermissionCheckEvent e) {
|
||||
if (!(e.getSender() instanceof ProxiedPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ProxiedPlayer player = ((ProxiedPlayer) e.getSender());
|
||||
|
||||
User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId()));
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserData userData = user.getUserData();
|
||||
if (userData == null) {
|
||||
plugin.getLog().warn("Player " + player.getName() + " does not have any user data setup.");
|
||||
plugin.doAsync(() -> user.setupData(false));
|
||||
return;
|
||||
}
|
||||
|
||||
Contexts contexts = new Contexts(
|
||||
plugin.getContextManager().getApplicableContext(player),
|
||||
plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_PERMS),
|
||||
plugin.getConfiguration().get(ConfigKeys.INCLUDING_GLOBAL_WORLD_PERMS),
|
||||
true,
|
||||
plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_GROUPS),
|
||||
plugin.getConfiguration().get(ConfigKeys.APPLYING_GLOBAL_WORLD_GROUPS),
|
||||
false
|
||||
);
|
||||
|
||||
e.setHasPermission(userData.getPermissionData(contexts).getPermissionValue(e.getPermission()).asBoolean());
|
||||
}
|
||||
|
||||
// We don't pre-process all servers, so we have to do it here.
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onServerSwitch(ServerConnectEvent e) {
|
||||
String serverName = e.getTarget().getName();
|
||||
|
||||
@@ -323,7 +323,8 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin {
|
||||
|
||||
@Override
|
||||
public boolean isPlayerOnline(UUID external) {
|
||||
return getProxy().getPlayer(external) != null;
|
||||
ProxiedPlayer player = getProxy().getPlayer(external);
|
||||
return player != null && player.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user