Skip to content

Commit b499a05

Browse files
committed
Possible fix for the elusive Forge config corruption bug
Block file watcher from proceeding until config is done saving Related: MinecraftForge/MinecraftForge#9122
1 parent c3e3dff commit b499a05

File tree

2 files changed

+122
-0
lines changed

2 files changed

+122
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package org.embeddedt.modernfix.forge.config;
2+
3+
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
4+
import com.electronwill.nightconfig.core.file.FileConfig;
5+
import com.electronwill.nightconfig.core.file.FileWatcher;
6+
import cpw.mods.modlauncher.api.LamdbaExceptionUtils;
7+
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
8+
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
9+
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
10+
import org.embeddedt.modernfix.util.CommonModUtil;
11+
12+
import java.lang.reflect.Field;
13+
import java.nio.file.Path;
14+
import java.util.Collections;
15+
import java.util.Set;
16+
import java.util.concurrent.ConcurrentHashMap;
17+
import java.util.function.Function;
18+
19+
/**
20+
* Relatively simple patch to wait for config saving to finish, made complex by Night Config classes being package-private,
21+
* and Forge not allowing mixins into libraries.
22+
*/
23+
public class NightConfigFixer {
24+
public static void monitorFileWatcher() {
25+
CommonModUtil.runWithoutCrash(() -> {
26+
FileWatcher watcher = FileWatcher.defaultInstance();
27+
Field field = FileWatcher.class.getDeclaredField("watchedFiles");
28+
field.setAccessible(true);
29+
ConcurrentHashMap<Path, ?> theMap = (ConcurrentHashMap<Path, ?>)field.get(watcher);
30+
// replace the backing map of watched files with one we control
31+
field.set(watcher, new MonitoringMap(theMap));
32+
ModernFixMixinPlugin.instance.logger.info("Applied Forge config corruption patch");
33+
}, "replacing Night Config watchedFiles map");
34+
}
35+
36+
private static final Class<?> WATCHED_FILE = LamdbaExceptionUtils.uncheck(() -> Class.forName("com.electronwill.nightconfig.core.file.FileWatcher$WatchedFile"));
37+
private static final Class<?> CONFIG_WATCHER = LamdbaExceptionUtils.uncheck(() -> Class.forName("net.minecraftforge.fml.config.ConfigFileTypeHandler$ConfigWatcher"));
38+
private static final Field CHANGE_HANDLER = ObfuscationReflectionHelper.findField(WATCHED_FILE, "changeHandler");
39+
40+
private static final Field COMMENTED_FILE_CONFIG = ObfuscationReflectionHelper.findField(CONFIG_WATCHER, "commentedFileConfig");
41+
42+
private static final Class<?> WRITE_SYNC_CONFIG = LamdbaExceptionUtils.uncheck(() -> Class.forName("com.electronwill.nightconfig.core.file.WriteSyncFileConfig"));
43+
private static final Class<?> AUTOSAVE_CONFIG = LamdbaExceptionUtils.uncheck(() -> Class.forName("com.electronwill.nightconfig.core.file.AutosaveCommentedFileConfig"));
44+
private static final Field AUTOSAVE_FILECONFIG = ObfuscationReflectionHelper.findField(AUTOSAVE_CONFIG, "fileConfig");
45+
46+
private static final Field CURRENTLY_WRITING = ObfuscationReflectionHelper.findField(WRITE_SYNC_CONFIG, "currentlyWriting");
47+
48+
static class MonitoringMap extends ConcurrentHashMap<Path, Object> {
49+
public MonitoringMap(ConcurrentHashMap<Path, ?> oldMap) {
50+
super(oldMap);
51+
}
52+
53+
@Override
54+
public Object computeIfAbsent(Path key, Function<? super Path, ?> mappingFunction) {
55+
return super.computeIfAbsent(key, path -> {
56+
Object watchedFile = mappingFunction.apply(path);
57+
try {
58+
Runnable changeHandler = (Runnable)CHANGE_HANDLER.get(watchedFile);
59+
// replace Forge's config tracker with our own
60+
if(CONFIG_WATCHER.isAssignableFrom(changeHandler.getClass()))
61+
CHANGE_HANDLER.set(watchedFile, new MonitoringConfigTracker(changeHandler));
62+
else
63+
ModernFixMixinPlugin.instance.logger.warn("Unexpected Forge config tracker class: {}", changeHandler.getClass().getName());
64+
} catch(ReflectiveOperationException e) {
65+
e.printStackTrace();
66+
}
67+
return watchedFile;
68+
});
69+
}
70+
}
71+
72+
static class MonitoringConfigTracker implements Runnable {
73+
private final Runnable configTracker;
74+
75+
MonitoringConfigTracker(Runnable r) {
76+
this.configTracker = r;
77+
}
78+
79+
private static final Set<Class<?>> UNKNOWN_FILE_CONFIG_CLASSES = Collections.synchronizedSet(new ReferenceOpenHashSet<>());
80+
81+
private boolean isSaving(FileConfig config) throws ReflectiveOperationException {
82+
if(WRITE_SYNC_CONFIG.isAssignableFrom(config.getClass())) {
83+
return CURRENTLY_WRITING.getBoolean(config);
84+
} else if(AUTOSAVE_CONFIG.isAssignableFrom(config.getClass())) {
85+
FileConfig fc = (FileConfig)AUTOSAVE_FILECONFIG.get(config);
86+
return isSaving(fc);
87+
} else {
88+
if(UNKNOWN_FILE_CONFIG_CLASSES.add(config.getClass()))
89+
ModernFixMixinPlugin.instance.logger.warn("Unexpected FileConfig class: {}", config.getClass().getName());
90+
return false;
91+
}
92+
}
93+
94+
/**
95+
* This entrypoint runs when the file watcher has detected a change on the config file. Before passing
96+
* this through to Forge, use reflection hacks to confirm the config system is not still writing to the file.
97+
* If it is, spin until writing finishes. Immediately returning might result in the event never being observed.
98+
*/
99+
@Override
100+
public void run() {
101+
try {
102+
CommentedFileConfig cfg = (CommentedFileConfig)COMMENTED_FILE_CONFIG.get(this.configTracker);
103+
while(isSaving(cfg)) {
104+
// block until the config is no longer marked as saving
105+
// Forge shouldn't save the config twice in a row under normal conditions so this should fix the
106+
// original bug
107+
try {
108+
Thread.sleep(500);
109+
} catch (InterruptedException e) {
110+
return;
111+
}
112+
}
113+
} catch(ReflectiveOperationException e) {
114+
e.printStackTrace();
115+
}
116+
configTracker.run();
117+
}
118+
}
119+
}

forge/src/main/java/org/embeddedt/modernfix/platform/forge/ModernFixPlatformHooksImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.embeddedt.modernfix.api.constants.IntegrationConstants;
3030
import org.embeddedt.modernfix.forge.classloading.FastAccessTransformerList;
3131
import org.embeddedt.modernfix.forge.classloading.ModernFixResourceFinder;
32+
import org.embeddedt.modernfix.forge.config.NightConfigFixer;
3233
import org.embeddedt.modernfix.forge.packet.PacketHandler;
3334
import org.embeddedt.modernfix.forge.spark.SparkLaunchProfiler;
3435
import org.embeddedt.modernfix.util.CommonModUtil;
@@ -180,6 +181,8 @@ public static void injectPlatformSpecificHacks() {
180181
if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.spark_profile_launch.OnForge")) {
181182
CommonModUtil.runWithoutCrash(() -> SparkLaunchProfiler.start("launch"), "Failed to start profiler");
182183
}
184+
185+
NightConfigFixer.monitorFileWatcher();
183186
}
184187

185188
private Method defineClassMethod = null;

0 commit comments

Comments
 (0)