mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Defer runtime injection
- Add RuntimeInjection so the Java agent attach and the server-code injection run once, on demand, instead of during every enable - Install the injection when a world Iris created is about to be built and when Bukkit asks for the generator of a world configured outside Iris - Lock the runtime with the same reason a boot-time injection failure used to raise when the deferred install fails - Add general.eagerRuntimeInjection to keep boot-time injection and its startup verification - Re-read the startup lock after a deferred install so a failed install refuses the world instead of generating it - Move the stale cache/temp sweep off the boot thread and make every pack import wait it out - Move the default-pack presence warning off the boot thread; it walks the whole pack tree to pick a warning - Build the wand and dust templates on the first tick instead of during service enable - Report enable duration, per-phase timings and the three slowest services on every boot - Drop the wand and dust templates on service disable
This commit is contained in:
1 parent
6c6dba0232
commit
486c6eeae9
11 files changed
+426
-35
No files matched your search
@@ -149,6 +149,7 @@ import java.lang.reflect.Modifier;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -161,6 +162,7 @@ import java.util.logging.Level;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@SuppressWarnings("CanBeFinal")
|
@SuppressWarnings("CanBeFinal")
|
||||||
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
||||||
@@ -626,6 +628,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
|
J.s(() -> Bukkit.getPluginManager().disablePlugin(this), 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
EnableTimings timings = new EnableTimings();
|
||||||
alreadyDrained.set(false);
|
alreadyDrained.set(false);
|
||||||
postStopFinisherStarted.set(false);
|
postStopFinisherStarted.set(false);
|
||||||
serverStopTeardownDeferred.set(false);
|
serverStopTeardownDeferred.set(false);
|
||||||
@@ -647,6 +650,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
DirectorMiniMenu.Theme.irisGreen(), IrisLanguage.directorResolver(), IrisLanguage.editorOptions()));
|
||||||
PaperLibBootstrap.install();
|
PaperLibBootstrap.install();
|
||||||
SimdSupport.install();
|
SimdSupport.install();
|
||||||
|
timings.mark("bootstrap");
|
||||||
services = new KMap<>();
|
services = new KMap<>();
|
||||||
BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane());
|
BukkitPlatform.hostHud(new HudActionBar(this), new HudBossBarLane());
|
||||||
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
|
// Explicit, ordered service list: the previous reflective jar scan gave hash-ordered
|
||||||
@@ -685,9 +689,13 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
IrisServices.register(PreservationRegistry.class, services.get(PreservationSVC.class));
|
||||||
compat = IrisCompat.configured(getDataFile("compat.json"));
|
compat = IrisCompat.configured(getDataFile("compat.json"));
|
||||||
IrisServices.register(IrisCompat.class, compat);
|
IrisServices.register(IrisCompat.class, compat);
|
||||||
|
timings.mark("services");
|
||||||
ServerConfigurator.configure();
|
ServerConfigurator.configure();
|
||||||
|
timings.mark("serverConfig");
|
||||||
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
|
StartupValidationOutcome datapackValidation = DatapackIngestService.validateOnStartup();
|
||||||
|
timings.mark("datapacks");
|
||||||
IrisSafeguard.execute();
|
IrisSafeguard.execute();
|
||||||
|
timings.mark("safeguard");
|
||||||
getSender().setTag(getTag());
|
getSender().setTag(getTag());
|
||||||
// A cosmetic banner must never abort the bootstrap.
|
// A cosmetic banner must never abort the bootstrap.
|
||||||
J.attempt(this::splash);
|
J.attempt(this::splash);
|
||||||
@@ -696,6 +704,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
// Paper's bootstrap runs before any plugin logger exists, so orphan-storage warnings raised there
|
// Paper's bootstrap runs before any plugin logger exists, so orphan-storage warnings raised there
|
||||||
// never reach logs/latest.log. Replay them once now that the platform log is up.
|
// never reach logs/latest.log. Replay them once now that the platform log is up.
|
||||||
MissingWorldStorageLog.replayToPlatformLog();
|
MissingWorldStorageLog.replayToPlatformLog();
|
||||||
|
timings.mark("splash");
|
||||||
tickets = new ChunkTickets();
|
tickets = new ChunkTickets();
|
||||||
linkMultiverseCore = new MultiverseCoreLink();
|
linkMultiverseCore = new MultiverseCoreLink();
|
||||||
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
|
IrisServices.register(MultiverseCoreLink.class, linkMultiverseCore);
|
||||||
@@ -708,18 +717,24 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
|
IrisServices.register(ManagedWorldLoader.class, (ManagedWorldLoader) this::loadManagedWorld);
|
||||||
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("iris.json"));
|
SettingsHotloadWatch watch = new SettingsHotloadWatch(getDataFile("iris.json"));
|
||||||
settingsHotloadWatch = watch;
|
settingsHotloadWatch = watch;
|
||||||
// Stale-temp cleanup must complete before services enable: StudioSVC.onEnable downloads
|
// Off the boot thread, because a stale cache/temp can hold a whole abandoned pack import and
|
||||||
// packs through cache/temp on an async thread, and a concurrent delete of that folder
|
// deleting it recursively is unbounded. Every pack import waits this sweep out instead, which is
|
||||||
// truncated pack imports mid-copy (partial packs/<key> without dimensions/).
|
// the invariant that mattered: a delete running underneath an import truncated it mid-copy.
|
||||||
IO.delete(getTemp());
|
StudioSVC.gateDownloadsOnStaleTempCleanup(MultiBurst.ioBurst.completeValueAsync(() -> {
|
||||||
|
IO.delete(getTemp());
|
||||||
|
return null;
|
||||||
|
}));
|
||||||
|
timings.mark("tempSweep");
|
||||||
// One throwing service must not abort the bootstrap: the steps after this loop
|
// One throwing service must not abort the bootstrap: the steps after this loop
|
||||||
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
|
// (listeners, shutdown hook, replacement journals) are the safety-critical ones.
|
||||||
// Only services that actually enabled get listeners and a later onDisable.
|
// Only services that actually enabled get listeners and a later onDisable.
|
||||||
enabledServices.clear();
|
enabledServices.clear();
|
||||||
for (IrisService service : orderedServices) {
|
for (IrisService service : orderedServices) {
|
||||||
|
long serviceStartedAt = System.nanoTime();
|
||||||
try {
|
try {
|
||||||
service.onEnable();
|
service.onEnable();
|
||||||
enabledServices.add(service);
|
enabledServices.add(service);
|
||||||
|
timings.markService(service.getClass().getSimpleName(), serviceStartedAt);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
// A service failure is NOT a datapack validation failure: the admission gate
|
// A service failure is NOT a datapack validation failure: the admission gate
|
||||||
// must never lock every login over a broken cosmetic service. Log loudly,
|
// must never lock every login over a broken cosmetic service. Log loudly,
|
||||||
@@ -740,10 +755,12 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
|
Iris.reportError("Failed to register listener for " + service.getClass().getSimpleName() + ".", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
timings.mark("serviceEnable");
|
||||||
if (datapackValidation == StartupValidationOutcome.READY) {
|
if (datapackValidation == StartupValidationOutcome.READY) {
|
||||||
IrisServices.get(ExternalDataSVC.class).setContentChangeListener(generatorResolver::requestExternalContentRefresh);
|
IrisServices.get(ExternalDataSVC.class).setContentChangeListener(generatorResolver::requestExternalContentRefresh);
|
||||||
generatorResolver.validateAllPacks();
|
generatorResolver.validateAllPacks();
|
||||||
}
|
}
|
||||||
|
timings.mark("packValidation");
|
||||||
addShutdownHook();
|
addShutdownHook();
|
||||||
pendingWorldReplacements.processPendingStartupReplacements();
|
pendingWorldReplacements.processPendingStartupReplacements();
|
||||||
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
pendingWorldDeletes.processPendingStartupWorldDeletes();
|
||||||
@@ -771,9 +788,51 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
// match a slice type; the block-state slice is deliberately never retainable (regenerable, huge).
|
// match a slice type; the block-state slice is deliberately never retainable (regenerable, huge).
|
||||||
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
|
IrisToolbelt.retainMantleDataForSlice(TreeBlockMaterial.class.getCanonicalName());
|
||||||
});
|
});
|
||||||
|
timings.mark("startupTasks");
|
||||||
|
timings.report();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Where the enable thread went, so a boot that got slower can be attributed to a phase rather than to
|
||||||
|
* Iris in general. The total is a lifecycle milestone and lands in logs/latest.log; the breakdown is
|
||||||
|
* for whoever is looking.
|
||||||
|
*/
|
||||||
|
private static final class EnableTimings {
|
||||||
|
private static final int REPORTED_SERVICES = 3;
|
||||||
|
|
||||||
|
private final long startedAt = System.nanoTime();
|
||||||
|
private final StringBuilder phases = new StringBuilder();
|
||||||
|
private final Map<String, Long> serviceMillis = new LinkedHashMap<>();
|
||||||
|
private long lastMark = startedAt;
|
||||||
|
|
||||||
|
private void mark(String phase) {
|
||||||
|
long now = System.nanoTime();
|
||||||
|
if (!phases.isEmpty()) {
|
||||||
|
phases.append(' ');
|
||||||
|
}
|
||||||
|
phases.append(phase).append('=').append(TimeUnit.NANOSECONDS.toMillis(now - lastMark)).append("ms");
|
||||||
|
lastMark = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markService(String service, long serviceStartedAt) {
|
||||||
|
serviceMillis.put(service, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - serviceStartedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void report() {
|
||||||
|
IrisLogging.notice("Enabled in %dms (%s)",
|
||||||
|
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startedAt), phases);
|
||||||
|
String slowest = serviceMillis.entrySet().stream()
|
||||||
|
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
|
||||||
|
.limit(REPORTED_SERVICES)
|
||||||
|
.map(entry -> entry.getKey() + "=" + entry.getValue() + "ms")
|
||||||
|
.collect(Collectors.joining(" "));
|
||||||
|
if (!slowest.isBlank()) {
|
||||||
|
IrisLogging.notice("Slowest services: %s", slowest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void reconcileStartupWorlds() {
|
private void reconcileStartupWorlds() {
|
||||||
generatorResolver.startupWorldsReady().whenComplete((ignored, failure) -> {
|
generatorResolver.startupWorldsReady().whenComplete((ignored, failure) -> {
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
|
|||||||
+23
-5
@@ -30,6 +30,7 @@ import art.arcane.iris.core.pack.PackValidationCache;
|
|||||||
import art.arcane.iris.core.pack.PackValidationRegistry;
|
import art.arcane.iris.core.pack.PackValidationRegistry;
|
||||||
import art.arcane.iris.core.pack.PackValidationResult;
|
import art.arcane.iris.core.pack.PackValidationResult;
|
||||||
import art.arcane.iris.core.pack.PackValidator;
|
import art.arcane.iris.core.pack.PackValidator;
|
||||||
|
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||||
import art.arcane.iris.core.service.ExternalDataSVC;
|
import art.arcane.iris.core.service.ExternalDataSVC;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.spi.IrisPlatforms;
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
@@ -486,17 +487,23 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
Iris.debug("Generator discovery probe for loaded world " + worldName);
|
Iris.debug("Generator discovery probe for loaded world " + worldName);
|
||||||
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
|
return IrisFailClosedChunkGenerator.discoveryProbe(worldName);
|
||||||
}
|
}
|
||||||
Optional<String> startupDenial = IrisStartupValidation.denialReason();
|
ChunkGenerator startupLock = startupLockedGenerator(worldName);
|
||||||
if (startupDenial.isPresent()) {
|
if (startupLock != null) {
|
||||||
Iris.warn("Keeping configured Iris world '" + worldName
|
return startupLock;
|
||||||
+ "' generation-locked: " + startupDenial.get());
|
|
||||||
return IrisFailClosedChunkGenerator.startupLock(worldName, startupDenial.get());
|
|
||||||
}
|
}
|
||||||
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
|
ChunkGenerator stagedGenerator = WorldLifecycleStaging.consumeGenerator(worldName);
|
||||||
if (stagedGenerator != null) {
|
if (stagedGenerator != null) {
|
||||||
Iris.debug("Using staged runtime generator for " + worldName);
|
Iris.debug("Using staged runtime generator for " + worldName);
|
||||||
return stagedGenerator;
|
return stagedGenerator;
|
||||||
}
|
}
|
||||||
|
// A world Iris created staged its generator above and installed the injection on the way in. This
|
||||||
|
// is a world configured outside Iris, and Bukkit asks for its generator before it builds the level,
|
||||||
|
// so this is the last point at which the injection it needs can still be installed in time.
|
||||||
|
RuntimeInjection.installIfDeferred();
|
||||||
|
startupLock = startupLockedGenerator(worldName);
|
||||||
|
if (startupLock != null) {
|
||||||
|
return startupLock;
|
||||||
|
}
|
||||||
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
Iris.debug("Default World Generator Called for " + worldName + " using ID: " + id);
|
||||||
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
if (id == null || id.isEmpty()) id = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||||
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
Iris.debug("Generator ID: " + id + " requested by bukkit/plugin");
|
||||||
@@ -516,6 +523,17 @@ public final class IrisWorldGeneratorResolver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static ChunkGenerator startupLockedGenerator(String worldName) {
|
||||||
|
Optional<String> startupDenial = IrisStartupValidation.denialReason();
|
||||||
|
if (startupDenial.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Iris.warn("Keeping configured Iris world '" + worldName
|
||||||
|
+ "' generation-locked: " + startupDenial.get());
|
||||||
|
return IrisFailClosedChunkGenerator.startupLock(worldName, startupDenial.get());
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean isPlotSquaredGeneratorDiscoveryProbe(String worldName, String id) {
|
private static boolean isPlotSquaredGeneratorDiscoveryProbe(String worldName, String id) {
|
||||||
return PLOT_SQUARED_DISCOVERY_WORLD.equals(worldName) && id != null && id.isEmpty();
|
return PLOT_SQUARED_DISCOVERY_WORLD.equals(worldName) && id != null && id.isEmpty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ public class WandSVC implements IrisService {
|
|||||||
private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30"));
|
private static final int MS_PER_TICK = Integer.parseInt(System.getProperty("iris.ms_per_tick", "30"));
|
||||||
private static final int PLAYER_RESCAN_INTERVAL_TICKS = 100;
|
private static final int PLAYER_RESCAN_INTERVAL_TICKS = 100;
|
||||||
|
|
||||||
private static ItemStack dust;
|
private static volatile ItemStack dust;
|
||||||
private static ItemStack wand;
|
private static volatile ItemStack wand;
|
||||||
|
|
||||||
private final Map<UUID, Player> activePlayers = new ConcurrentHashMap<>();
|
private final Map<UUID, Player> activePlayers = new ConcurrentHashMap<>();
|
||||||
private final AtomicBoolean playerRescanScheduled = new AtomicBoolean(false);
|
private final AtomicBoolean playerRescanScheduled = new AtomicBoolean(false);
|
||||||
@@ -374,12 +374,16 @@ public class WandSVC implements IrisService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
wand = createWand();
|
|
||||||
dust = createDust();
|
|
||||||
enabled = true;
|
enabled = true;
|
||||||
activePlayers.clear();
|
activePlayers.clear();
|
||||||
ticksUntilPlayerRescan = 0;
|
ticksUntilPlayerRescan = 0;
|
||||||
taskId = J.ar(this::tickAll, 1);
|
taskId = J.ar(this::tickAll, 1);
|
||||||
|
// Building the templates reaches the item and enchantment registries, which costs more than the
|
||||||
|
// rest of this service's startup put together and is only needed to recognise a pre-marker wand.
|
||||||
|
J.s(() -> {
|
||||||
|
wand = createWand();
|
||||||
|
dust = createDust();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -389,6 +393,8 @@ public class WandSVC implements IrisService {
|
|||||||
J.car(taskId);
|
J.car(taskId);
|
||||||
taskId = -1;
|
taskId = -1;
|
||||||
}
|
}
|
||||||
|
wand = null;
|
||||||
|
dust = null;
|
||||||
activePlayers.clear();
|
activePlayers.clear();
|
||||||
playerRescanScheduled.set(false);
|
playerRescanScheduled.set(false);
|
||||||
}
|
}
|
||||||
@@ -607,7 +613,8 @@ public class WandSVC implements IrisService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Byte marker = is.getItemMeta().getPersistentDataContainer().get(dustKey(), PersistentDataType.BYTE);
|
Byte marker = is.getItemMeta().getPersistentDataContainer().get(dustKey(), PersistentDataType.BYTE);
|
||||||
return (marker != null && marker == (byte) 1) || is.isSimilar(dust);
|
ItemStack template = dust;
|
||||||
|
return (marker != null && marker == (byte) 1) || (template != null && is.isSimilar(template));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static NamespacedKey wandKey() {
|
private static NamespacedKey wandKey() {
|
||||||
|
|||||||
@@ -308,6 +308,13 @@ public class IrisSettings {
|
|||||||
public boolean debug = false;
|
public boolean debug = false;
|
||||||
public boolean dumpMantleOnError = false;
|
public boolean dumpMantleOnError = false;
|
||||||
public boolean disableNMS = false;
|
public boolean disableNMS = false;
|
||||||
|
/**
|
||||||
|
* Attach the Java agent and install the server-code injection while Iris enables, instead of the
|
||||||
|
* first time a world Iris generates is about to load. Boot-time injection costs roughly a second
|
||||||
|
* on a server that may never load an Iris world; turn this on to pay it up front and have the
|
||||||
|
* startup safeguard verify it.
|
||||||
|
*/
|
||||||
|
public boolean eagerRuntimeInjection = false;
|
||||||
public boolean splashLogoStartup = true;
|
public boolean splashLogoStartup = true;
|
||||||
public boolean useConsoleCustomColors = true;
|
public boolean useConsoleCustomColors = true;
|
||||||
public boolean useCustomColorsIngame = true;
|
public boolean useCustomColorsIngame = true;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package art.arcane.iris.core.lifecycle;
|
|||||||
|
|
||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
import art.arcane.iris.core.localization.IrisLanguage;
|
import art.arcane.iris.core.localization.IrisLanguage;
|
||||||
|
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||||
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
import art.arcane.iris.core.runtime.WorldRuntimeControlService;
|
||||||
import art.arcane.iris.spi.IrisServices;
|
import art.arcane.iris.spi.IrisServices;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ public final class IrisRuntimeStatics {
|
|||||||
IrisServices.clear();
|
IrisServices.clear();
|
||||||
WorldLifecycleService.reset();
|
WorldLifecycleService.reset();
|
||||||
WorldRuntimeControlService.reset();
|
WorldRuntimeControlService.reset();
|
||||||
|
RuntimeInjection.reset();
|
||||||
ServerConfigurator.resetLoadedDatapackRuntime();
|
ServerConfigurator.resetLoadedDatapackRuntime();
|
||||||
IrisLanguage.shutdown();
|
IrisLanguage.shutdown();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.core.lifecycle;
|
package art.arcane.iris.core.lifecycle;
|
||||||
|
|
||||||
import art.arcane.iris.core.ServerConfigurator;
|
import art.arcane.iris.core.ServerConfigurator;
|
||||||
|
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.core.tools.IrisToolbelt;
|
import art.arcane.iris.core.tools.IrisToolbelt;
|
||||||
import art.arcane.iris.util.common.scheduling.J;
|
import art.arcane.iris.util.common.scheduling.J;
|
||||||
@@ -85,6 +86,7 @@ public final class WorldLifecycleService {
|
|||||||
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
public CompletableFuture<World> create(WorldLifecycleRequest request) {
|
||||||
WorldLifecycleBackend backend;
|
WorldLifecycleBackend backend;
|
||||||
try {
|
try {
|
||||||
|
RuntimeInjection.installIfDeferred();
|
||||||
if (request.callerKind() == WorldLifecycleCaller.FORCED_STUDIO) {
|
if (request.callerKind() == WorldLifecycleCaller.FORCED_STUDIO) {
|
||||||
ServerConfigurator.requireWorldCreationReady(true);
|
ServerConfigurator.requireWorldCreationReady(true);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.IrisStartupValidation;
|
||||||
|
import art.arcane.iris.core.nms.INMS;
|
||||||
|
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import art.arcane.iris.util.project.agent.Agent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaches the Java agent and installs the server-code injection Iris generation needs, once per boot.
|
||||||
|
* <p>
|
||||||
|
* Both steps are expensive - a self-attach to the running JVM, then three class retransformations - and
|
||||||
|
* neither is needed until a world that Iris generates is about to exist. A server that never loads an Iris
|
||||||
|
* world never pays for them. Whoever is about to produce such a world calls {@link #installIfDeferred()}
|
||||||
|
* first; it is idempotent, and a failure marks the runtime invalid so the same lock that guards a failed
|
||||||
|
* boot-time injection guards a failed deferred one.
|
||||||
|
*/
|
||||||
|
public final class RuntimeInjection {
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
|
||||||
|
private static volatile Outcome outcome;
|
||||||
|
|
||||||
|
private RuntimeInjection() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Failure {
|
||||||
|
NONE,
|
||||||
|
NO_BINDING,
|
||||||
|
AGENT,
|
||||||
|
INJECTION
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Outcome(boolean installed, Failure failure, String lockReason) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the install if it has not run yet and reports what happened. Never throws.
|
||||||
|
*/
|
||||||
|
public static Outcome install() {
|
||||||
|
Outcome current = outcome;
|
||||||
|
if (current != null) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
synchronized (LOCK) {
|
||||||
|
if (outcome != null) {
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
Outcome resolved = attemptInstall();
|
||||||
|
outcome = resolved;
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs on the deferred path and locks the runtime when that fails, so the caller's own readiness
|
||||||
|
* check refuses the world instead of loading it onto uninstrumented server code.
|
||||||
|
*/
|
||||||
|
public static void installIfDeferred() {
|
||||||
|
Outcome resolved = install();
|
||||||
|
if (resolved.installed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IrisLogging.error("Iris runtime injection is unavailable: " + resolved.lockReason());
|
||||||
|
IrisStartupValidation.markRuntimeInvalid(resolved.lockReason());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInstalled() {
|
||||||
|
Outcome current = outcome;
|
||||||
|
return current != null && current.installed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forgets the result so the next boot in this JVM installs against the binding it is running with.
|
||||||
|
*/
|
||||||
|
public static void reset() {
|
||||||
|
synchronized (LOCK) {
|
||||||
|
outcome = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static String agentLockReason() {
|
||||||
|
return "Iris Java agent is unavailable. Add -javaagent:" + Agent.AGENT_JAR.getPath()
|
||||||
|
+ " to the JVM arguments before -jar and restart the server.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Outcome attemptInstall() {
|
||||||
|
if (!INMS.isBound() || INMS.get() instanceof NMSBinding1X) {
|
||||||
|
return new Outcome(false, Failure.NO_BINDING,
|
||||||
|
"Iris cannot use this server's NMS runtime. Resolve the Server Version or NMS Disabled error above and restart the server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Agent.install()) {
|
||||||
|
return new Outcome(false, Failure.AGENT, agentLockReason());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!INMS.get().injectBukkit()) {
|
||||||
|
return new Outcome(false, Failure.INJECTION,
|
||||||
|
"Iris runtime injection failed. Resolve the startup errors and restart the server.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Outcome(true, Failure.NONE, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package art.arcane.iris.core.safeguard.task;
|
|||||||
|
|
||||||
import art.arcane.iris.BuildConstants;
|
import art.arcane.iris.BuildConstants;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
import art.arcane.iris.core.IrisWorlds;
|
import art.arcane.iris.core.IrisWorlds;
|
||||||
|
import art.arcane.iris.core.runtime.RuntimeInjection;
|
||||||
import art.arcane.iris.core.IrisWorldStorage;
|
import art.arcane.iris.core.IrisWorldStorage;
|
||||||
import art.arcane.iris.core.nms.INMS;
|
import art.arcane.iris.core.nms.INMS;
|
||||||
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
import art.arcane.iris.core.nms.v1X.NMSBinding1X;
|
||||||
@@ -128,28 +130,29 @@ public final class Tasks {
|
|||||||
});
|
});
|
||||||
|
|
||||||
private static final Task INJECTION = Task.critical("injection", INJECTION_LOCK_REASON, () -> {
|
private static final Task INJECTION = Task.critical("injection", INJECTION_LOCK_REASON, () -> {
|
||||||
if (!INMS.isBound() || INMS.get() instanceof NMSBinding1X) {
|
if (!IrisSettings.get().getGeneral().isEagerRuntimeInjection()) {
|
||||||
return CheckResult.danger(NMS_LOCK_REASON,
|
return CheckResult.stable(
|
||||||
Diagnostic.Logger.ERROR.create("Code Injection"),
|
Diagnostic.Logger.INFO.create("Runtime Injection"),
|
||||||
Diagnostic.Logger.ERROR.create("- Runtime injection was skipped because Iris has no usable NMS binding."));
|
Diagnostic.Logger.INFO.create("- Deferred to the first world load. Set general.eagerRuntimeInjection to verify it during startup."));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Agent.install()) {
|
RuntimeInjection.Outcome outcome = RuntimeInjection.install();
|
||||||
String agentLockReason = "Iris Java agent is unavailable. Add -javaagent:"
|
if (outcome.installed()) {
|
||||||
+ Agent.AGENT_JAR.getPath() + " to the JVM arguments before -jar and restart the server.";
|
return CheckResult.stable();
|
||||||
return CheckResult.danger(agentLockReason,
|
}
|
||||||
|
|
||||||
|
return switch (outcome.failure()) {
|
||||||
|
case NO_BINDING -> CheckResult.danger(outcome.lockReason(),
|
||||||
|
Diagnostic.Logger.ERROR.create("Code Injection"),
|
||||||
|
Diagnostic.Logger.ERROR.create("- Runtime injection was skipped because Iris has no usable NMS binding."));
|
||||||
|
case AGENT -> CheckResult.danger(outcome.lockReason(),
|
||||||
Diagnostic.Logger.ERROR.create("Java Agent"),
|
Diagnostic.Logger.ERROR.create("Java Agent"),
|
||||||
Diagnostic.Logger.ERROR.create("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."),
|
Diagnostic.Logger.ERROR.create("- Add -javaagent:" + Agent.AGENT_JAR.getPath() + " before -jar in your startup command and restart the server."),
|
||||||
Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment."));
|
Diagnostic.Logger.ERROR.create("- Dynamic attachment requires -XX:+EnableDynamicAgentLoading and a host that permits JVM attachment."));
|
||||||
}
|
case INJECTION, NONE -> CheckResult.danger(INJECTION_LOCK_REASON,
|
||||||
|
|
||||||
if (!INMS.get().injectBukkit()) {
|
|
||||||
return CheckResult.danger(INJECTION_LOCK_REASON,
|
|
||||||
Diagnostic.Logger.ERROR.create("Code Injection"),
|
Diagnostic.Logger.ERROR.create("Code Injection"),
|
||||||
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
|
Diagnostic.Logger.ERROR.create("- Failed to inject code. Please contact support"));
|
||||||
}
|
};
|
||||||
|
|
||||||
return CheckResult.stable();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
private static final Task DIMENSION_TYPES = Task.critical("dimensionTypes", DIMENSION_TYPE_LOCK_REASON, () -> {
|
private static final Task DIMENSION_TYPES = Task.critical("dimensionTypes", DIMENSION_TYPE_LOCK_REASON, () -> {
|
||||||
|
|||||||
@@ -90,8 +90,10 @@ import java.util.Objects;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -106,23 +108,54 @@ public class StudioSVC implements IrisService {
|
|||||||
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
private static final Pattern PROJECT_NAME = Pattern.compile("[a-z0-9_-]+");
|
||||||
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
private static final AtomicCache<Integer> counter = new AtomicCache<>();
|
||||||
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
private final StudioTransitionQueue studioTransitions = new StudioTransitionQueue();
|
||||||
|
private static final long STALE_TEMP_CLEANUP_TIMEOUT_SECONDS = 120L;
|
||||||
|
private static volatile CompletableFuture<Void> staleTempCleanup = CompletableFuture.completedFuture(null);
|
||||||
|
|
||||||
private final Object downloadAdmissionMonitor = new Object();
|
private final Object downloadAdmissionMonitor = new Object();
|
||||||
private volatile IrisProject activeProject;
|
private volatile IrisProject activeProject;
|
||||||
private volatile CompletableFuture<StudioOpenCoordinator.StudioOpenResult> activeOpen;
|
private volatile CompletableFuture<StudioOpenCoordinator.StudioOpenResult> activeOpen;
|
||||||
private PackDownloadExecution activeDownload;
|
private PackDownloadExecution activeDownload;
|
||||||
private boolean downloadAdmissionOpen;
|
private boolean downloadAdmissionOpen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds the stale cache/temp sweep a boot starts in the background. Pack imports stage through that
|
||||||
|
* folder, so a delete running underneath one truncates it; the import waits the sweep out instead of
|
||||||
|
* the boot waiting for the sweep.
|
||||||
|
*/
|
||||||
|
public static void gateDownloadsOnStaleTempCleanup(CompletableFuture<Void> cleanup) {
|
||||||
|
staleTempCleanup = cleanup == null ? CompletableFuture.completedFuture(null) : cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void awaitStaleTempCleanup() {
|
||||||
|
CompletableFuture<Void> cleanup = staleTempCleanup;
|
||||||
|
if (cleanup.isDone()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cleanup.get(STALE_TEMP_CLEANUP_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
} catch (InterruptedException interrupted) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
IrisLogging.reportError("Interrupted while waiting for the stale Iris temp sweep to finish.", interrupted);
|
||||||
|
} catch (ExecutionException | TimeoutException failure) {
|
||||||
|
IrisLogging.reportError("The stale Iris temp sweep did not finish; this pack import may race it.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
synchronized (downloadAdmissionMonitor) {
|
synchronized (downloadAdmissionMonitor) {
|
||||||
activeDownload = null;
|
activeDownload = null;
|
||||||
downloadAdmissionOpen = true;
|
downloadAdmissionOpen = true;
|
||||||
}
|
}
|
||||||
String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType();
|
MultiBurst.ioBurst.completeValueAsync(() -> {
|
||||||
if (!PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) {
|
String configuredPack = IrisSettings.get().getGenerator().getDefaultWorldType();
|
||||||
IrisLogging.warn("Default pack '" + configuredPack
|
if (!PackDownloader.isPackPresent(getWorkspaceFolder(), configuredPack)) {
|
||||||
+ "' is not installed. Install a built-in pack with /iris download pack=overworld or provide /iris download link=<zip-url>.");
|
IrisLogging.warn("Default pack '" + configuredPack
|
||||||
}
|
+ "' is not installed. Install a built-in pack with /iris download pack=overworld or provide /iris download link=<zip-url>.");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1274,7 +1307,10 @@ public class StudioSVC implements IrisService {
|
|||||||
|
|
||||||
execution = new PackDownloadExecution(
|
execution = new PackDownloadExecution(
|
||||||
lease,
|
lease,
|
||||||
cancellation -> executePackMutation(mutation, reporter, failureMessage, cancellation)
|
cancellation -> {
|
||||||
|
awaitStaleTempCleanup();
|
||||||
|
executePackMutation(mutation, reporter, failureMessage, cancellation);
|
||||||
|
}
|
||||||
);
|
);
|
||||||
PackDownloadExecution trackedExecution = execution;
|
PackDownloadExecution trackedExecution = execution;
|
||||||
execution.onCompletion(() -> {
|
execution.onCompletion(() -> {
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package art.arcane.iris.core.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.IrisStartupValidation;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attaching the agent and retransforming server classes costs about a second of every boot, and a server
|
||||||
|
* that never loads an Iris world never needs either. Deferring it only stays safe while a deferred failure
|
||||||
|
* locks the runtime exactly like a boot-time one did, so the world it was about to generate is refused
|
||||||
|
* rather than handed to uninstrumented server code.
|
||||||
|
*/
|
||||||
|
public class RuntimeInjectionTest {
|
||||||
|
@Before
|
||||||
|
public void resetInjection() {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
RuntimeInjection.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void clearValidation() {
|
||||||
|
RuntimeInjection.reset();
|
||||||
|
IrisStartupValidation.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anInstallWithoutAUsableNmsBindingIsRefusedAndNamesTheReason() {
|
||||||
|
RuntimeInjection.Outcome outcome = RuntimeInjection.install();
|
||||||
|
|
||||||
|
assertFalse(outcome.installed());
|
||||||
|
assertEquals(RuntimeInjection.Failure.NO_BINDING, outcome.failure());
|
||||||
|
assertTrue(outcome.lockReason(), outcome.lockReason().contains("NMS runtime"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theOutcomeIsResolvedOnceUntilReset() {
|
||||||
|
RuntimeInjection.Outcome first = RuntimeInjection.install();
|
||||||
|
|
||||||
|
assertSame(first, RuntimeInjection.install());
|
||||||
|
|
||||||
|
RuntimeInjection.reset();
|
||||||
|
assertFalse(RuntimeInjection.install() == first);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aDeferredInstallFailureLocksTheRuntimeWithItsReason() {
|
||||||
|
IrisStartupValidation.begin();
|
||||||
|
IrisStartupValidation.markDatapacksReady();
|
||||||
|
IrisStartupValidation.markPacksReady();
|
||||||
|
IrisStartupValidation.markRuntimeReady();
|
||||||
|
assertTrue(IrisStartupValidation.isReady());
|
||||||
|
|
||||||
|
RuntimeInjection.installIfDeferred();
|
||||||
|
|
||||||
|
assertFalse(IrisStartupValidation.isReady());
|
||||||
|
assertEquals(RuntimeInjection.install().lockReason(),
|
||||||
|
IrisStartupValidation.denialReason().orElseThrow());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void anUninstalledRuntimeReportsItself() {
|
||||||
|
RuntimeInjection.install();
|
||||||
|
|
||||||
|
assertFalse(RuntimeInjection.isInstalled());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package art.arcane.iris.core.safeguard;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.IrisSettings;
|
||||||
|
import art.arcane.iris.core.safeguard.task.CheckResult;
|
||||||
|
import art.arcane.iris.core.safeguard.task.Diagnostic;
|
||||||
|
import art.arcane.iris.core.safeguard.task.Task;
|
||||||
|
import art.arcane.iris.core.safeguard.task.Tasks;
|
||||||
|
import art.arcane.iris.spi.IrisPlatform;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.testsupport.BukkitTestServer;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With injection deferred the injection check has nothing to verify yet, so it must say so and stay Stable
|
||||||
|
* rather than reporting a Danger the server has not actually hit. Turning the setting on brings the boot
|
||||||
|
* verification back.
|
||||||
|
*/
|
||||||
|
public class TasksInjectionDeferralTest {
|
||||||
|
private IrisPlatform previousPlatform;
|
||||||
|
private IrisSettings previousSettings;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void installServerAndSettings() {
|
||||||
|
BukkitTestServer.install();
|
||||||
|
previousPlatform = IrisPlatforms.isBound() ? IrisPlatforms.get() : null;
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
IrisPlatforms.bind(mock(IrisPlatform.class));
|
||||||
|
previousSettings = IrisSettings.settings;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void restore() {
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
IrisPlatforms.unbind();
|
||||||
|
if (previousPlatform != null) {
|
||||||
|
IrisPlatforms.bind(previousPlatform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void injectionIsDeferredByDefaultAndSaysSo() {
|
||||||
|
assertTrue("deferred injection is the default", !IrisSettings.settings.getGeneral().isEagerRuntimeInjection());
|
||||||
|
|
||||||
|
CheckResult result = injection().run();
|
||||||
|
|
||||||
|
assertEquals(Mode.STABLE, result.mode());
|
||||||
|
List<String> lines = result.diagnostics().stream().map(Diagnostic::getMessage).toList();
|
||||||
|
assertTrue(lines.toString(), lines.contains("Runtime Injection"));
|
||||||
|
assertTrue(lines.toString(), lines.stream().anyMatch(line -> line.contains("general.eagerRuntimeInjection")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void theEagerSettingBringsBackBootTimeVerification() {
|
||||||
|
IrisSettings.settings.getGeneral().setEagerRuntimeInjection(true);
|
||||||
|
|
||||||
|
CheckResult result = injection().run();
|
||||||
|
|
||||||
|
assertEquals(Mode.UNSTABLE, result.mode());
|
||||||
|
assertTrue(result.lockReason(), result.lockReason().contains("NMS runtime"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Task injection() {
|
||||||
|
for (Task task : Tasks.getTasks()) {
|
||||||
|
if ("injection".equals(task.getId())) {
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("The injection check is missing from the safeguard task list");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user