Fix some concurrency issues with login handling

This commit is contained in:
Luck 2017-03-26 18:40:09 +01:00
parent 486b19aa90
commit 8e557d122b
No known key found for this signature in database
GPG Key ID: EFA9B3EC5FD90F8B
14 changed files with 375 additions and 165 deletions

View File

@ -9,7 +9,7 @@ log-error: "&7&l[&bLuck&3Perms&7&l] &4[ERROR] {0}"
empty: "{0}" empty: "{0}"
player-online: "&aOnline" player-online: "&aOnline"
player-offline: "&cOffline" player-offline: "&cOffline"
loading-error: "Permissions data could not be loaded. Please contact an administrator." loading-error: "Permissions data could not be loaded. Please try again later."
op-disabled: "&bThe vanilla OP system is disabled on this server." op-disabled: "&bThe vanilla OP system is disabled on this server."
op-disabled-sponge: "&2Server Operator status has no effect when a permission plugin is installed. Please edit user data directly." op-disabled-sponge: "&2Server Operator status has no effect when a permission plugin is installed. Please edit user data directly."
log: "&3LOG &3&l> {0}" log: "&3LOG &3&l> {0}"

View File

@ -22,12 +22,15 @@
package me.lucko.luckperms.bukkit; package me.lucko.luckperms.bukkit;
import lombok.RequiredArgsConstructor;
import me.lucko.luckperms.bukkit.model.Injector; import me.lucko.luckperms.bukkit.model.Injector;
import me.lucko.luckperms.bukkit.model.LPPermissible; import me.lucko.luckperms.bukkit.model.LPPermissible;
import me.lucko.luckperms.common.caching.UserCache;
import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.constants.Message; import me.lucko.luckperms.common.constants.Message;
import me.lucko.luckperms.common.core.model.User; import me.lucko.luckperms.common.core.model.User;
import me.lucko.luckperms.common.utils.AbstractListener; import me.lucko.luckperms.common.utils.LoginHelper;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
@ -39,62 +42,78 @@ import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerLoginEvent; import org.bukkit.event.player.PlayerLoginEvent;
import org.bukkit.event.player.PlayerQuitEvent; import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.server.PluginEnableEvent; import org.bukkit.event.server.PluginEnableEvent;
import org.bukkit.scheduler.BukkitTask;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
class BukkitListener extends AbstractListener implements Listener { @RequiredArgsConstructor
public class BukkitListener implements Listener {
private final LPBukkitPlugin plugin; private final LPBukkitPlugin plugin;
private final Set<UUID> deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>()); private final Set<UUID> deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>());
private final Set<UUID> deniedLogin = new HashSet<>(); private final Set<UUID> deniedLogin = Collections.synchronizedSet(new HashSet<>());
private final Map<UUID, BukkitTask> cleanupTasks = Collections.synchronizedMap(new HashMap<>());
BukkitListener(LPBukkitPlugin plugin) {
super(plugin);
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.LOW) @EventHandler(priority = EventPriority.LOW)
public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) { public void onPlayerPreLogin(AsyncPlayerPreLoginEvent e) {
/* 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) */
/* the player was denied entry to the server before this priority.
log this, so we can handle appropriately later. */
if (e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { if (e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
plugin.getLog().warn("Connection from " + e.getUniqueId() + " was already denied. No permissions data will be loaded.");
deniedAsyncLogin.add(e.getUniqueId()); deniedAsyncLogin.add(e.getUniqueId());
return; return;
} }
/* either the plugin hasn't finished starting yet, or there was an issue connecting to the DB, performing file i/o, etc.
we don't let players join in this case, because it means they can connect to the server without their permissions data.
some server admins rely on negating perms to stop users from causing damage etc, so it's really important that
this data is loaded. */
if (!plugin.isStarted() || !plugin.getStorage().isAcceptingLogins()) { if (!plugin.isStarted() || !plugin.getStorage().isAcceptingLogins()) {
// log that the user tried to login, but was denied at this stage.
deniedAsyncLogin.add(e.getUniqueId()); deniedAsyncLogin.add(e.getUniqueId());
// The datastore is disabled, prevent players from joining the server // actually deny the connection.
plugin.getLog().warn("The plugin storage is not loaded. Denying connection from: " + e.getUniqueId() + " - " + e.getName()); plugin.getLog().warn("Permissions storage is not loaded yet. Denying connection from: " + e.getUniqueId() + " - " + e.getName());
e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.toString()); e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.asString(plugin.getLocaleManager()));
return; return;
} }
// remove any pending cleanup tasks /* Actually process the login for the connection.
BukkitTask task = cleanupTasks.remove(e.getUniqueId()); We do this here to delay the login until the data is ready.
if (task != null) { If the login gets cancelled later on, then this will be cleaned up.
task.cancel();
}
// Process login This includes:
onAsyncLogin(e.getUniqueId(), e.getName()); - loading uuid data
- loading permissions
- creating a user instance in the UserManager for this connection.
- setting up cached data. */
try {
LoginHelper.loadUser(plugin, e.getUniqueId(), e.getName());
} catch (Exception ex) {
ex.printStackTrace();
// there was some error loading data. deny the connection
deniedAsyncLogin.add(e.getUniqueId());
e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.asString(plugin.getLocaleManager()));
}
} }
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerPreLoginMonitor(AsyncPlayerPreLoginEvent e) { public void onPlayerPreLoginMonitor(AsyncPlayerPreLoginEvent e) {
// If they were denied before/at LOW, then don't bother handling here. /* Listen to see if the event was cancelled after we initially handled the connection
If the connection was cancelled here, we need to do something to clean up the data that was loaded. */
// Check to see if this connection was denied at LOW.
if (deniedAsyncLogin.remove(e.getUniqueId())) { if (deniedAsyncLogin.remove(e.getUniqueId())) {
// this is a problem, as they were denied at low priority, but are now being allowed. // This is a problem, as they were denied at low priority, but are now being allowed.
if (e.getLoginResult() == AsyncPlayerPreLoginEvent.Result.ALLOWED) { if (e.getLoginResult() == AsyncPlayerPreLoginEvent.Result.ALLOWED) {
new IllegalStateException("Player connection was re-allowed for " + e.getUniqueId()).printStackTrace(); plugin.getLog().severe("Player connection was re-allowed for " + e.getUniqueId());
e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, ""); e.disallow(AsyncPlayerPreLoginEvent.Result.KICK_OTHER, "");
} }
@ -102,14 +121,22 @@ class BukkitListener extends AbstractListener implements Listener {
} }
// Login event was cancelled by another plugin // Login event was cancelled by another plugin
if (plugin.isStarted() && plugin.getStorage().isAcceptingLogins() && e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) { if (e.getLoginResult() != AsyncPlayerPreLoginEvent.Result.ALLOWED) {
cleanupUser(e.getUniqueId()); // Schedule cleanup of this user.
plugin.getUserManager().scheduleUnload(e.getUniqueId());
} }
} }
@EventHandler(priority = EventPriority.LOW) @EventHandler(priority = EventPriority.LOW)
public void onPlayerLogin(PlayerLoginEvent e) { public void onPlayerLogin(PlayerLoginEvent e) {
/* Called when the player starts logging into the server.
At this point, the users data should be present and loaded.
Listening on LOW priority to allow plugins to further modify data here. (auth plugins, etc.) */
/* the player was denied entry to the server before this priority.
log this, so we can handle appropriately later. */
if (e.getResult() != PlayerLoginEvent.Result.ALLOWED) { if (e.getResult() != PlayerLoginEvent.Result.ALLOWED) {
plugin.getLog().warn("Login from " + e.getPlayer().getUniqueId() + " was denied before an attachment could be injected.");
deniedLogin.add(e.getPlayer().getUniqueId()); deniedLogin.add(e.getPlayer().getUniqueId());
return; return;
} }
@ -117,21 +144,17 @@ class BukkitListener extends AbstractListener implements Listener {
final Player player = e.getPlayer(); final Player player = e.getPlayer();
final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId())); final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId()));
/* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */
if (user == null) { if (user == null) {
deniedLogin.add(e.getPlayer().getUniqueId()); deniedLogin.add(e.getPlayer().getUniqueId());
// User wasn't loaded for whatever reason. plugin.getLog().warn("User " + player.getUniqueId() + " - " + player.getName() + " doesn't have data pre-loaded. - denying login.");
plugin.getLog().warn("User " + player.getUniqueId() + " - " + player.getName() + " could not be loaded. - denying login."); e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.asString(plugin.getLocaleManager()));
e.disallow(PlayerLoginEvent.Result.KICK_OTHER, Message.LOADING_ERROR.toString());
return; return;
} }
// remove any pending cleanup tasks // User instance is there, now we can inject our custom Permissible into the player.
BukkitTask task = cleanupTasks.remove(e.getPlayer().getUniqueId()); // Care should be taken at this stage to ensure that async tasks which manipulate bukkit data check that the player is still online.
if (task != null) {
task.cancel();
}
try { try {
// Make a new permissible for the user // Make a new permissible for the user
LPPermissible lpPermissible = new LPPermissible(player, user, plugin); LPPermissible lpPermissible = new LPPermissible(player, user, plugin);
@ -147,30 +170,43 @@ class BukkitListener extends AbstractListener implements Listener {
// We assume all users are not op, but those who are need extra calculation. // We assume all users are not op, but those who are need extra calculation.
if (player.isOp()) { if (player.isOp()) {
plugin.doAsync(() -> user.getUserData().preCalculate(plugin.getPreProcessContexts(true))); plugin.doAsync(() -> {
UserCache userData = user.getUserData();
if (userData == null) {
return;
}
userData.preCalculate(plugin.getPreProcessContexts(true));
});
} }
} }
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.MONITOR)
public void onPlayerLoginMonitor(PlayerLoginEvent e) { public void onPlayerLoginMonitor(PlayerLoginEvent e) {
// If they were denied before/at LOW, then don't bother handling here. /* Listen to see if the event was cancelled after we initially handled the login
If the connection was cancelled here, we need to do something to clean up the data that was loaded. */
// Check to see if this connection was denied at LOW.
if (deniedLogin.remove(e.getPlayer().getUniqueId())) { if (deniedLogin.remove(e.getPlayer().getUniqueId())) {
// this is a problem, as they were denied at low priority, but are now being allowed. // This is a problem, as they were denied at low priority, but are now being allowed.
if (e.getResult() == PlayerLoginEvent.Result.ALLOWED) { if (e.getResult() == PlayerLoginEvent.Result.ALLOWED) {
new IllegalStateException("Player connection was re-allowed for " + e.getPlayer().getUniqueId()).printStackTrace(); plugin.getLog().severe("Player connection was re-allowed for " + e.getPlayer().getUniqueId());
e.disallow(PlayerLoginEvent.Result.KICK_OTHER, ""); e.disallow(PlayerLoginEvent.Result.KICK_OTHER, "");
} }
return; return;
} }
// Login event was cancelled by another plugin
if (e.getResult() != PlayerLoginEvent.Result.ALLOWED) { if (e.getResult() != PlayerLoginEvent.Result.ALLOWED) {
// The player got denied on sync login. // Schedule cleanup of this user.
cleanupUser(e.getPlayer().getUniqueId()); plugin.getUserManager().scheduleUnload(e.getPlayer().getUniqueId());
} else { return;
plugin.refreshAutoOp(e.getPlayer());
} }
// everything is going well. login was processed ok, this is just to refresh auto-op status.
plugin.refreshAutoOp(e.getPlayer());
} }
// Wait until the last priority to unload, so plugins can still perform permission checks on this event // Wait until the last priority to unload, so plugins can still perform permission checks on this event
@ -186,21 +222,8 @@ class BukkitListener extends AbstractListener implements Listener {
player.setOp(false); player.setOp(false);
} }
// Call internal leave handling // Request that the users data is unloaded.
onLeave(player.getUniqueId()); plugin.getUserManager().scheduleUnload(player.getUniqueId());
}
private void cleanupUser(UUID uuid) {
if (cleanupTasks.containsKey(uuid)) {
return;
}
BukkitTask task = plugin.getServer().getScheduler().runTaskLater(plugin, () -> {
onLeave(uuid);
cleanupTasks.remove(uuid);
}, 60L);
cleanupTasks.put(uuid, task);
} }
@EventHandler @EventHandler
@ -209,7 +232,7 @@ class BukkitListener extends AbstractListener implements Listener {
return; return;
} }
String s = e.getMessage() String s = e.getMessage().toLowerCase()
.replace("/", "") .replace("/", "")
.replace("bukkit:", "") .replace("bukkit:", "")
.replace("spigot:", "") .replace("spigot:", "")
@ -217,7 +240,7 @@ class BukkitListener extends AbstractListener implements Listener {
if (s.equals("op") || s.startsWith("op ") || s.equals("deop") || s.startsWith("deop ")) { if (s.equals("op") || s.startsWith("op ") || s.equals("deop") || s.startsWith("deop ")) {
e.setCancelled(true); e.setCancelled(true);
e.getPlayer().sendMessage(Message.OP_DISABLED.toString()); e.getPlayer().sendMessage(Message.OP_DISABLED.asString(plugin.getLocaleManager()));
} }
} }

View File

@ -39,6 +39,7 @@ import me.lucko.luckperms.bukkit.model.LPPermissible;
import me.lucko.luckperms.bukkit.vault.VaultHook; import me.lucko.luckperms.bukkit.vault.VaultHook;
import me.lucko.luckperms.common.api.ApiHandler; import me.lucko.luckperms.common.api.ApiHandler;
import me.lucko.luckperms.common.api.ApiProvider; import me.lucko.luckperms.common.api.ApiProvider;
import me.lucko.luckperms.common.caching.UserCache;
import me.lucko.luckperms.common.caching.handlers.CachedStateManager; import me.lucko.luckperms.common.caching.handlers.CachedStateManager;
import me.lucko.luckperms.common.calculators.CalculatorFactory; import me.lucko.luckperms.common.calculators.CalculatorFactory;
import me.lucko.luckperms.common.commands.sender.Sender; import me.lucko.luckperms.common.commands.sender.Sender;
@ -73,6 +74,7 @@ import me.lucko.luckperms.common.treeview.PermissionVault;
import me.lucko.luckperms.common.utils.BufferedRequest; import me.lucko.luckperms.common.utils.BufferedRequest;
import me.lucko.luckperms.common.utils.FileWatcher; import me.lucko.luckperms.common.utils.FileWatcher;
import me.lucko.luckperms.common.utils.LoggerImpl; import me.lucko.luckperms.common.utils.LoggerImpl;
import me.lucko.luckperms.common.utils.LoginHelper;
import me.lucko.luckperms.common.verbose.VerboseHandler; import me.lucko.luckperms.common.verbose.VerboseHandler;
import org.bukkit.World; import org.bukkit.World;
@ -302,7 +304,7 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
// Load any online users (in the case of a reload) // Load any online users (in the case of a reload)
for (Player player : getServer().getOnlinePlayers()) { for (Player player : getServer().getOnlinePlayers()) {
scheduler.doAsync(() -> { scheduler.doAsync(() -> {
listener.onAsyncLogin(player.getUniqueId(), player.getName()); LoginHelper.loadUser(this, player.getUniqueId(), player.getName());
User user = getUserManager().get(getUuidCache().getUUID(player.getUniqueId())); User user = getUserManager().get(getUuidCache().getUUID(player.getUniqueId()));
if (user != null) { if (user != null) {
scheduler.doSync(() -> { scheduler.doSync(() -> {
@ -431,11 +433,21 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
if (getConfiguration().get(ConfigKeys.AUTO_OP)) { if (getConfiguration().get(ConfigKeys.AUTO_OP)) {
try { try {
LPPermissible permissible = Injector.getPermissible(player.getUniqueId()); LPPermissible permissible = Injector.getPermissible(player.getUniqueId());
if (permissible == null) { if (permissible == null || !permissible.getActive().get()) {
return; return;
} }
Map<String, Boolean> backing = permissible.getUser().getUserData().getPermissionData(permissible.calculateContexts()).getImmutableBacking(); User user = permissible.getUser();
if (user == null) {
return;
}
UserCache userData = user.getUserData();
if (userData == null) {
return;
}
Map<String, Boolean> backing = userData.getPermissionData(permissible.calculateContexts()).getImmutableBacking();
boolean op = Optional.ofNullable(backing.get("luckperms.autoop")).orElse(false); boolean op = Optional.ofNullable(backing.get("luckperms.autoop")).orElse(false);
player.setOp(op); player.setOp(op);
} catch (Exception ignored) {} } catch (Exception ignored) {}
@ -511,7 +523,8 @@ public class LPBukkitPlugin extends JavaPlugin implements LuckPermsPlugin {
@Override @Override
public boolean isPlayerOnline(UUID external) { public boolean isPlayerOnline(UUID external) {
return getServer().getPlayer(external) != null; Player player = getServer().getPlayer(external);
return player != null && player.isOnline();
} }
@Override @Override

View File

@ -64,7 +64,7 @@ public class Injector {
PermissibleBase existing = (PermissibleBase) HUMAN_ENTITY_FIELD.get(player); PermissibleBase existing = (PermissibleBase) HUMAN_ENTITY_FIELD.get(player);
if (existing instanceof LPPermissible) { if (existing instanceof LPPermissible) {
// uh oh // uh oh
throw new IllegalStateException(); throw new IllegalStateException("LPPermissible already injected into player " + player.toString());
} }
// Move attachments over from the old permissible. // Move attachments over from the old permissible.
@ -73,6 +73,7 @@ public class Injector {
attachments.clear(); attachments.clear();
existing.clearPermissions(); existing.clearPermissions();
lpPermissible.getActive().set(true);
lpPermissible.recalculatePermissions(); lpPermissible.recalculatePermissions();
lpPermissible.setOldPermissible(existing); lpPermissible.setOldPermissible(existing);
@ -98,6 +99,8 @@ public class Injector {
((LPPermissible) permissible).unsubscribeFromAllAsync(); ((LPPermissible) permissible).unsubscribeFromAllAsync();
} }
((LPPermissible) permissible).getActive().set(false);
if (dummy) { if (dummy) {
HUMAN_ENTITY_FIELD.set(player, new DummyPermissibleBase()); HUMAN_ENTITY_FIELD.set(player, new DummyPermissibleBase());
} else { } else {

View File

@ -49,6 +49,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -66,6 +67,8 @@ public class LPPermissible extends PermissibleBase {
@Setter @Setter
private PermissibleBase oldPermissible = null; private PermissibleBase oldPermissible = null;
private final AtomicBoolean active = new AtomicBoolean(false);
// Attachment stuff. // Attachment stuff.
private final Map<String, PermissionAttachmentInfo> attachmentPermissions = new ConcurrentHashMap<>(); private final Map<String, PermissionAttachmentInfo> attachmentPermissions = new ConcurrentHashMap<>();
private final List<PermissionAttachment> attachments = Collections.synchronizedList(new LinkedList<>()); private final List<PermissionAttachment> attachments = Collections.synchronizedList(new LinkedList<>());
@ -81,10 +84,18 @@ public class LPPermissible extends PermissibleBase {
} }
public void updateSubscriptionsAsync() { public void updateSubscriptionsAsync() {
if (!active.get()) {
return;
}
plugin.doAsync(this::updateSubscriptions); plugin.doAsync(this::updateSubscriptions);
} }
public void updateSubscriptions() { public void updateSubscriptions() {
if (!active.get()) {
return;
}
UserCache cache = user.getUserData(); UserCache cache = user.getUserData();
if (cache == null) { if (cache == null) {
return; return;
@ -110,9 +121,7 @@ public class LPPermissible extends PermissibleBase {
} }
public void addAttachments(List<PermissionAttachment> attachments) { public void addAttachments(List<PermissionAttachment> attachments) {
for (PermissionAttachment attachment : attachments) { this.attachments.addAll(attachments);
this.attachments.add(attachment);
}
} }
public Contexts calculateContexts() { public Contexts calculateContexts() {
@ -128,7 +137,7 @@ public class LPPermissible extends PermissibleBase {
} }
private boolean hasData() { private boolean hasData() {
return user.getUserData() != null; return user != null && user.getUserData() != null;
} }
@Override @Override

View File

@ -22,6 +22,8 @@
package me.lucko.luckperms.bungee; package me.lucko.luckperms.bungee;
import lombok.RequiredArgsConstructor;
import me.lucko.luckperms.api.Contexts; import me.lucko.luckperms.api.Contexts;
import me.lucko.luckperms.api.caching.UserData; import me.lucko.luckperms.api.caching.UserData;
import me.lucko.luckperms.api.context.MutableContextSet; 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.UuidCache;
import me.lucko.luckperms.common.core.model.User; import me.lucko.luckperms.common.core.model.User;
import me.lucko.luckperms.common.defaults.Rule; 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.chat.TextComponent;
import net.md_5.bungee.api.connection.PendingConnection; 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.EventHandler;
import net.md_5.bungee.event.EventPriority; 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.UUID;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@SuppressWarnings("WeakerAccess") @RequiredArgsConstructor
public class BungeeListener extends AbstractListener implements Listener { public class BungeeListener implements Listener {
private static final TextComponent WARN_MESSAGE = new TextComponent(Message.LOADING_ERROR.toString());
private final LPBungeePlugin plugin; private final LPBungeePlugin plugin;
BungeeListener(LPBungeePlugin plugin) { private final Set<UUID> deniedLogin = Collections.synchronizedSet(new HashSet<>());
super(plugin);
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGH) @EventHandler(priority = EventPriority.LOW)
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)
public void onPlayerLogin(LoginEvent e) { 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 This means that a player will have the same UUID across the network, even if parts of the network are running in
Offline mode. */ 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); 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(() -> { plugin.doAsync(() -> {
final long startTime = System.currentTimeMillis();
final UuidCache cache = plugin.getUuidCache(); final UuidCache cache = plugin.getUuidCache();
final PendingConnection c = e.getConnection();
if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUIDS)) { if (!plugin.getConfiguration().get(ConfigKeys.USE_SERVER_UUIDS)) {
UUID uuid = plugin.getStorage().getUUID(c.getName()).join(); 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 // No previous data for this player
plugin.getApiProvider().getEventFactory().handleUserFirstLogin(c.getUniqueId(), c.getName()); plugin.getApiProvider().getEventFactory().handleUserFirstLogin(c.getUniqueId(), c.getName());
cache.addToCache(c.getUniqueId(), c.getUniqueId()); 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(); plugin.getStorage().force().saveUUIDData(c.getName(), c.getUniqueId()).join();
} }
} else { } else {
@ -118,12 +120,14 @@ public class BungeeListener extends AbstractListener implements Listener {
} }
// Online mode, no cache needed. This is just for name -> uuid lookup. // 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(); 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 /* We have to make a new user on this thread whilst the connection is being held, or we get concurrency issues
// and the BungeeCord server try to make a new user at the same time. 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(); plugin.getStorage().force().loadUser(cache.getUUID(c.getUniqueId()), c.getName()).join();
User user = plugin.getUserManager().get(cache.getUUID(c.getUniqueId())); User user = plugin.getUserManager().get(cache.getUUID(c.getUniqueId()));
if (user == null) { if (user == null) {
plugin.getLog().warn("Failed to load user: " + c.getName()); plugin.getLog().warn("Failed to load user: " + c.getName());
@ -148,7 +152,13 @@ public class BungeeListener extends AbstractListener implements Listener {
if (time >= 1000) { if (time >= 1000) {
plugin.getLog().warn("Processing login for " + c.getName() + " took " + time + "ms."); 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); 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())); final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(e.getPlayer().getUniqueId()));
if (user == null) { 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 // Wait until the last priority to unload, so plugins can still perform permission checks on this event
@EventHandler(priority = EventPriority.HIGHEST) @EventHandler(priority = EventPriority.HIGHEST)
public void onPlayerQuit(PlayerDisconnectEvent e) { 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) @EventHandler(priority = EventPriority.LOWEST)
public void onServerSwitch(ServerConnectEvent e) { public void onServerSwitch(ServerConnectEvent e) {
String serverName = e.getTarget().getName(); String serverName = e.getTarget().getName();

View File

@ -323,7 +323,8 @@ public class LPBungeePlugin extends Plugin implements LuckPermsPlugin {
@Override @Override
public boolean isPlayerOnline(UUID external) { public boolean isPlayerOnline(UUID external) {
return getProxy().getPlayer(external) != null; ProxiedPlayer player = getProxy().getPlayer(external);
return player != null && player.isConnected();
} }
@Override @Override

View File

@ -27,6 +27,7 @@ import lombok.Getter;
import me.lucko.luckperms.common.commands.sender.Sender; import me.lucko.luckperms.common.commands.sender.Sender;
import me.lucko.luckperms.common.commands.utils.Util; import me.lucko.luckperms.common.commands.utils.Util;
import me.lucko.luckperms.common.locale.LocaleManager;
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
@AllArgsConstructor @AllArgsConstructor
@ -44,7 +45,7 @@ public enum Message {
EMPTY("{0}", true), EMPTY("{0}", true),
PLAYER_ONLINE("&aOnline", false), PLAYER_ONLINE("&aOnline", false),
PLAYER_OFFLINE("&cOffline", false), PLAYER_OFFLINE("&cOffline", false),
LOADING_ERROR("Permissions data could not be loaded. Please contact an administrator.", true), LOADING_ERROR("Permissions data could not be loaded. Please try again later.", true),
OP_DISABLED("&bThe vanilla OP system is disabled on this server.", false), OP_DISABLED("&bThe vanilla OP system is disabled on this server.", false),
OP_DISABLED_SPONGE("&2Server Operator status has no effect when a permission plugin is installed. Please edit user data directly.", true), OP_DISABLED_SPONGE("&2Server Operator status has no effect when a permission plugin is installed. Please edit user data directly.", true),
LOG("&3LOG &3&l> {0}", true), LOG("&3LOG &3&l> {0}", true),
@ -452,6 +453,21 @@ public enum Message {
return Util.color(showPrefix ? PREFIX + message : message); return Util.color(showPrefix ? PREFIX + message : message);
} }
public String asString(LocaleManager localeManager) {
String prefix = localeManager.getTranslation(PREFIX);
if (prefix == null) {
prefix = PREFIX.getMessage();
}
String s = localeManager.getTranslation(this);
if (s == null) {
s = message;
}
s = s.replace("{PREFIX}", prefix).replace("\\n", "\n");
return Util.color(showPrefix ? (prefix + s) : (s));
}
public void send(Sender sender, Object... objects) { public void send(Sender sender, Object... objects) {
String prefix = sender.getPlatform().getLocaleManager().getTranslation(PREFIX); String prefix = sender.getPlatform().getLocaleManager().getTranslation(PREFIX);
if (prefix == null) { if (prefix == null) {

View File

@ -60,6 +60,13 @@ public interface UserManager extends Manager<UserIdentifier, User> {
*/ */
void cleanup(User user); void cleanup(User user);
/**
* Schedules a task to cleanup a user after a certain period of time, if they're not on the server anymore.
*
* @param uuid external uuid of the player
*/
void scheduleUnload(UUID uuid);
/** /**
* Reloads the data of all online users * Reloads the data of all online users
*/ */

View File

@ -134,6 +134,18 @@ public class GenericUserManager extends AbstractManager<UserIdentifier, User> im
} }
} }
@Override
public void scheduleUnload(UUID uuid) {
plugin.getScheduler().doAsyncLater(() -> {
User user = get(plugin.getUuidCache().getUUID(uuid));
if (user != null && !plugin.isPlayerOnline(uuid)) {
user.unregisterData();
unload(user);
}
plugin.getUuidCache().clearCache(uuid);
}, 40L);
}
@Override @Override
public void updateAllUsers() { public void updateAllUsers() {
plugin.doSync(() -> { plugin.doSync(() -> {

View File

@ -22,7 +22,7 @@
package me.lucko.luckperms.common.utils; package me.lucko.luckperms.common.utils;
import lombok.AllArgsConstructor; import lombok.experimental.UtilityClass;
import me.lucko.luckperms.common.config.ConfigKeys; import me.lucko.luckperms.common.config.ConfigKeys;
import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.core.UuidCache;
@ -33,13 +33,12 @@ import me.lucko.luckperms.common.plugin.LuckPermsPlugin;
import java.util.UUID; import java.util.UUID;
/** /**
* An abstract listener shared by Bukkit & Sponge. * Utilities for use in platform listeners
*/ */
@AllArgsConstructor @UtilityClass
public class AbstractListener { public class LoginHelper {
private final LuckPermsPlugin plugin;
public void onAsyncLogin(UUID u, String username) { public static void loadUser(LuckPermsPlugin plugin, UUID u, String username) {
final long startTime = System.currentTimeMillis(); final long startTime = System.currentTimeMillis();
final UuidCache cache = plugin.getUuidCache(); final UuidCache cache = plugin.getUuidCache();
@ -67,6 +66,7 @@ public class AbstractListener {
User user = plugin.getUserManager().get(cache.getUUID(u)); User user = plugin.getUserManager().get(cache.getUUID(u));
if (user == null) { if (user == null) {
plugin.getLog().warn("Failed to load user: " + username); plugin.getLog().warn("Failed to load user: " + username);
throw new RuntimeException("Failed to load user");
} else { } else {
// Setup defaults for the user // Setup defaults for the user
boolean save = false; boolean save = false;
@ -90,20 +90,7 @@ public class AbstractListener {
} }
} }
protected void onLeave(UUID uuid) { public static void refreshPlayer(LuckPermsPlugin plugin, UUID uuid) {
final UuidCache cache = plugin.getUuidCache();
final User user = plugin.getUserManager().get(cache.getUUID(uuid));
if (user != null) {
user.unregisterData();
plugin.getUserManager().unload(user);
}
// Unload the user from memory when they disconnect;
cache.clearCache(uuid);
}
protected void refreshPlayer(UUID uuid) {
final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(uuid)); final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(uuid));
if (user != null) { if (user != null) {
user.getRefreshBuffer().requestDirectly(); user.getRefreshBuffer().requestDirectly();

View File

@ -440,7 +440,7 @@ public class LPSpongePlugin implements LuckPermsPlugin {
@Override @Override
public boolean isPlayerOnline(UUID external) { public boolean isPlayerOnline(UUID external) {
return game.getServer().getPlayer(external).isPresent(); return game.getServer().getPlayer(external).map(Player::isOnline).orElse(false);
} }
@Override @Override

View File

@ -22,12 +22,14 @@
package me.lucko.luckperms.sponge; package me.lucko.luckperms.sponge;
import lombok.RequiredArgsConstructor;
import me.lucko.luckperms.api.caching.UserData; import me.lucko.luckperms.api.caching.UserData;
import me.lucko.luckperms.api.context.MutableContextSet; import me.lucko.luckperms.api.context.MutableContextSet;
import me.lucko.luckperms.common.constants.Message; import me.lucko.luckperms.common.constants.Message;
import me.lucko.luckperms.common.core.UuidCache; import me.lucko.luckperms.common.core.UuidCache;
import me.lucko.luckperms.common.core.model.User; import me.lucko.luckperms.common.core.model.User;
import me.lucko.luckperms.common.utils.AbstractListener; import me.lucko.luckperms.common.utils.LoginHelper;
import me.lucko.luckperms.sponge.timings.LPTiming; import me.lucko.luckperms.sponge.timings.LPTiming;
import org.spongepowered.api.command.CommandSource; import org.spongepowered.api.command.CommandSource;
@ -35,49 +37,131 @@ import org.spongepowered.api.entity.living.player.Player;
import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.Order; import org.spongepowered.api.event.Order;
import org.spongepowered.api.event.command.SendCommandEvent; import org.spongepowered.api.event.command.SendCommandEvent;
import org.spongepowered.api.event.filter.IsCancelled;
import org.spongepowered.api.event.network.ClientConnectionEvent; import org.spongepowered.api.event.network.ClientConnectionEvent;
import org.spongepowered.api.profile.GameProfile; import org.spongepowered.api.profile.GameProfile;
import org.spongepowered.api.text.serializer.TextSerializers; import org.spongepowered.api.text.serializer.TextSerializers;
import org.spongepowered.api.util.Tristate;
import org.spongepowered.api.world.World; import org.spongepowered.api.world.World;
import co.aikar.timings.Timing; import co.aikar.timings.Timing;
import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@SuppressWarnings("WeakerAccess") @RequiredArgsConstructor
public class SpongeListener extends AbstractListener { public class SpongeListener {
private final LPSpongePlugin plugin; private final LPSpongePlugin plugin;
SpongeListener(LPSpongePlugin plugin) { private final Set<UUID> deniedAsyncLogin = Collections.synchronizedSet(new HashSet<>());
super(plugin); private final Set<UUID> deniedLogin = Collections.synchronizedSet(new HashSet<>());
this.plugin = plugin;
}
@Listener(order = Order.EARLY) @Listener(order = Order.AFTER_PRE)
@IsCancelled(Tristate.UNDEFINED)
public void onClientAuth(ClientConnectionEvent.Auth e) { public void onClientAuth(ClientConnectionEvent.Auth e) {
if (!plugin.getStorage().isAcceptingLogins()) { /* Called when the player first attempts a connection with the server.
/* Datastore is disabled, prevent players from joining the server Listening on AFTER_PRE priority to allow plugins to modify username / UUID data here. (auth plugins) */
Just don't load their data, they will be kicked at login */
final GameProfile p = e.getProfile();
/* the player was denied entry to the server before this priority.
log this, so we can handle appropriately later. */
if (e.isCancelled()) {
plugin.getLog().warn("Connection from " + p.getUniqueId() + " was already denied. No permissions data will be loaded.");
deniedAsyncLogin.add(p.getUniqueId());
return; return;
} }
final GameProfile p = e.getProfile(); /* either the plugin hasn't finished starting yet, or there was an issue connecting to the DB, performing file i/o, etc.
onAsyncLogin(p.getUniqueId(), p.getName().get()); // Load the user into LuckPerms we don't let players join in this case, because it means they can connect to the server without their permissions data.
some server admins rely on negating perms to stop users from causing damage etc, so it's really important that
this data is loaded. */
if (!plugin.getStorage().isAcceptingLogins()) {
// log that the user tried to login, but was denied at this stage.
deniedAsyncLogin.add(p.getUniqueId());
// actually deny the connection.
plugin.getLog().warn("Permissions storage is not loaded yet. Denying connection from: " + p.getUniqueId() + " - " + p.getName());
e.setCancelled(true);
e.setMessageCancelled(true);
//noinspection deprecation
e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.asString(plugin.getLocaleManager())));
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. */
try {
LoginHelper.loadUser(plugin, p.getUniqueId(), p.getName().orElseThrow(() -> new RuntimeException("No username present for user " + p.getUniqueId())));
} catch (Exception ex) {
ex.printStackTrace();
e.setCancelled(true);
e.setMessageCancelled(true);
//noinspection deprecation
e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.asString(plugin.getLocaleManager())));
}
} }
@SuppressWarnings("deprecation") @Listener(order = Order.BEFORE_POST)
@Listener(order = Order.EARLY) @IsCancelled(Tristate.UNDEFINED)
public void onClientAuthMonitor(ClientConnectionEvent.Auth e) {
/* Listen to see if the event was cancelled after we initially handled the connection
If the connection was cancelled here, we need to do something to clean up the data that was loaded. */
// Check to see if this connection was denied at LOW.
if (deniedAsyncLogin.remove(e.getProfile().getUniqueId())) {
// This is a problem, as they were denied at low priority, but are now being allowed.
if (e.isCancelled()) {
plugin.getLog().severe("Player connection was re-allowed for " + e.getProfile().getUniqueId());
e.setCancelled(true);
}
}
}
@Listener(order = Order.AFTER_PRE)
@IsCancelled(Tristate.UNDEFINED)
public void onClientLogin(ClientConnectionEvent.Login e) { public void onClientLogin(ClientConnectionEvent.Login e) {
try (Timing ignored = plugin.getTimings().time(LPTiming.ON_CLIENT_LOGIN)) { try (Timing ignored = plugin.getTimings().time(LPTiming.ON_CLIENT_LOGIN)) {
/* Called when the player starts logging into the server.
At this point, the users data should be present and loaded.
Listening on LOW priority to allow plugins to further modify data here. (auth plugins, etc.) */
final GameProfile player = e.getProfile(); final GameProfile player = e.getProfile();
/* the player was denied entry to the server before this priority.
log this, so we can handle appropriately later. */
if (e.isCancelled()) {
plugin.getLog().warn("Login from " + player.getUniqueId() + " was denied before an attachment could be injected.");
deniedLogin.add(player.getUniqueId());
return;
}
final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId())); final User user = plugin.getUserManager().get(plugin.getUuidCache().getUUID(player.getUniqueId()));
// Check if the user was loaded successfully. /* User instance is null for whatever reason. Could be that it was unloaded between asyncpre and now. */
if (user == null) { if (user == null) {
deniedLogin.add(player.getUniqueId());
plugin.getLog().warn("User " + player.getUniqueId() + " - " + player.getName() + " doesn't have data pre-loaded. - denying login.");
e.setCancelled(true); e.setCancelled(true);
e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.toString())); e.setMessageCancelled(true);
//noinspection deprecation
e.setMessage(TextSerializers.LEGACY_FORMATTING_CODE.deserialize(Message.LOADING_ERROR.asString(plugin.getLocaleManager())));
return; return;
} }
@ -108,7 +192,7 @@ public class SpongeListener extends AbstractListener {
@Listener(order = Order.EARLY) @Listener(order = Order.EARLY)
public void onClientJoin(ClientConnectionEvent.Join e) { public void onClientJoin(ClientConnectionEvent.Join e) {
// Refresh permissions again // Refresh permissions again
plugin.doAsync(() -> refreshPlayer(e.getTargetEntity().getUniqueId())); plugin.doAsync(() -> LoginHelper.refreshPlayer(plugin, e.getTargetEntity().getUniqueId()));
} }
@Listener(order = Order.LAST) @Listener(order = Order.LAST)

View File

@ -205,6 +205,11 @@ public class SpongeUserManager implements UserManager, LPSubjectCollection {
// Do nothing - this instance uses other means in order to cleanup // Do nothing - this instance uses other means in order to cleanup
} }
@Override
public void scheduleUnload(UUID uuid) {
// Do nothing - this instance uses other means in order to cleanup
}
@Override @Override
public void updateAllUsers() { public void updateAllUsers() {
plugin.doSync(() -> { plugin.doSync(() -> {