Last active
November 20, 2021 18:01
-
-
Save abextm/406be37cc74edddb0157dfce8bec372b to your computer and use it in GitHub Desktop.
janky hmr
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package abex.os.hmr; | |
import com.google.common.base.CharMatcher; | |
import com.google.common.base.Splitter; | |
import com.google.common.io.Files; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.IOException; | |
import java.lang.invoke.MethodHandles; | |
import java.lang.reflect.InvocationTargetException; | |
import java.net.URISyntaxException; | |
import java.net.URL; | |
import java.nio.file.FileSystems; | |
import java.nio.file.StandardWatchEventKinds; | |
import java.nio.file.WatchKey; | |
import java.nio.file.WatchService; | |
import java.util.ArrayList; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Properties; | |
import java.util.Set; | |
import java.util.Timer; | |
import java.util.TimerTask; | |
import javax.inject.Inject; | |
import javax.swing.SwingUtilities; | |
import lombok.Getter; | |
import lombok.Setter; | |
import lombok.SneakyThrows; | |
import lombok.extern.slf4j.Slf4j; | |
import net.runelite.client.RuneLite; | |
import net.runelite.client.eventbus.EventBus; | |
import net.runelite.client.events.ExternalPluginsChanged; | |
import net.runelite.client.externalplugins.ExternalPluginManager; | |
import net.runelite.client.plugins.Plugin; | |
import net.runelite.client.plugins.PluginDescriptor; | |
import net.runelite.client.plugins.PluginInstantiationException; | |
import net.runelite.client.plugins.PluginManager; | |
import net.runelite.client.util.ReflectUtil; | |
@Slf4j | |
public class ExternalPluginHotReloader | |
{ | |
private static List<String> pluginClassNames; | |
public ExternalPluginHotReloader() throws IOException | |
{ | |
} | |
public static void main(String... args) throws Exception | |
{ | |
File pluginFile = new File("runelite-plugin.properties"); | |
if (!pluginFile.exists()) | |
{ | |
throw new RuntimeException("runelite-plugin.properties does not exist. Perhaps your working directory is incorrect. It should be the root of your plugin hub plugin."); | |
} | |
Properties props = loadProperties(pluginFile); | |
//TODO: validate the whole thing | |
{ | |
String pluginsStr = (String) props.remove("plugins"); | |
if (pluginsStr == null) | |
{ | |
throw new RuntimeException("plugins must be set in runelite-plugin.properties"); | |
} | |
pluginClassNames = Splitter.on(CharMatcher.anyOf(",:;")) | |
.omitEmptyStrings() | |
.trimResults() | |
.splitToList(pluginsStr); | |
} | |
ExternalPluginManager.loadBuiltin(HMRPlugin.class); | |
RuneLite.main(args); | |
} | |
@PluginDescriptor( | |
name = "hmr", | |
description = "", | |
hidden = true) | |
public static class HMRPlugin extends Plugin | |
{ | |
@Inject | |
private ExternalPluginHotReloader hmr; | |
@Override | |
protected void startUp() throws Exception | |
{ | |
hmr.startLater(0); | |
} | |
} | |
private static Properties loadProperties(File path) throws IOException | |
{ | |
Properties props = new Properties(); | |
try (FileInputStream fis = new FileInputStream(path)) | |
{ | |
props.load(fis); | |
} | |
return props; | |
} | |
private class ProxyCL extends ClassLoader implements ReflectUtil.PrivateLookupableClassLoader | |
{ | |
@Getter | |
@Setter | |
private MethodHandles.Lookup lookup; | |
private final ClassLoader realParent; | |
public ProxyCL() | |
{ | |
super(null); | |
this.realParent = getClass().getClassLoader(); | |
ReflectUtil.installLookupHelper(this); | |
} | |
@Override | |
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException | |
{ | |
URL resource = realParent.getResource(name.replace('.', '/') + ".class"); | |
if (resource != null && resource.toString().endsWith(".class")) | |
{ | |
try | |
{ | |
File f = new File(resource.toURI()); | |
byte[] data = Files.toByteArray(f); | |
watch(f); | |
return defineClass(name, data, 0, data.length); | |
} | |
catch (IOException e) | |
{ | |
log.warn("{}", resource, e); | |
} | |
catch (URISyntaxException | IllegalArgumentException ignored) | |
{ | |
} | |
} | |
return realParent.loadClass(name); | |
} | |
@Override | |
public Class<?> defineClass0(String name, byte[] b, int off, int len) throws ClassFormatError | |
{ | |
return super.defineClass(name, b, off, len); | |
} | |
} | |
private final Set<File> filesOfInterest = new HashSet<>(); | |
private final WatchService watcher = FileSystems.getDefault().newWatchService(); | |
@Inject | |
private PluginManager pluginManager; | |
@Inject | |
private EventBus eventBus; | |
private final List<Plugin> loaded = new ArrayList<>(); | |
private final Timer timer = new Timer(true); | |
private TimerTask task; | |
@SneakyThrows | |
private void watch(File f) | |
{ | |
if (!filesOfInterest.add(f)) | |
{ | |
return; | |
} | |
if (!f.isDirectory()) | |
{ | |
f = f.getParentFile(); | |
if (!filesOfInterest.add(f)) | |
{ | |
return; | |
} | |
} | |
f.toPath().register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY); | |
} | |
{ | |
Thread t = new Thread(() -> | |
{ | |
for (; ; ) | |
{ | |
try | |
{ | |
WatchKey key = watcher.take(); | |
key.pollEvents(); | |
startLater(500); | |
key.reset(); | |
} | |
catch (Exception e) | |
{ | |
log.warn("", e); | |
} | |
} | |
}, "HMR"); | |
t.setDaemon(true); | |
t.start(); | |
} | |
private synchronized void startLater(int delayMS) | |
{ | |
if (task != null) | |
{ | |
task.cancel(); | |
} | |
task = new TimerTask() | |
{ | |
@Override | |
public void run() | |
{ | |
start(); | |
} | |
}; | |
timer.schedule(task, delayMS); | |
} | |
private void start() | |
{ | |
log.info("Reloading"); | |
for (Plugin p : loaded) | |
{ | |
try | |
{ | |
SwingUtilities.invokeAndWait(() -> | |
{ | |
try | |
{ | |
pluginManager.stopPlugin(p); | |
} | |
catch (Exception e) | |
{ | |
throw new RuntimeException(e); | |
} | |
}); | |
} | |
catch (InterruptedException | InvocationTargetException e) | |
{ | |
log.warn("Unable to stop external plugin \"{}\"", p.getClass().getName(), e); | |
} | |
pluginManager.remove(p); | |
} | |
loaded.clear(); | |
ProxyCL pcl = new ProxyCL(); | |
for (String className : pluginClassNames) | |
{ | |
try | |
{ | |
List<Class<?>> clazzes = new ArrayList<>(); | |
clazzes.add(pcl.loadClass(className)); | |
List<Plugin> newPlugins2 = pluginManager.loadPlugins(clazzes, null); | |
loaded.addAll(newPlugins2); | |
pluginManager.loadDefaultPluginConfiguration(newPlugins2); | |
SwingUtilities.invokeAndWait(() -> | |
{ | |
try | |
{ | |
for (Plugin p : newPlugins2) | |
{ | |
pluginManager.startPlugin(p); | |
} | |
} | |
catch (PluginInstantiationException e) | |
{ | |
throw new RuntimeException(e); | |
} | |
}); | |
} | |
catch (ThreadDeath e) | |
{ | |
throw e; | |
} | |
catch (Throwable e) | |
{ | |
log.warn("Unable to start or load external plugin \"{}\"", className, e); | |
} | |
} | |
eventBus.post(new ExternalPluginsChanged(new ArrayList<>())); | |
log.info("Done"); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment