mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 05:50:39 +00:00
Release runtime statics
- Null Iris.instance, the console sender, the Multiverse link, the compat set and the chunk tickets at the end of teardownRuntime - Bind the host bridge suppliers to the constructing plugin instead of reading the instance static on every call - State that Iris is disabled when service, getTemp or getSender is reached after teardown instead of raising a bare NullPointerException - Add IrisRuntimeStatics so a disable drops the service registry, both capability singletons, the loaded datapack runtime and the localization roots - Add WorldLifecycleService.reset and WorldRuntimeControlService.reset so the next boot snapshots the server it is running on - Add ServerConfigurator.resetLoadedDatapackRuntime so an enable revalidates instead of trusting the previous boot's fingerprint - Clear the localization data folder and plain-text memo on shutdown - Add BukkitPlatform.releaseHost and call it last in terminal cleanup so the disabled plugin, console sender and log bridge stop being reachable from a static
This commit is contained in:
1 parent
f83cf71b11
commit
6c6dba0232
10 files changed
+197
-13
No files matched your search
@@ -39,6 +39,7 @@ import art.arcane.iris.core.SettingsHotloadWatch;
|
|||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
import art.arcane.iris.core.datapack.DatapackIngestService;
|
import art.arcane.iris.core.datapack.DatapackIngestService;
|
||||||
import art.arcane.iris.core.datapack.DatapackIngestService.StartupValidationOutcome;
|
import art.arcane.iris.core.datapack.DatapackIngestService.StartupValidationOutcome;
|
||||||
|
import art.arcane.iris.core.lifecycle.IrisRuntimeStatics;
|
||||||
import art.arcane.iris.core.lifecycle.ManagedWorldLoader;
|
import art.arcane.iris.core.lifecycle.ManagedWorldLoader;
|
||||||
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
|
import art.arcane.iris.core.lifecycle.MissingWorldStorageLog;
|
||||||
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
|
import art.arcane.iris.core.lifecycle.PaperLibBootstrap;
|
||||||
@@ -216,16 +217,23 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
public static VolmitSender getSender() {
|
||||||
if (sender == null) {
|
VolmitSender current = sender;
|
||||||
sender = new VolmitSender(Bukkit.getConsoleSender());
|
if (current == null) {
|
||||||
sender.setTag(instance.getTag());
|
Iris plugin = instance;
|
||||||
|
current = new VolmitSender(Bukkit.getConsoleSender());
|
||||||
|
current.setTag(plugin == null ? IrisSafeguard.mode().tag("") : plugin.getTag());
|
||||||
|
sender = current;
|
||||||
}
|
}
|
||||||
return sender;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <T> T service(Class<T> c) {
|
public static <T> T service(Class<T> c) {
|
||||||
return (T) instance.services.get(c);
|
Iris plugin = instance;
|
||||||
|
if (plugin == null || plugin.services == null) {
|
||||||
|
throw new IllegalStateException("Iris is disabled; " + c.getSimpleName() + " is unavailable");
|
||||||
|
}
|
||||||
|
return (T) plugin.services.get(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void callEvent(Event e) {
|
public static void callEvent(Event e) {
|
||||||
@@ -264,7 +272,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static File getTemp() {
|
public static File getTemp() {
|
||||||
return instance.getDataFolder("cache", "temp");
|
Iris plugin = instance;
|
||||||
|
if (plugin == null) {
|
||||||
|
throw new IllegalStateException("Iris is disabled; the temp folder is unavailable");
|
||||||
|
}
|
||||||
|
return plugin.getDataFolder("cache", "temp");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void msg(String string) {
|
public static void msg(String string) {
|
||||||
@@ -543,11 +555,11 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
Iris::msg,
|
Iris::msg,
|
||||||
Iris::reportError,
|
Iris::reportError,
|
||||||
(event) -> Iris.callEvent((org.bukkit.event.Event) event),
|
(event) -> Iris.callEvent((org.bukkit.event.Event) event),
|
||||||
() -> Iris.instance.getDataFolder(),
|
this::getDataFolder,
|
||||||
(path) -> Iris.instance.getDataFile(path),
|
this::getDataFile,
|
||||||
() -> Iris.instance.getJarFile(),
|
this::getJarFile,
|
||||||
() -> Iris.instance.getIrisVersion(),
|
this::getIrisVersion,
|
||||||
() -> Iris.instance.getMCVersion()));
|
this::getMCVersion));
|
||||||
SlimJar.load();
|
SlimJar.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1055,10 +1067,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||||
}
|
}
|
||||||
clearQueues();
|
clearQueues();
|
||||||
IrisServices.clear();
|
IrisRuntimeStatics.reset();
|
||||||
|
releaseStatics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A disabled plugin must not stay reachable through a static. Everything that reads these either runs
|
||||||
|
* while Iris is enabled or already handles their absence; the platform binding outlives them because
|
||||||
|
* core still logs through it until finishTerminalCleanup unbinds it.
|
||||||
|
*/
|
||||||
|
private static void releaseStatics() {
|
||||||
|
linkMultiverseCore = null;
|
||||||
|
compat = null;
|
||||||
|
tickets = null;
|
||||||
|
sender = null;
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void quiesceRuntimeForServerShutdown(String reason) {
|
private void quiesceRuntimeForServerShutdown(String reason) {
|
||||||
serverStopTeardownDeferred.set(true);
|
serverStopTeardownDeferred.set(true);
|
||||||
INMS.get().deferPluginClassLoaderClose();
|
INMS.get().deferPluginClassLoaderClose();
|
||||||
@@ -1201,6 +1227,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BukkitPlatform.releaseHost();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1395,7 +1422,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getIrisVersion() {
|
public int getIrisVersion() {
|
||||||
String input = Iris.instance.getDescription().getVersion();
|
String input = getDescription().getVersion();
|
||||||
int hyphenIndex = input.indexOf('-');
|
int hyphenIndex = input.indexOf('-');
|
||||||
if (hyphenIndex != -1) {
|
if (hyphenIndex != -1) {
|
||||||
String result = input.substring(0, hyphenIndex);
|
String result = input.substring(0, hyphenIndex);
|
||||||
|
|||||||
@@ -158,6 +158,20 @@ public class ServerConfigurator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forgets what the previous boot loaded, so an enable in the same JVM recompiles and revalidates rather
|
||||||
|
* than trusting a fingerprint taken against a server that is gone.
|
||||||
|
*/
|
||||||
|
public static void resetLoadedDatapackRuntime() {
|
||||||
|
synchronized (DATAPACK_INSTALL_LOCK) {
|
||||||
|
loadedDatapackRuntimeReady = false;
|
||||||
|
loadedDatapackCompilerInputFingerprint = "";
|
||||||
|
loadedDatapackRegistryRequirements = Map.of();
|
||||||
|
loadedDatapackRuntimeGeneration++;
|
||||||
|
loadedDatapackRestartRequired = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static LoadedDatapackRuntimeInvalidation invalidateLoadedDatapackRuntime() {
|
public static LoadedDatapackRuntimeInvalidation invalidateLoadedDatapackRuntime() {
|
||||||
synchronized (DATAPACK_INSTALL_LOCK) {
|
synchronized (DATAPACK_INSTALL_LOCK) {
|
||||||
boolean wasReady = loadedDatapackRuntimeReady;
|
boolean wasReady = loadedDatapackRuntimeReady;
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops every process-wide cache core keeps about the running server, so a disable leaves nothing an
|
||||||
|
* enable in the same JVM could answer from.
|
||||||
|
* <p>
|
||||||
|
* The capability singletons are the reason this exists: each holds reflective handles onto the live
|
||||||
|
* CraftServer and MinecraftServer, and a boot that reused them would be talking to the previous server's
|
||||||
|
* objects. The platform binding itself is not cleared here - core still logs through it while the adapter
|
||||||
|
* finishes its own teardown - and is unbound last by the adapter.
|
||||||
|
*/
|
||||||
|
public final class IrisRuntimeStatics {
|
||||||
|
private IrisRuntimeStatics() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void reset() {
|
||||||
|
IrisServices.clear();
|
||||||
|
WorldLifecycleService.reset();
|
||||||
|
WorldRuntimeControlService.reset();
|
||||||
|
ServerConfigurator.resetLoadedDatapackRuntime();
|
||||||
|
IrisLanguage.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,15 @@ public final class WorldLifecycleService {
|
|||||||
this.worldBackendByKey = new ConcurrentHashMap<>();
|
this.worldBackendByKey = new ConcurrentHashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the cached snapshot so the next boot in this JVM builds one against the server it is running on.
|
||||||
|
*/
|
||||||
|
public static void reset() {
|
||||||
|
synchronized (WorldLifecycleService.class) {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static WorldLifecycleService get() {
|
public static WorldLifecycleService get() {
|
||||||
WorldLifecycleService current = instance;
|
WorldLifecycleService current = instance;
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
|
|||||||
@@ -429,6 +429,8 @@ public final class IrisLanguage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static synchronized void shutdown() {
|
public static synchronized void shutdown() {
|
||||||
|
dataFolder = null;
|
||||||
|
PLAIN_MEMO.set(null);
|
||||||
PluginLanguageService current = selections;
|
PluginLanguageService current = selections;
|
||||||
selections = null;
|
selections = null;
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
|
|||||||
@@ -76,6 +76,15 @@ public final class WorldRuntimeControlService {
|
|||||||
+ ", " + backend.describeCapabilities();
|
+ ", " + backend.describeCapabilities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the cached snapshot so the next boot in this JVM builds one against the server it is running on.
|
||||||
|
*/
|
||||||
|
public static void reset() {
|
||||||
|
synchronized (WorldRuntimeControlService.class) {
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static WorldRuntimeControlService get() {
|
public static WorldRuntimeControlService get() {
|
||||||
WorldRuntimeControlService current = instance;
|
WorldRuntimeControlService current = instance;
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ final class DecoratorCore {
|
|||||||
|
|
||||||
static final ThreadLocal<PlaceOpts> SCRATCH_OPTS = ThreadLocal.withInitial(PlaceOpts::new);
|
static final ThreadLocal<PlaceOpts> SCRATCH_OPTS = ThreadLocal.withInitial(PlaceOpts::new);
|
||||||
|
|
||||||
|
|
||||||
static final class PlaceOpts {
|
static final class PlaceOpts {
|
||||||
boolean caveSkipFluid;
|
boolean caveSkipFluid;
|
||||||
boolean underwater;
|
boolean underwater;
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ public final class BukkitPlatform implements IrisPlatform {
|
|||||||
PLUGIN = plugin;
|
PLUGIN = plugin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops every reference this class holds to the disabled plugin. Called last in the adapter's teardown,
|
||||||
|
* after core has stopped logging through the host bridge, so a disabled plugin instance is not reachable
|
||||||
|
* from a static for the life of the JVM.
|
||||||
|
*/
|
||||||
|
public static void releaseHost() {
|
||||||
|
PLUGIN = null;
|
||||||
|
HUD_BAR = null;
|
||||||
|
HUD_LANES = null;
|
||||||
|
CONSOLE = null;
|
||||||
|
BRIDGE = null;
|
||||||
|
}
|
||||||
|
|
||||||
public static Plugin plugin() {
|
public static Plugin plugin() {
|
||||||
Plugin plugin = PLUGIN;
|
Plugin plugin = PLUGIN;
|
||||||
if (plugin == null) {
|
if (plugin == null) {
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||||
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotSame;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iris keeps process-wide caches that describe the running server: the service registry, and the lifecycle
|
||||||
|
* and runtime-control singletons that each hold a capability snapshot of reflective handles onto the live
|
||||||
|
* server. Nothing cleared them on disable, so a re-enable in the same JVM kept answering from the previous
|
||||||
|
* boot's snapshot.
|
||||||
|
*/
|
||||||
|
public class IrisRuntimeStaticsTest {
|
||||||
|
@Before
|
||||||
|
public void installServer() {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void clearStatics() {
|
||||||
|
IrisRuntimeStatics.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetUnbindsEveryRegisteredService() {
|
||||||
|
Runnable service = () -> {
|
||||||
|
};
|
||||||
|
IrisServices.register(Runnable.class, service);
|
||||||
|
|
||||||
|
IrisRuntimeStatics.reset();
|
||||||
|
|
||||||
|
assertNull(IrisServices.getOrNull(Runnable.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resetDropsTheCachedCapabilitySingletons() {
|
||||||
|
WorldLifecycleService lifecycle = WorldLifecycleService.get();
|
||||||
|
WorldRuntimeControlService control = WorldRuntimeControlService.get();
|
||||||
|
|
||||||
|
IrisRuntimeStatics.reset();
|
||||||
|
|
||||||
|
assertNotSame(lifecycle, WorldLifecycleService.get());
|
||||||
|
assertNotSame(control, WorldRuntimeControlService.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit;
|
||||||
|
|
||||||
|
import org.bukkit.plugin.Plugin;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The host statics are how core reaches the plugin. Nothing cleared them on disable, so the disabled
|
||||||
|
* plugin instance, its console sender and its log bridge stayed reachable from a static for the life of
|
||||||
|
* the JVM.
|
||||||
|
*/
|
||||||
|
public class BukkitPlatformHostReleaseTest {
|
||||||
|
@After
|
||||||
|
public void clearHost() {
|
||||||
|
BukkitPlatform.releaseHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void releasingTheHostDropsThePluginAndTheHud() {
|
||||||
|
BukkitPlatform.hostPlugin(mock(Plugin.class));
|
||||||
|
|
||||||
|
BukkitPlatform.releaseHost();
|
||||||
|
|
||||||
|
assertFalse(BukkitPlatform.hasPlugin());
|
||||||
|
assertFalse(BukkitPlatform.hasHud());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user