From b357eee181a047d4185c79739f02452e62f776b7 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Sun, 18 Oct 2026 18:15:46 -0400 Subject: [PATCH] Fixed runtime configuration - Timeout overrides retain original values for restoration - Timeout restoration preserves operator changes and symbolic links - Tests cover timeout persistence and failed configuration writes - Runtime targets use separate Gradle caches and shared module outputs - Modded build inputs include test fixtures and runtime cache settings --- adapters/fabric/settings.gradle | 2 + adapters/forge/settings.gradle | 2 + adapters/neoforge/settings.gradle | 2 + build.gradle | 7 +- .../pack/datapack/ServerConfigurator.java | 166 ++++++++++++++-- .../ServerConfiguratorTimeoutTest.java | 19 +- .../ServerTimeoutConfigurationTest.java | 182 ++++++++++++++++++ .../modded-generation-build-revision.gradle | 2 +- gradle/runtime-cache.settings.gradle | 23 +++ settings.gradle | 1 + 10 files changed, 380 insertions(+), 26 deletions(-) create mode 100644 core/src/test/java/art/arcane/iris/pack/datapack/ServerTimeoutConfigurationTest.java create mode 100644 gradle/runtime-cache.settings.gradle diff --git a/adapters/fabric/settings.gradle b/adapters/fabric/settings.gradle index c46bcfa7c..0f0967615 100644 --- a/adapters/fabric/settings.gradle +++ b/adapters/fabric/settings.gradle @@ -33,6 +33,8 @@ plugins { rootProject.name = 'iris-fabric' +apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile + dependencyResolutionManagement { versionCatalogs { libs { diff --git a/adapters/forge/settings.gradle b/adapters/forge/settings.gradle index d24d880c0..16facc601 100644 --- a/adapters/forge/settings.gradle +++ b/adapters/forge/settings.gradle @@ -33,6 +33,8 @@ plugins { rootProject.name = 'iris-forge' +apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile + dependencyResolutionManagement { versionCatalogs { libs { diff --git a/adapters/neoforge/settings.gradle b/adapters/neoforge/settings.gradle index e8ebcffd5..5661790d2 100644 --- a/adapters/neoforge/settings.gradle +++ b/adapters/neoforge/settings.gradle @@ -33,6 +33,8 @@ plugins { rootProject.name = 'iris-neoforge' +apply from: new File(settingsDir, '../../gradle/runtime-cache.settings.gradle').canonicalFile + dependencyResolutionManagement { versionCatalogs { libs { diff --git a/build.gradle b/build.gradle index 8f6c80e23..0af6e27a1 100644 --- a/build.gradle +++ b/build.gradle @@ -310,6 +310,7 @@ List moddedSharedInputs = [] 'adapters/modded-common/src', 'adapters/client-common/src', 'core/src/main', + 'core/src/testFixtures', 'core/build.gradle', 'core/agent/src', 'core/agent/build.gradle', @@ -327,6 +328,7 @@ List moddedSharedInputs = [] 'gradle/native-minecraft-sources.gradle', 'gradle/mod-target.gradle', 'gradle/mod-targets.json', + 'gradle/runtime-cache.settings.gradle', 'gradle/modded-version-sources.gradle' ].each { String path -> moddedSharedInputs.add(layout.projectDirectory.file(path).asFile) } if (localVolmLibCheckout != null) { @@ -688,12 +690,13 @@ if (!(irisRuntimeJava in [21, 25])) { throw new GradleException('irisRuntimeJava must be 21 or 25') } Set sharedRuntimeProjects = [':core', ':core:agent', ':spi'] as Set +String runtimeOutputVariant = gradle.extensions.extraProperties.get('irisRuntimeOutputVariant').toString() allprojects { apply plugin: 'java' boolean java21Runtime = irisRuntimeJava == 21 && sharedRuntimeProjects.contains(project.path) - if (java21Runtime) { - layout.buildDirectory.set(layout.projectDirectory.dir('build/java21')) + if (!runtimeOutputVariant.isEmpty() && sharedRuntimeProjects.contains(project.path)) { + layout.buildDirectory.set(layout.projectDirectory.dir("build/${runtimeOutputVariant}")) } tasks.withType(Test).configureEach { diff --git a/core/src/main/java/art/arcane/iris/pack/datapack/ServerConfigurator.java b/core/src/main/java/art/arcane/iris/pack/datapack/ServerConfigurator.java index db13d866d..d697bea0e 100644 --- a/core/src/main/java/art/arcane/iris/pack/datapack/ServerConfigurator.java +++ b/core/src/main/java/art/arcane/iris/pack/datapack/ServerConfigurator.java @@ -45,6 +45,9 @@ import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.io.AtomicFileIO; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; import art.arcane.iris.platform.bootstrap.ServerProperties; import art.arcane.iris.platform.bukkit.plugin.VolmitSender; import art.arcane.iris.world.task.J; @@ -104,6 +107,12 @@ public class ServerConfigurator { if (failure != null) { IrisLogging.reportError("Unable to persist the Spigot timeout setting.", failure); } + } else { + Throwable failure = J.attemptCatch(() -> timeoutConfiguration().restore( + "settings.timeout-time", serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig())); + if (failure != null) { + IrisLogging.reportError("Unable to restore the Spigot timeout setting.", failure); + } } if (s.isConfigurePaperWatchdogDelay() && BukkitPlatform.isPaperServer()) { @@ -111,6 +120,12 @@ public class ServerConfigurator { if (failure != null) { IrisLogging.reportError("Unable to persist the Paper watchdog setting.", failure); } + } else if (!s.isConfigurePaperWatchdogDelay()) { + Throwable failure = J.attemptCatch(() -> timeoutConfiguration().restore( + "watchdog.early-warning-delay", null, null)); + if (failure != null) { + IrisLogging.reportError("Unable to restore the Paper watchdog setting.", failure); + } } if (DefaultPackBootstrapProvisioner.wasProvisionedThisStartup()) { @@ -238,26 +253,26 @@ public class ServerConfigurator { } private static void increaseKeepAliveSpigot() throws IOException, ReflectiveOperationException { - increaseKeepAliveSpigot(serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig()); + increaseKeepAliveSpigot(serverConfigurationOption("spigot-settings"), Bukkit.spigot().getConfig(), timeoutConfiguration()); } - static void increaseKeepAliveSpigot(Path file, YamlConfiguration configuration) throws IOException { + static void increaseKeepAliveSpigot(Path file, YamlConfiguration configuration, ServerTimeoutConfiguration timeouts) throws IOException { long previous = configuration.getLong("settings.timeout-time"); long minimum = TimeUnit.MINUTES.toSeconds(5); if (previous >= minimum) { return; } - persistTimeout(file, configuration, "settings.timeout-time", minimum); + timeouts.raise(file, configuration, "settings.timeout-time", minimum); IrisLogging.warn("Updated " + file + " settings.timeout-time: " + previous + " -> " + minimum + " seconds. Takes effect after a server restart."); IrisLogging.warn("Disable autoConfiguration.configureSpigotTimeoutTime in iris.json to manage this setting yourself."); } private static void increasePaperWatchdog() throws IOException, InvalidConfigurationException, ReflectiveOperationException { - increasePaperWatchdog(serverConfigurationOption("paper-settings-directory").resolve("paper-global.yml")); + increasePaperWatchdog(serverConfigurationOption("paper-settings-directory").resolve("paper-global.yml"), timeoutConfiguration()); } - static void increasePaperWatchdog(Path file) throws IOException, InvalidConfigurationException { + static void increasePaperWatchdog(Path file, ServerTimeoutConfiguration timeouts) throws IOException, InvalidConfigurationException { YamlConfiguration configuration = new YamlConfiguration(); configuration.load(file.toFile()); long previous = configuration.getLong("watchdog.early-warning-delay"); @@ -265,22 +280,14 @@ public class ServerConfigurator { if (previous >= minimum) { return; } - persistTimeout(file, configuration, "watchdog.early-warning-delay", minimum); + timeouts.raise(file, configuration, "watchdog.early-warning-delay", minimum); IrisLogging.warn("Updated " + file + " watchdog.early-warning-delay: " + previous + " -> " + minimum + " milliseconds. Takes effect after a server restart."); IrisLogging.warn("Disable autoConfiguration.configurePaperWatchdogDelay in iris.json to manage this setting yourself."); } - private static void persistTimeout(Path file, YamlConfiguration configuration, String key, long minimum) throws IOException { - Path target = file.toRealPath(); - Object previous = configuration.isSet(key) ? configuration.get(key) : null; - configuration.set(key, minimum); - try { - AtomicFileIO.writeString(target, configuration.saveToString()); - } catch (IOException | RuntimeException failure) { - configuration.set(key, previous); - throw failure; - } + private static ServerTimeoutConfiguration timeoutConfiguration() { + return new ServerTimeoutConfiguration(IrisPlatforms.get().dataFolder().toPath().resolve("server-timeouts.json")); } static Path serverConfigurationOption(String name) throws ReflectiveOperationException, IOException { @@ -964,4 +971,131 @@ public class ServerConfigurator { } } + static final class ServerTimeoutConfiguration { + private static final Object MUTATION_LOCK = new Object(); + private static final Gson JSON = new GsonBuilder().setPrettyPrinting().serializeNulls().create(); + + private final Path recoveryFile; + + ServerTimeoutConfiguration(Path recoveryFile) { + this.recoveryFile = Objects.requireNonNull(recoveryFile); + } + + void raise(Path file, YamlConfiguration configuration, String key, long minimum) throws IOException { + synchronized (MUTATION_LOCK) { + Path target = file.toRealPath(); + YamlConfiguration persisted = load(target); + Long original = value(persisted, key); + List changes = readChanges(); + SavedTimeout saved = find(changes, target, key); + if (saved == null || !Objects.equals(original, saved.original()) && !Objects.equals(original, saved.applied())) { + changes.remove(saved); + changes.add(new SavedTimeout(target.toString(), key, original, minimum)); + writeChanges(changes); + } + Object previous = configuration.isSet(key) ? configuration.get(key) : null; + configuration.set(key, minimum); + try { + AtomicFileIO.writeString(target, configuration.saveToString()); + } catch (IOException | RuntimeException failure) { + configuration.set(key, previous); + throw failure; + } + } + } + + void restore(String key, Path liveFile, YamlConfiguration live) throws IOException { + synchronized (MUTATION_LOCK) { + List changes = readChanges(); + for (SavedTimeout saved : List.copyOf(changes)) { + if (!saved.key().equals(key)) { + continue; + } + Path target = Path.of(saved.file()); + YamlConfiguration configuration = load(target); + if (Objects.equals(value(configuration, key), saved.applied())) { + configuration.set(key, saved.original()); + AtomicFileIO.writeString(target, configuration.saveToString()); + if (live != null && target.equals(liveFile.toRealPath()) + && Objects.equals(value(live, key), saved.applied())) { + live.set(key, saved.original()); + } + IrisLogging.info("Restored " + target + " " + key + " to " + + (saved.original() == null ? "its server default" : saved.original()) + + ". Takes effect after a server restart."); + } else { + IrisLogging.info("Kept the current " + target + " " + key + + " because it differs from the value saved by Iris."); + } + changes.remove(saved); + writeChanges(changes); + } + } + } + + private List readChanges() throws IOException { + if (!Files.exists(recoveryFile)) { + return new ArrayList<>(); + } + try { + SavedTimeouts saved = JSON.fromJson(Files.readString(recoveryFile), SavedTimeouts.class); + if (saved == null) { + throw new IOException("Empty server timeout recovery data: " + recoveryFile); + } + return new ArrayList<>(saved.changes()); + } catch (JsonParseException | IllegalArgumentException exception) { + throw new IOException("Invalid server timeout recovery data: " + recoveryFile, exception); + } + } + + private void writeChanges(List changes) throws IOException { + AtomicFileIO.writeString(recoveryFile, JSON.toJson(new SavedTimeouts(changes))); + } + + private static SavedTimeout find(List changes, Path target, String key) { + for (SavedTimeout change : changes) { + if (change.file().equals(target.toString()) && change.key().equals(key)) { + return change; + } + } + return null; + } + + private static YamlConfiguration load(Path file) throws IOException { + YamlConfiguration configuration = new YamlConfiguration(); + try { + configuration.load(file.toFile()); + } catch (InvalidConfigurationException exception) { + throw new IOException("Invalid server configuration: " + file, exception); + } + return configuration; + } + + private static Long value(YamlConfiguration configuration, String key) throws IOException { + if (!configuration.isSet(key)) { + return null; + } + if (!configuration.isInt(key) && !configuration.isLong(key)) { + throw new IOException("Server timeout " + key + " must be an integer."); + } + return configuration.getLong(key); + } + + private record SavedTimeout(String file, String key, Long original, long applied) { + private SavedTimeout { + Objects.requireNonNull(file, "file"); + Objects.requireNonNull(key, "key"); + if (!Path.of(file).isAbsolute() || applied <= 0 + || !key.equals("settings.timeout-time") && !key.equals("watchdog.early-warning-delay")) { + throw new IllegalArgumentException("Invalid saved server timeout"); + } + } + } + + private record SavedTimeouts(List changes) { + private SavedTimeouts { + changes = List.copyOf(changes); + } + } + } } diff --git a/core/src/test/java/art/arcane/iris/pack/datapack/ServerConfiguratorTimeoutTest.java b/core/src/test/java/art/arcane/iris/pack/datapack/ServerConfiguratorTimeoutTest.java index ce49e303f..21ba84a4a 100644 --- a/core/src/test/java/art/arcane/iris/pack/datapack/ServerConfiguratorTimeoutTest.java +++ b/core/src/test/java/art/arcane/iris/pack/datapack/ServerConfiguratorTimeoutTest.java @@ -1,5 +1,6 @@ package art.arcane.iris.pack.datapack; +import art.arcane.iris.pack.datapack.ServerConfigurator.ServerTimeoutConfiguration; import art.arcane.iris.spi.IrisLogging; import org.bukkit.Bukkit; import org.bukkit.Server; @@ -41,7 +42,7 @@ public class ServerConfiguratorTimeoutTest { return null; }); - ServerConfigurator.increaseKeepAliveSpigot(file, live); + ServerConfigurator.increaseKeepAliveSpigot(file, live, timeouts()); assertEquals(300L, live.getLong("settings.timeout-time")); live.set("world-settings.default.sample-world-setting", true); @@ -62,7 +63,7 @@ public class ServerConfiguratorTimeoutTest { YamlConfiguration live = load(file); try (MockedStatic logging = mockStatic(IrisLogging.class)) { - ServerConfigurator.increaseKeepAliveSpigot(file, live); + ServerConfigurator.increaseKeepAliveSpigot(file, live, timeouts()); assertEquals(600L, live.getLong("settings.timeout-time")); assertEquals(original, Files.readString(file)); @@ -81,7 +82,7 @@ public class ServerConfiguratorTimeoutTest { YamlConfiguration live = load(link); try (MockedStatic logging = mockStatic(IrisLogging.class)) { - ServerConfigurator.increaseKeepAliveSpigot(link, live); + ServerConfigurator.increaseKeepAliveSpigot(link, live, timeouts()); assertTrue(Files.isSymbolicLink(link)); assertEquals(relativeTarget, Files.readSymbolicLink(link)); @@ -99,7 +100,7 @@ public class ServerConfiguratorTimeoutTest { + "misc:\n sample-setting: retained\n"); try (MockedStatic logging = mockStatic(IrisLogging.class)) { - ServerConfigurator.increasePaperWatchdog(file); + ServerConfigurator.increasePaperWatchdog(file, timeouts()); YamlConfiguration persisted = load(file); assertEquals(180000L, persisted.getLong("watchdog.early-warning-delay")); @@ -117,7 +118,7 @@ public class ServerConfiguratorTimeoutTest { Files.writeString(file, original); try (MockedStatic logging = mockStatic(IrisLogging.class)) { - ServerConfigurator.increasePaperWatchdog(file); + ServerConfigurator.increasePaperWatchdog(file, timeouts()); assertEquals(original, Files.readString(file)); logging.verifyNoInteractions(); @@ -131,7 +132,7 @@ public class ServerConfiguratorTimeoutTest { Files.writeString(file, malformed); try (MockedStatic logging = mockStatic(IrisLogging.class)) { - assertThrows(InvalidConfigurationException.class, () -> ServerConfigurator.increasePaperWatchdog(file)); + assertThrows(InvalidConfigurationException.class, () -> ServerConfigurator.increasePaperWatchdog(file, timeouts())); assertEquals(malformed, Files.readString(file)); logging.verifyNoInteractions(); @@ -148,7 +149,7 @@ public class ServerConfiguratorTimeoutTest { try (MockedStatic logging = mockStatic(IrisLogging.class)) { assertThrows(IOException.class, () -> ServerConfigurator.increaseKeepAliveSpigot( - blockedParent.resolve("spigot.yml"), live)); + blockedParent.resolve("spigot.yml"), live, timeouts())); assertEquals(60L, live.getLong("settings.timeout-time")); assertEquals("retained", live.getString("settings.sample-setting")); @@ -175,6 +176,10 @@ public class ServerConfiguratorTimeoutTest { } } + private ServerTimeoutConfiguration timeouts() { + return new ServerTimeoutConfiguration(temporaryFolder.getRoot().toPath().resolve("server-timeouts.json")); + } + private static YamlConfiguration load(Path file) throws IOException, InvalidConfigurationException { YamlConfiguration configuration = new YamlConfiguration(); configuration.load(file.toFile()); diff --git a/core/src/test/java/art/arcane/iris/pack/datapack/ServerTimeoutConfigurationTest.java b/core/src/test/java/art/arcane/iris/pack/datapack/ServerTimeoutConfigurationTest.java new file mode 100644 index 000000000..d743f997e --- /dev/null +++ b/core/src/test/java/art/arcane/iris/pack/datapack/ServerTimeoutConfigurationTest.java @@ -0,0 +1,182 @@ +package art.arcane.iris.pack.datapack; + +import art.arcane.iris.pack.datapack.ServerConfigurator.ServerTimeoutConfiguration; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.volmlib.util.io.AtomicFileIO; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.MockedStatic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.mockStatic; + +public class ServerTimeoutConfigurationTest { + private static final String SPIGOT_KEY = "settings.timeout-time"; + private static final String PAPER_KEY = "watchdog.early-warning-delay"; + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void originalValuesSurviveRestartsAndRestoreIndependently() throws Exception { + Path spigot = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + Path paper = configuration("paper-global.yml", "watchdog:\n early-warning-delay: 10000\n early-warning-every: 5000\n"); + try (MockedStatic logging = mockStatic(IrisLogging.class)) { + ServerConfigurator.increaseKeepAliveSpigot(spigot, load(spigot), timeouts()); + ServerConfigurator.increasePaperWatchdog(paper, timeouts()); + ServerConfigurator.increaseKeepAliveSpigot(spigot, load(spigot), timeouts()); + YamlConfiguration live = load(spigot); + timeouts().restore(SPIGOT_KEY, spigot, live); + assertEquals(60L, load(spigot).getLong(SPIGOT_KEY)); + assertEquals(60L, live.getLong(SPIGOT_KEY)); + assertEquals(180000L, load(paper).getLong(PAPER_KEY)); + live.set("settings.unrelated", true); + live.save(spigot.toFile()); + assertEquals(60L, load(spigot).getLong(SPIGOT_KEY)); + timeouts().restore(PAPER_KEY, null, null); + assertEquals(10000L, load(paper).getLong(PAPER_KEY)); + assertEquals(5000L, load(paper).getLong("watchdog.early-warning-every")); + assertFalse(Files.readString(recoveryFile()).contains("spigot.yml")); + assertFalse(Files.readString(recoveryFile()).contains("paper-global.yml")); + } + } + + @Test + public void restoringAbsentSettingRemovesTheOverride() throws Exception { + Path file = configuration("spigot.yml", "settings:\n unrelated: retained\n"); + YamlConfiguration live = load(file); + try (MockedStatic logging = mockStatic(IrisLogging.class)) { + ServerConfigurator.increaseKeepAliveSpigot(file, live, timeouts()); + timeouts().restore(SPIGOT_KEY, file, live); + assertFalse(load(file).isSet(SPIGOT_KEY)); + assertFalse(live.isSet(SPIGOT_KEY)); + assertEquals("retained", load(file).getString("settings.unrelated")); + } + } + + @Test + public void restorePreservesOperatorChangesAndOtherSettings() throws Exception { + Path file = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + try (MockedStatic logging = mockStatic(IrisLogging.class)) { + ServerConfigurator.increaseKeepAliveSpigot(file, load(file), timeouts()); + Files.writeString(file, "# Operator settings\nsettings:\n timeout-time: 90\n unrelated: retained\n"); + String changed = Files.readString(file); + timeouts().restore(SPIGOT_KEY, file, load(file)); + assertEquals(changed, Files.readString(file)); + ServerConfigurator.increaseKeepAliveSpigot(file, load(file), timeouts()); + timeouts().restore(SPIGOT_KEY, file, load(file)); + assertEquals(90L, load(file).getLong(SPIGOT_KEY)); + assertEquals("retained", load(file).getString("settings.unrelated")); + } + } + + @Test + public void symlinkSurvivesRaiseAndRestore() throws Exception { + Path target = configuration("shared-spigot.yml", "settings:\n timeout-time: 60\n"); + Path link = temporaryFolder.getRoot().toPath().resolve("spigot.yml"); + Files.createSymbolicLink(link, target.getFileName()); + try (MockedStatic logging = mockStatic(IrisLogging.class)) { + ServerConfigurator.increaseKeepAliveSpigot(link, load(link), timeouts()); + timeouts().restore(SPIGOT_KEY, link, load(link)); + assertTrue(Files.isSymbolicLink(link)); + assertEquals(60L, load(target).getLong(SPIGOT_KEY)); + } + } + + @Test + public void aLaterAdjustmentRecordsTheOperatorsNewOriginal() throws Exception { + Path file = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + try (MockedStatic logging = mockStatic(IrisLogging.class)) { + ServerConfigurator.increaseKeepAliveSpigot(file, load(file), timeouts()); + Files.writeString(file, "settings:\n timeout-time: 90\n"); + ServerConfigurator.increaseKeepAliveSpigot(file, load(file), timeouts()); + timeouts().restore(SPIGOT_KEY, file, load(file)); + assertEquals(90L, load(file).getLong(SPIGOT_KEY)); + } + } + + @Test + public void failedRecoveryWriteLeavesServerSettingsUntouched() throws Exception { + Path file = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + YamlConfiguration live = load(file); + Path blocked = temporaryFolder.newFile("blocked").toPath(); + ServerTimeoutConfiguration timeouts = new ServerTimeoutConfiguration(blocked.resolve("server-timeouts.json")); + assertThrows(IOException.class, () -> timeouts.raise(file, live, SPIGOT_KEY, 300L)); + assertEquals(60L, load(file).getLong(SPIGOT_KEY)); + assertEquals(60L, live.getLong(SPIGOT_KEY)); + } + + @Test + public void failedServerWriteKeepsTheOriginalAndRestoresTheLiveValue() throws Exception { + Path file = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + YamlConfiguration live = load(file); + try (MockedStatic writes = mockStatic(AtomicFileIO.class, CALLS_REAL_METHODS)) { + writes.when(() -> AtomicFileIO.writeString(eq(file.toRealPath()), anyString())) + .thenThrow(new IOException("blocked server write")); + assertThrows(IOException.class, () -> timeouts().raise(file, live, SPIGOT_KEY, 300L)); + } + assertEquals(60L, load(file).getLong(SPIGOT_KEY)); + assertEquals(60L, live.getLong(SPIGOT_KEY)); + assertTrue(Files.readString(recoveryFile()).contains("\"original\": 60")); + } + + @Test + public void interruptedRestoreRetriesWithoutOverwritingLaterOperatorEdits() throws Exception { + Path file = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + try (MockedStatic logging = mockStatic(IrisLogging.class)) { + ServerConfigurator.increaseKeepAliveSpigot(file, load(file), timeouts()); + try (MockedStatic writes = mockStatic(AtomicFileIO.class, CALLS_REAL_METHODS)) { + writes.when(() -> AtomicFileIO.writeString(eq(recoveryFile()), anyString())) + .thenThrow(new IOException("interrupted recovery write")); + assertThrows(IOException.class, () -> timeouts().restore(SPIGOT_KEY, file, load(file))); + } + assertEquals(60L, load(file).getLong(SPIGOT_KEY)); + assertTrue(Files.readString(recoveryFile()).contains("spigot.yml")); + Files.writeString(file, "settings:\n timeout-time: 120\n"); + timeouts().restore(SPIGOT_KEY, file, load(file)); + assertEquals(120L, load(file).getLong(SPIGOT_KEY)); + assertFalse(Files.readString(recoveryFile()).contains("spigot.yml")); + } + } + + @Test + public void malformedRecoveryDataPreventsNewConfigurationWrites() throws Exception { + Path file = configuration("spigot.yml", "settings:\n timeout-time: 60\n"); + Files.writeString(recoveryFile(), "{invalid"); + assertThrows(IOException.class, () -> timeouts().raise(file, load(file), SPIGOT_KEY, 300L)); + assertEquals(60L, load(file).getLong(SPIGOT_KEY)); + assertEquals("{invalid", Files.readString(recoveryFile())); + } + + private Path configuration(String name, String content) throws IOException { + Path file = temporaryFolder.newFile(name).toPath(); + Files.writeString(file, content); + return file; + } + + private ServerTimeoutConfiguration timeouts() { + return new ServerTimeoutConfiguration(recoveryFile()); + } + + private Path recoveryFile() { + return temporaryFolder.getRoot().toPath().resolve("server-timeouts.json"); + } + + private static YamlConfiguration load(Path file) throws Exception { + YamlConfiguration configuration = new YamlConfiguration(); + configuration.load(file.toFile()); + return configuration; + } +} diff --git a/gradle/modded-generation-build-revision.gradle b/gradle/modded-generation-build-revision.gradle index b57ce7291..df258591b 100644 --- a/gradle/modded-generation-build-revision.gradle +++ b/gradle/modded-generation-build-revision.gradle @@ -18,7 +18,7 @@ TaskProvider resolvedRevisionVerification = tasks.register('verifyReso classpath = files('../../buildSrc/build/libs/buildSrc.jar') mainClass.set('art.arcane.iris.buildtools.GenerationBuildRevision') doFirst { - String runtimeVariant = providers.gradleProperty('irisRuntimeJava').getOrElse(irisModTarget.java.toString()) == '21' ? 'java21/' : '' + String runtimeVariant = gradle.extensions.extraProperties.get('irisRuntimeOutputVariant').toString() List arguments = ['verify-dependencies', file("../../core/build/${runtimeVariant}generated/generation-revision/META-INF/iris/generation-kernels/abi-1.revision").absolutePath] for (ResolvedArtifact artifact : configurations.bundle.resolvedConfiguration.resolvedArtifacts) { String group = artifact.moduleVersion.id.group diff --git a/gradle/runtime-cache.settings.gradle b/gradle/runtime-cache.settings.gradle new file mode 100644 index 000000000..989beba22 --- /dev/null +++ b/gradle/runtime-cache.settings.gradle @@ -0,0 +1,23 @@ +import groovy.json.JsonSlurper + +boolean adapter = rootProject.name.startsWith('iris-') +File irisDirectory = adapter ? new File(settingsDir, '../..').canonicalFile : settingsDir +String minecraftVersion = providers.gradleProperty('minecraftVersion').getOrElse('26.3') +Map> modTargets = (Map>) new JsonSlurper() + .parse(new File(irisDirectory, 'gradle/mod-targets.json')) +Map modTarget = modTargets.get(minecraftVersion) +if (modTarget == null) { + throw new GradleException("Unsupported Iris mod target ${minecraftVersion}; choose ${modTargets.keySet().join(', ')}") +} +String runtimeJava = providers.gradleProperty('irisRuntimeJava').getOrElse(modTarget.java.toString()) +String variant = adapter ? "minecraft${minecraftVersion}-java${runtimeJava}" : "java${runtimeJava}" +File projectCacheRoot = gradle.startParameter.projectCacheDir ?: new File(settingsDir, '.gradle') +gradle.startParameter.projectCacheDir = new File(projectCacheRoot, variant) + +String runtimeOutputVariant = runtimeJava == '21' ? 'java21/' : '' +if (adapter) { + runtimeOutputVariant = "modded/${rootProject.name}/${variant}/" +} else if (gradle.parent != null && gradle.parent.extensions.extraProperties.has('irisRuntimeOutputVariant')) { + runtimeOutputVariant = gradle.parent.extensions.extraProperties.get('irisRuntimeOutputVariant').toString() +} +gradle.extensions.extraProperties.set('irisRuntimeOutputVariant', runtimeOutputVariant) diff --git a/settings.gradle b/settings.gradle index d5fa041aa..fa2892f4f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ plugins { rootProject.name = 'Iris' +apply from: new File(settingsDir, 'gradle/runtime-cache.settings.gradle') apply from: new File(settingsDir, 'gradle/volmlib-resolution.settings.gradle') include(':core', ':core:agent')