From 30fe5b32b347c467651e45e9dcdb8cfd07bec61f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 30 May 2025 11:32:23 +0200 Subject: [PATCH 01/68] At least this works Yes it HAS to be in a different module --- build.gradle.kts | 3 + .../internal/restart/ChangeableUrls.java | 187 ++++++ .../restart/DefaultRestartInitializer.java | 83 +++ .../internal/restart/MainMethod.java | 90 +++ .../internal/restart/RestartLauncher.java | 62 ++ .../internal/restart/Restarter.java | 603 ++++++++++++++++++ .../restart/SilentExitExceptionHandler.java | 99 +++ 7 files changed, 1127 insertions(+) create mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java create mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java create mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java create mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java create mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java create mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java diff --git a/build.gradle.kts b/build.gradle.kts index eddaaea..41baddd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,6 +11,9 @@ repositories { } dependencies { + implementation("org.springframework.boot:spring-boot-starter:3.4.2") + implementation("org.springframework.boot:spring-boot-devtools:3.4.2") + testImplementation(kotlin("test")) } diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java b/src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java new file mode 100644 index 0000000..0b683e3 --- /dev/null +++ b/src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java @@ -0,0 +1,187 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.freya02.botcommands.internal.restart; + +import org.apache.commons.logging.Log; +import org.springframework.boot.devtools.logger.DevToolsLogFactory; +import org.springframework.boot.devtools.settings.DevToolsSettings; +import org.springframework.core.log.LogMessage; +import org.springframework.util.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.management.ManagementFactory; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; + +/** + * A filtered collection of URLs which can change after the application has started. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +final class ChangeableUrls implements Iterable { + + private static final Log logger = DevToolsLogFactory.getLog(ChangeableUrls.class); + + private final List urls; + + private ChangeableUrls(URL... urls) { + DevToolsSettings settings = DevToolsSettings.get(); + List reloadableUrls = new ArrayList<>(urls.length); + for (URL url : urls) { + if ((settings.isRestartInclude(url) || isDirectoryUrl(url.toString())) && !settings.isRestartExclude(url)) { + reloadableUrls.add(url); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Matching URLs for reloading : " + reloadableUrls); + } + this.urls = Collections.unmodifiableList(reloadableUrls); + } + + private boolean isDirectoryUrl(String urlString) { + return urlString.startsWith("file:") && urlString.endsWith("/"); + } + + @Override + public Iterator iterator() { + return this.urls.iterator(); + } + + int size() { + return this.urls.size(); + } + + URL[] toArray() { + return this.urls.toArray(new URL[0]); + } + + List toList() { + return Collections.unmodifiableList(this.urls); + } + + @Override + public String toString() { + return this.urls.toString(); + } + + static ChangeableUrls fromClassLoader(ClassLoader classLoader) { + List urls = new ArrayList<>(); + for (URL url : urlsFromClassLoader(classLoader)) { + urls.add(url); + urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url)); + } + return fromUrls(urls); + } + + private static URL[] urlsFromClassLoader(ClassLoader classLoader) { + if (classLoader instanceof URLClassLoader urlClassLoader) { + return urlClassLoader.getURLs(); + } + return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) + .map(ChangeableUrls::toURL) + .toArray(URL[]::new); + } + + private static URL toURL(String classPathEntry) { + try { + return new File(classPathEntry).toURI().toURL(); + } + catch (MalformedURLException ex) { + throw new IllegalArgumentException("URL could not be created from '" + classPathEntry + "'", ex); + } + } + + private static List getUrlsFromClassPathOfJarManifestIfPossible(URL url) { + try { + File file = new File(url.toURI()); + if (file.isFile()) { + try (JarFile jarFile = new JarFile(file)) { + try { + return getUrlsFromManifestClassPathAttribute(url, jarFile); + } + catch (IOException ex) { + throw new IllegalStateException( + "Failed to read Class-Path attribute from manifest of jar " + url, ex); + } + } + } + } + catch (Exception ex) { + // Assume it's not a jar and continue + } + return Collections.emptyList(); + } + + private static List getUrlsFromManifestClassPathAttribute(URL jarUrl, JarFile jarFile) throws IOException { + Manifest manifest = jarFile.getManifest(); + if (manifest == null) { + return Collections.emptyList(); + } + String classPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); + if (!StringUtils.hasText(classPath)) { + return Collections.emptyList(); + } + String[] entries = StringUtils.delimitedListToStringArray(classPath, " "); + List urls = new ArrayList<>(entries.length); + List nonExistentEntries = new ArrayList<>(); + for (String entry : entries) { + try { + URL referenced = new URL(jarUrl, entry); + if (new File(referenced.getFile()).exists()) { + urls.add(referenced); + } + else { + referenced = new URL(jarUrl, URLDecoder.decode(entry, StandardCharsets.UTF_8)); + if (new File(referenced.getFile()).exists()) { + urls.add(referenced); + } + else { + nonExistentEntries.add(referenced); + } + } + } + catch (MalformedURLException ex) { + throw new IllegalStateException("Class-Path attribute contains malformed URL", ex); + } + } + if (!nonExistentEntries.isEmpty()) { + logger.info(LogMessage.of(() -> "The Class-Path manifest attribute in " + jarFile.getName() + + " referenced one or more files that do not exist: " + + StringUtils.collectionToCommaDelimitedString(nonExistentEntries))); + } + return urls; + } + + static ChangeableUrls fromUrls(Collection urls) { + return fromUrls(new ArrayList<>(urls).toArray(new URL[urls.size()])); + } + + static ChangeableUrls fromUrls(URL... urls) { + return new ChangeableUrls(urls); + } + +} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java b/src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java new file mode 100644 index 0000000..73f4627 --- /dev/null +++ b/src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.freya02.botcommands.internal.restart; + +import org.springframework.boot.devtools.restart.RestartInitializer; + +import java.net.URL; + +/** + * Default {@link RestartInitializer} that only enable initial restart when running a + * standard "main" method. Skips initialization when running "fat" jars (included + * exploded) or when running from a test. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.3.0 + */ +public class DefaultRestartInitializer implements RestartInitializer { + + @Override + public URL[] getInitialUrls(Thread thread) { + return getUrls(thread); + } + + /** + * Returns if the thread is for a main invocation. By default {@link #isMain(Thread) + * checks the name of the thread} and {@link #isDevelopmentClassLoader(ClassLoader) + * the context classloader}. + * @param thread the thread to check + * @return {@code true} if the thread is a main invocation + * @see #isMainThread + * @see #isDevelopmentClassLoader(ClassLoader) + */ + protected boolean isMain(Thread thread) { + return isMainThread(thread) && isDevelopmentClassLoader(thread.getContextClassLoader()); + } + + /** + * Returns whether the given {@code thread} is considered to be the main thread. + * @param thread the thread to check + * @return {@code true} if it's the main thread, otherwise {@code false} + * @since 2.4.0 + */ + protected boolean isMainThread(Thread thread) { + return thread.getName().equals("main"); + } + + /** + * Returns whether the given {@code classLoader} is one that is typically used during + * development. + * @param classLoader the ClassLoader to check + * @return {@code true} if it's a ClassLoader typically used during development, + * otherwise {@code false} + * @since 2.4.0 + */ + protected boolean isDevelopmentClassLoader(ClassLoader classLoader) { + return classLoader.getClass().getName().contains("AppClassLoader"); + } + + /** + * Return the URLs that should be used with initialization. + * @param thread the source thread + * @return the URLs + */ + protected URL[] getUrls(Thread thread) { + return ChangeableUrls.fromClassLoader(thread.getContextClassLoader()).toArray(); + } + +} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java b/src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java new file mode 100644 index 0000000..fe3e92b --- /dev/null +++ b/src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.freya02.botcommands.internal.restart; + +import org.springframework.util.Assert; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +/** + * The "main" method located from a running thread. + * + * @author Phillip Webb + */ +class MainMethod { + + private final Method method; + + MainMethod() { + this(Thread.currentThread()); + } + + MainMethod(Thread thread) { + Assert.notNull(thread, "Thread must not be null"); + this.method = getMainMethod(thread); + } + + private Method getMainMethod(Thread thread) { + StackTraceElement[] stackTrace = thread.getStackTrace(); + for (int i = stackTrace.length - 1; i >= 0; i--) { + StackTraceElement element = stackTrace[i]; + if ("main".equals(element.getMethodName()) && !isLoaderClass(element.getClassName())) { + Method method = getMainMethod(element); + if (method != null) { + return method; + } + } + } + throw new IllegalStateException("Unable to find main method"); + } + + private boolean isLoaderClass(String className) { + return className.startsWith("org.springframework.boot.loader."); + } + + private Method getMainMethod(StackTraceElement element) { + try { + Class elementClass = Class.forName(element.getClassName()); + Method method = elementClass.getDeclaredMethod("main", String[].class); + if (Modifier.isStatic(method.getModifiers())) { + return method; + } + } + catch (Exception ex) { + // Ignore + } + return null; + } + + /** + * Returns the actual main method. + * @return the main method + */ + Method getMethod() { + return this.method; + } + + /** + * Return the name of the declaring class. + * @return the declaring class name + */ + String getDeclaringClassName() { + return this.method.getDeclaringClass().getName(); + } + +} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java b/src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java new file mode 100644 index 0000000..b30fe5f --- /dev/null +++ b/src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.freya02.botcommands.internal.restart; + +import java.lang.reflect.Method; + +/** + * Thread used to launch a restarted application. + * + * @author Phillip Webb + */ +class RestartLauncher extends Thread { + + private final String mainClassName; + + private final String[] args; + + private Throwable error; + + RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args, + UncaughtExceptionHandler exceptionHandler) { + this.mainClassName = mainClassName; + this.args = args; + setName("restartedMain"); + setUncaughtExceptionHandler(exceptionHandler); + setDaemon(false); + setContextClassLoader(classLoader); + } + + @Override + public void run() { + try { + Class mainClass = Class.forName(this.mainClassName, false, getContextClassLoader()); + Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); + mainMethod.setAccessible(true); + mainMethod.invoke(null, new Object[] { this.args }); + } + catch (Throwable ex) { + this.error = ex; + getUncaughtExceptionHandler().uncaughtException(this, ex); + } + } + + Throwable getError() { + return this.error; + } + +} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java b/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java new file mode 100644 index 0000000..bb6aef2 --- /dev/null +++ b/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java @@ -0,0 +1,603 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.freya02.botcommands.internal.restart; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.CachedIntrospectionResults; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.devtools.restart.*; +import org.springframework.boot.devtools.restart.FailureHandler.Outcome; +import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; +import org.springframework.boot.devtools.restart.classloader.RestartClassLoader; +import org.springframework.boot.logging.DeferredLog; +import org.springframework.cglib.core.ClassNameReader; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import java.beans.Introspector; +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Allows a running application to be restarted with an updated classpath. The restarter + * works by creating a new application ClassLoader that is split into two parts. The top + * part contains static URLs that don't change (for example 3rd party libraries and Spring + * Boot itself) and the bottom part contains URLs where classes and resources might be + * updated. + *

+ * The Restarter should be {@link #initialize(String[]) initialized} early to ensure that + * classes are loaded multiple times. Mostly the {@link RestartApplicationListener} can be + * relied upon to perform initialization, however, you may need to call + * {@link #initialize(String[])} directly if your SpringApplication arguments are not + * identical to your main method arguments. + *

+ * By default, applications running in an IDE (i.e. those not packaged as "uber jars") + * will automatically detect URLs that can change. It's also possible to manually + * configure URLs or class file updates for remote restart scenarios. + * + * @author Phillip Webb + * @author Andy Wilkinson + * @since 1.3.0 + * @see RestartApplicationListener + * @see #initialize(String[]) + * @see #getInstance() + * @see #restart() + */ +public class Restarter { + + private static final Object INSTANCE_MONITOR = new Object(); + + private static final String[] NO_ARGS = {}; + + private static Restarter instance; + + private final Set urls = new LinkedHashSet<>(); + + private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles(); + + private final Map attributes = new ConcurrentHashMap<>(); + + private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque<>(); + + private final Lock stopLock = new ReentrantLock(); + + private final Object monitor = new Object(); + + private Log logger = new DeferredLog(); + + private final boolean forceReferenceCleanup; + + private boolean enabled = true; + + private final URL[] initialUrls; + + private final String mainClassName; + + private final ClassLoader applicationClassLoader; + + private final String[] args; + + private final UncaughtExceptionHandler exceptionHandler; + + private boolean finished = false; + + private final List rootContexts = new CopyOnWriteArrayList<>(); + + /** + * Internal constructor to create a new {@link Restarter} instance. + * @param thread the source thread + * @param args the application arguments + * @param forceReferenceCleanup if soft/weak reference cleanup should be forced + * @param initializer the restart initializer + * @see #initialize(String[]) + */ + protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup, RestartInitializer initializer) { + Assert.notNull(thread, "Thread must not be null"); + Assert.notNull(args, "Args must not be null"); + Assert.notNull(initializer, "Initializer must not be null"); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Creating new Restarter for thread " + thread); + } + SilentExitExceptionHandler.setup(thread); + this.forceReferenceCleanup = forceReferenceCleanup; + this.initialUrls = initializer.getInitialUrls(thread); + this.mainClassName = getMainClassName(thread); + this.applicationClassLoader = thread.getContextClassLoader(); + this.args = args; + this.exceptionHandler = thread.getUncaughtExceptionHandler(); + this.leakSafeThreads.add(new LeakSafeThread()); + } + + private String getMainClassName(Thread thread) { + try { + return new MainMethod(thread).getDeclaringClassName(); + } + catch (Exception ex) { + return null; + } + } + + protected void initialize(boolean restartOnInitialize) { + preInitializeLeakyClasses(); + if (this.initialUrls != null) { + this.urls.addAll(Arrays.asList(this.initialUrls)); + if (restartOnInitialize) { + this.logger.debug("Immediately restarting application"); + immediateRestart(); + } + } + } + + private void immediateRestart() { + try { + getLeakSafeThread().callAndWait(() -> { + start(FailureHandler.NONE); + cleanupCaches(); + return null; + }); + } + catch (Exception ex) { + this.logger.warn("Unable to initialize restarter", ex); + } + SilentExitExceptionHandler.exitCurrentThread(); + } + + /** + * CGLIB has a private exception field which needs to initialized early to ensure that + * the stacktrace doesn't retain a reference to the RestartClassLoader. + */ + private void preInitializeLeakyClasses() { + try { + Class readerClass = ClassNameReader.class; + Field field = readerClass.getDeclaredField("EARLY_EXIT"); + field.setAccessible(true); + ((Throwable) field.get(null)).fillInStackTrace(); + } + catch (Exception ex) { + this.logger.warn("Unable to pre-initialize classes", ex); + } + } + + /** + * Set if restart support is enabled. + * @param enabled if restart support is enabled + */ + private void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * Add additional URLs to be includes in the next restart. + * @param urls the urls to add + */ + public void addUrls(Collection urls) { + Assert.notNull(urls, "Urls must not be null"); + this.urls.addAll(urls); + } + + /** + * Add additional {@link ClassLoaderFiles} to be included in the next restart. + * @param classLoaderFiles the files to add + */ + public void addClassLoaderFiles(ClassLoaderFiles classLoaderFiles) { + Assert.notNull(classLoaderFiles, "ClassLoaderFiles must not be null"); + this.classLoaderFiles.addAll(classLoaderFiles); + } + + /** + * Return a {@link ThreadFactory} that can be used to create leak safe threads. + * @return a leak safe thread factory + */ + public ThreadFactory getThreadFactory() { + return new LeakSafeThreadFactory(); + } + + /** + * Restart the running application. + */ + public void restart() { + restart(FailureHandler.NONE); + } + + /** + * Restart the running application. + * @param failureHandler a failure handler to deal with application that doesn't start + */ + public void restart(FailureHandler failureHandler) { + if (!this.enabled) { + this.logger.debug("Application restart is disabled"); + return; + } + this.logger.debug("Restarting application"); + getLeakSafeThread().call(() -> { + Restarter.this.stop(); + Restarter.this.start(failureHandler); + return null; + }); + } + + /** + * Start the application. + * @param failureHandler a failure handler for application that won't start + * @throws Exception in case of errors + */ + protected void start(FailureHandler failureHandler) throws Exception { + do { + Throwable error = doStart(); + if (error == null) { + return; + } + if (failureHandler.handle(error) == Outcome.ABORT) { + return; + } + } + while (true); + } + + private Throwable doStart() throws Exception { + Assert.notNull(this.mainClassName, "Unable to find the main class to restart"); + URL[] urls = this.urls.toArray(new URL[0]); + ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles); + ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Starting application " + this.mainClassName + " with URLs " + Arrays.asList(urls)); + } + return relaunch(classLoader); + } + + /** + * Relaunch the application using the specified classloader. + * @param classLoader the classloader to use + * @return any exception that caused the launch to fail or {@code null} + * @throws Exception in case of errors + */ + protected Throwable relaunch(ClassLoader classLoader) throws Exception { + RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName, this.args, + this.exceptionHandler); + launcher.start(); + launcher.join(); + return launcher.getError(); + } + + /** + * Stop the application. + * @throws Exception in case of errors + */ + protected void stop() throws Exception { + this.logger.debug("Stopping application"); + this.stopLock.lock(); + try { + for (ConfigurableApplicationContext context : this.rootContexts) { + context.close(); + this.rootContexts.remove(context); + } + cleanupCaches(); + if (this.forceReferenceCleanup) { + forceReferenceCleanup(); + } + } + finally { + this.stopLock.unlock(); + } + System.gc(); + System.runFinalization(); + } + + private void cleanupCaches() { + Introspector.flushCaches(); + cleanupKnownCaches(); + } + + private void cleanupKnownCaches() { + // Whilst not strictly necessary it helps to clean up soft reference caches + // early rather than waiting for memory limits to be reached + ResolvableType.clearCache(); + cleanCachedIntrospectionResultsCache(); + ReflectionUtils.clearCache(); + clearAnnotationUtilsCache(); + } + + private void cleanCachedIntrospectionResultsCache() { + clear(CachedIntrospectionResults.class, "acceptedClassLoaders"); + clear(CachedIntrospectionResults.class, "strongClassCache"); + clear(CachedIntrospectionResults.class, "softClassCache"); + } + + private void clearAnnotationUtilsCache() { + try { + AnnotationUtils.clearCache(); + } + catch (Throwable ex) { + clear(AnnotationUtils.class, "findAnnotationCache"); + clear(AnnotationUtils.class, "annotatedInterfaceCache"); + } + } + + private void clear(Class type, String fieldName) { + try { + Field field = type.getDeclaredField(fieldName); + field.setAccessible(true); + Object instance = field.get(null); + if (instance instanceof Set) { + ((Set) instance).clear(); + } + if (instance instanceof Map) { + ((Map) instance).keySet().removeIf(this::isFromRestartClassLoader); + } + } + catch (Exception ex) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Unable to clear field " + type + " " + fieldName, ex); + } + } + } + + private boolean isFromRestartClassLoader(Object object) { + return (object instanceof Class && ((Class) object).getClassLoader() instanceof RestartClassLoader); + } + + /** + * Cleanup any soft/weak references by forcing an {@link OutOfMemoryError} error. + */ + private void forceReferenceCleanup() { + try { + final List memory = new LinkedList<>(); + while (true) { + memory.add(new long[102400]); + } + } + catch (OutOfMemoryError ex) { + // Expected + } + } + + /** + * Called to finish {@link Restarter} initialization when application logging is + * available. + */ + void finish() { + synchronized (this.monitor) { + if (!isFinished()) { + this.logger = DeferredLog.replay(this.logger, LogFactory.getLog(getClass())); + this.finished = true; + } + } + } + + boolean isFinished() { + synchronized (this.monitor) { + return this.finished; + } + } + + private LeakSafeThread getLeakSafeThread() { + try { + return this.leakSafeThreads.takeFirst(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + } + + public Object getOrAddAttribute(String name, final ObjectFactory objectFactory) { + Object value = this.attributes.get(name); + if (value == null) { + value = objectFactory.getObject(); + this.attributes.put(name, value); + } + return value; + } + + public Object removeAttribute(String name) { + return this.attributes.remove(name); + } + + /** + * Return the initial set of URLs as configured by the {@link RestartInitializer}. + * @return the initial URLs or {@code null} + */ + public URL[] getInitialUrls() { + return this.initialUrls; + } + + /** + * Initialize and disable restart support. + */ + public static void disable() { + initialize(NO_ARGS, false, RestartInitializer.NONE); + getInstance().setEnabled(false); + } + + /** + * Initialize restart support. See + * {@link #initialize(String[], boolean, RestartInitializer)} for details. + * @param args main application arguments + * @see #initialize(String[], boolean, RestartInitializer) + */ + public static void initialize(String[] args) { + initialize(args, false, new DefaultRestartInitializer()); + } + + /** + * Initialize restart support. See + * {@link #initialize(String[], boolean, RestartInitializer)} for details. + * @param args main application arguments + * @param initializer the restart initializer + * @see #initialize(String[], boolean, RestartInitializer) + */ + public static void initialize(String[] args, RestartInitializer initializer) { + initialize(args, false, initializer, true); + } + + /** + * Initialize restart support. See + * {@link #initialize(String[], boolean, RestartInitializer)} for details. + * @param args main application arguments + * @param forceReferenceCleanup if forcing of soft/weak reference should happen on + * @see #initialize(String[], boolean, RestartInitializer) + */ + public static void initialize(String[] args, boolean forceReferenceCleanup) { + initialize(args, forceReferenceCleanup, new DefaultRestartInitializer()); + } + + /** + * Initialize restart support. See + * {@link #initialize(String[], boolean, RestartInitializer, boolean)} for details. + * @param args main application arguments + * @param forceReferenceCleanup if forcing of soft/weak reference should happen on + * @param initializer the restart initializer + * @see #initialize(String[], boolean, RestartInitializer) + */ + public static void initialize(String[] args, boolean forceReferenceCleanup, RestartInitializer initializer) { + initialize(args, forceReferenceCleanup, initializer, true); + } + + /** + * Initialize restart support for the current application. Called automatically by + * {@link RestartApplicationListener} but can also be called directly if main + * application arguments are not the same as those passed to the + * {@link SpringApplication}. + * @param args main application arguments + * @param forceReferenceCleanup if forcing of soft/weak reference should happen on + * each restart. This will slow down restarts and is intended primarily for testing + * @param initializer the restart initializer + * @param restartOnInitialize if the restarter should be restarted immediately when + * the {@link RestartInitializer} returns non {@code null} results + */ + public static void initialize(String[] args, boolean forceReferenceCleanup, RestartInitializer initializer, + boolean restartOnInitialize) { + Restarter localInstance = null; + synchronized (INSTANCE_MONITOR) { + if (instance == null) { + localInstance = new Restarter(Thread.currentThread(), args, forceReferenceCleanup, initializer); + instance = localInstance; + } + } + if (localInstance != null) { + localInstance.initialize(restartOnInitialize); + } + } + + /** + * Return the active {@link Restarter} instance. Cannot be called before + * {@link #initialize(String[]) initialization}. + * @return the restarter + */ + public static Restarter getInstance() { + synchronized (INSTANCE_MONITOR) { + Assert.state(instance != null, "Restarter has not been initialized"); + return instance; + } + } + + /** + * Set the restarter instance (useful for testing). + * @param instance the instance to set + */ + static void setInstance(Restarter instance) { + synchronized (INSTANCE_MONITOR) { + Restarter.instance = instance; + } + } + + /** + * Clear the instance. Primarily provided for tests and not usually used in + * application code. + */ + public static void clearInstance() { + synchronized (INSTANCE_MONITOR) { + instance = null; + } + } + + /** + * Thread that is created early so not to retain the {@link RestartClassLoader}. + */ + private class LeakSafeThread extends Thread { + + private Callable callable; + + private Object result; + + LeakSafeThread() { + setDaemon(false); + } + + void call(Callable callable) { + this.callable = callable; + start(); + } + + @SuppressWarnings("unchecked") + V callAndWait(Callable callable) { + this.callable = callable; + start(); + try { + join(); + return (V) this.result; + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(ex); + } + } + + @Override + public void run() { + // We are safe to refresh the ActionThread (and indirectly call + // AccessController.getContext()) since our stack doesn't include the + // RestartClassLoader + try { + Restarter.this.leakSafeThreads.put(new LeakSafeThread()); + this.result = this.callable.call(); + } + catch (Exception ex) { + ex.printStackTrace(); + System.exit(1); + } + } + + } + + /** + * {@link ThreadFactory} that creates a leak safe thread. + */ + private final class LeakSafeThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(Runnable runnable) { + return getLeakSafeThread().callAndWait(() -> { + Thread thread = new Thread(runnable); + thread.setContextClassLoader(Restarter.this.applicationClassLoader); + return thread; + }); + } + + } + +} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java b/src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java new file mode 100644 index 0000000..e0621ce --- /dev/null +++ b/src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java @@ -0,0 +1,99 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.freya02.botcommands.internal.restart; + +import java.lang.Thread.UncaughtExceptionHandler; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +/** + * {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently. + * + * @author Phillip Webb + * @author Andy Wilkinson + */ +class SilentExitExceptionHandler implements UncaughtExceptionHandler { + + private final UncaughtExceptionHandler delegate; + + SilentExitExceptionHandler(UncaughtExceptionHandler delegate) { + this.delegate = delegate; + } + + @Override + public void uncaughtException(Thread thread, Throwable exception) { + if (exception instanceof SilentExitException || (exception instanceof InvocationTargetException targetException + && targetException.getTargetException() instanceof SilentExitException)) { + if (isJvmExiting(thread)) { + preventNonZeroExitCode(); + } + return; + } + if (this.delegate != null) { + this.delegate.uncaughtException(thread, exception); + } + } + + private boolean isJvmExiting(Thread exceptionThread) { + for (Thread thread : getAllThreads()) { + if (thread != exceptionThread && thread.isAlive() && !thread.isDaemon()) { + return false; + } + } + return true; + } + + protected Thread[] getAllThreads() { + ThreadGroup rootThreadGroup = getRootThreadGroup(); + Thread[] threads = new Thread[32]; + int count = rootThreadGroup.enumerate(threads); + while (count == threads.length) { + threads = new Thread[threads.length * 2]; + count = rootThreadGroup.enumerate(threads); + } + return Arrays.copyOf(threads, count); + } + + private ThreadGroup getRootThreadGroup() { + ThreadGroup candidate = Thread.currentThread().getThreadGroup(); + while (candidate.getParent() != null) { + candidate = candidate.getParent(); + } + return candidate; + } + + protected void preventNonZeroExitCode() { + System.exit(0); + } + + static void setup(Thread thread) { + UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler(); + if (!(handler instanceof SilentExitExceptionHandler)) { + handler = new SilentExitExceptionHandler(handler); + thread.setUncaughtExceptionHandler(handler); + } + } + + static void exitCurrentThread() { + throw new SilentExitException(); + } + + private static final class SilentExitException extends RuntimeException { + + } + +} From 5198bbc186a549f27d46e2656a6322e28420aee2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 30 May 2025 12:12:41 +0200 Subject: [PATCH 02/68] Set Kotlin jvmTarget --- build.gradle.kts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 41baddd..07e89cc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { kotlin("jvm") version "2.1.20" `maven-publish` @@ -26,6 +28,12 @@ java { targetCompatibility = JavaVersion.VERSION_21 } +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_21 + } +} + publishing { publications { create("maven") { From 6f33b596e61ff6d4106167b13ba4c7100e6cb5b9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 30 May 2025 13:03:17 +0200 Subject: [PATCH 03/68] Add RestartListener, remove unused code --- .../internal/restart/Restarter.java | 320 ++---------------- .../internal/restart/RestartListener.kt | 5 + 2 files changed, 31 insertions(+), 294 deletions(-) create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java b/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java index bb6aef2..343c062 100644 --- a/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java +++ b/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java @@ -17,82 +17,42 @@ package dev.freya02.botcommands.internal.restart; import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.springframework.beans.CachedIntrospectionResults; -import org.springframework.beans.factory.ObjectFactory; import org.springframework.boot.SpringApplication; -import org.springframework.boot.devtools.restart.*; +import org.springframework.boot.devtools.restart.FailureHandler; import org.springframework.boot.devtools.restart.FailureHandler.Outcome; +import org.springframework.boot.devtools.restart.RestartApplicationListener; +import org.springframework.boot.devtools.restart.RestartInitializer; import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; import org.springframework.boot.devtools.restart.classloader.RestartClassLoader; import org.springframework.boot.logging.DeferredLog; -import org.springframework.cglib.core.ClassNameReader; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; -import org.springframework.util.ReflectionUtils; -import java.beans.Introspector; import java.lang.Thread.UncaughtExceptionHandler; -import java.lang.reflect.Field; import java.net.URL; -import java.util.*; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; import java.util.concurrent.*; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; -/** - * Allows a running application to be restarted with an updated classpath. The restarter - * works by creating a new application ClassLoader that is split into two parts. The top - * part contains static URLs that don't change (for example 3rd party libraries and Spring - * Boot itself) and the bottom part contains URLs where classes and resources might be - * updated. - *

- * The Restarter should be {@link #initialize(String[]) initialized} early to ensure that - * classes are loaded multiple times. Mostly the {@link RestartApplicationListener} can be - * relied upon to perform initialization, however, you may need to call - * {@link #initialize(String[])} directly if your SpringApplication arguments are not - * identical to your main method arguments. - *

- * By default, applications running in an IDE (i.e. those not packaged as "uber jars") - * will automatically detect URLs that can change. It's also possible to manually - * configure URLs or class file updates for remote restart scenarios. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.3.0 - * @see RestartApplicationListener - * @see #initialize(String[]) - * @see #getInstance() - * @see #restart() - */ public class Restarter { private static final Object INSTANCE_MONITOR = new Object(); - private static final String[] NO_ARGS = {}; - private static Restarter instance; private final Set urls = new LinkedHashSet<>(); private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles(); - private final Map attributes = new ConcurrentHashMap<>(); - private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque<>(); private final Lock stopLock = new ReentrantLock(); - private final Object monitor = new Object(); - private Log logger = new DeferredLog(); - private final boolean forceReferenceCleanup; - - private boolean enabled = true; - private final URL[] initialUrls; private final String mainClassName; @@ -103,27 +63,23 @@ public class Restarter { private final UncaughtExceptionHandler exceptionHandler; - private boolean finished = false; - - private final List rootContexts = new CopyOnWriteArrayList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); /** * Internal constructor to create a new {@link Restarter} instance. * @param thread the source thread * @param args the application arguments - * @param forceReferenceCleanup if soft/weak reference cleanup should be forced * @param initializer the restart initializer * @see #initialize(String[]) */ - protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup, RestartInitializer initializer) { - Assert.notNull(thread, "Thread must not be null"); + protected Restarter(Thread thread, String[] args, RestartInitializer initializer) { + Assert.notNull(thread, "Thread must not be null"); Assert.notNull(args, "Args must not be null"); Assert.notNull(initializer, "Initializer must not be null"); - if (this.logger.isDebugEnabled()) { - this.logger.debug("Creating new Restarter for thread " + thread); + if (logger.isDebugEnabled()) { + logger.debug("Creating new Restarter for thread " + thread); } SilentExitExceptionHandler.setup(thread); - this.forceReferenceCleanup = forceReferenceCleanup; this.initialUrls = initializer.getInitialUrls(thread); this.mainClassName = getMainClassName(thread); this.applicationClassLoader = thread.getContextClassLoader(); @@ -132,6 +88,10 @@ protected Restarter(Thread thread, String[] args, boolean forceReferenceCleanup, this.leakSafeThreads.add(new LeakSafeThread()); } + public void addListener(RestartListener listener) { + listeners.add(listener); + } + private String getMainClassName(Thread thread) { try { return new MainMethod(thread).getDeclaringClassName(); @@ -142,7 +102,6 @@ private String getMainClassName(Thread thread) { } protected void initialize(boolean restartOnInitialize) { - preInitializeLeakyClasses(); if (this.initialUrls != null) { this.urls.addAll(Arrays.asList(this.initialUrls)); if (restartOnInitialize) { @@ -156,7 +115,6 @@ private void immediateRestart() { try { getLeakSafeThread().callAndWait(() -> { start(FailureHandler.NONE); - cleanupCaches(); return null; }); } @@ -166,48 +124,6 @@ private void immediateRestart() { SilentExitExceptionHandler.exitCurrentThread(); } - /** - * CGLIB has a private exception field which needs to initialized early to ensure that - * the stacktrace doesn't retain a reference to the RestartClassLoader. - */ - private void preInitializeLeakyClasses() { - try { - Class readerClass = ClassNameReader.class; - Field field = readerClass.getDeclaredField("EARLY_EXIT"); - field.setAccessible(true); - ((Throwable) field.get(null)).fillInStackTrace(); - } - catch (Exception ex) { - this.logger.warn("Unable to pre-initialize classes", ex); - } - } - - /** - * Set if restart support is enabled. - * @param enabled if restart support is enabled - */ - private void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - /** - * Add additional URLs to be includes in the next restart. - * @param urls the urls to add - */ - public void addUrls(Collection urls) { - Assert.notNull(urls, "Urls must not be null"); - this.urls.addAll(urls); - } - - /** - * Add additional {@link ClassLoaderFiles} to be included in the next restart. - * @param classLoaderFiles the files to add - */ - public void addClassLoaderFiles(ClassLoaderFiles classLoaderFiles) { - Assert.notNull(classLoaderFiles, "ClassLoaderFiles must not be null"); - this.classLoaderFiles.addAll(classLoaderFiles); - } - /** * Return a {@link ThreadFactory} that can be used to create leak safe threads. * @return a leak safe thread factory @@ -228,10 +144,6 @@ public void restart() { * @param failureHandler a failure handler to deal with application that doesn't start */ public void restart(FailureHandler failureHandler) { - if (!this.enabled) { - this.logger.debug("Application restart is disabled"); - return; - } this.logger.debug("Restarting application"); getLeakSafeThread().call(() -> { Restarter.this.stop(); @@ -285,113 +197,20 @@ protected Throwable relaunch(ClassLoader classLoader) throws Exception { /** * Stop the application. - * @throws Exception in case of errors - */ - protected void stop() throws Exception { + */ + protected void stop() { this.logger.debug("Stopping application"); this.stopLock.lock(); try { - for (ConfigurableApplicationContext context : this.rootContexts) { - context.close(); - this.rootContexts.remove(context); - } - cleanupCaches(); - if (this.forceReferenceCleanup) { - forceReferenceCleanup(); + for (RestartListener listener : listeners) { + listener.beforeStop(); } + listeners.clear(); } finally { this.stopLock.unlock(); } System.gc(); - System.runFinalization(); - } - - private void cleanupCaches() { - Introspector.flushCaches(); - cleanupKnownCaches(); - } - - private void cleanupKnownCaches() { - // Whilst not strictly necessary it helps to clean up soft reference caches - // early rather than waiting for memory limits to be reached - ResolvableType.clearCache(); - cleanCachedIntrospectionResultsCache(); - ReflectionUtils.clearCache(); - clearAnnotationUtilsCache(); - } - - private void cleanCachedIntrospectionResultsCache() { - clear(CachedIntrospectionResults.class, "acceptedClassLoaders"); - clear(CachedIntrospectionResults.class, "strongClassCache"); - clear(CachedIntrospectionResults.class, "softClassCache"); - } - - private void clearAnnotationUtilsCache() { - try { - AnnotationUtils.clearCache(); - } - catch (Throwable ex) { - clear(AnnotationUtils.class, "findAnnotationCache"); - clear(AnnotationUtils.class, "annotatedInterfaceCache"); - } - } - - private void clear(Class type, String fieldName) { - try { - Field field = type.getDeclaredField(fieldName); - field.setAccessible(true); - Object instance = field.get(null); - if (instance instanceof Set) { - ((Set) instance).clear(); - } - if (instance instanceof Map) { - ((Map) instance).keySet().removeIf(this::isFromRestartClassLoader); - } - } - catch (Exception ex) { - if (this.logger.isDebugEnabled()) { - this.logger.debug("Unable to clear field " + type + " " + fieldName, ex); - } - } - } - - private boolean isFromRestartClassLoader(Object object) { - return (object instanceof Class && ((Class) object).getClassLoader() instanceof RestartClassLoader); - } - - /** - * Cleanup any soft/weak references by forcing an {@link OutOfMemoryError} error. - */ - private void forceReferenceCleanup() { - try { - final List memory = new LinkedList<>(); - while (true) { - memory.add(new long[102400]); - } - } - catch (OutOfMemoryError ex) { - // Expected - } - } - - /** - * Called to finish {@link Restarter} initialization when application logging is - * available. - */ - void finish() { - synchronized (this.monitor) { - if (!isFinished()) { - this.logger = DeferredLog.replay(this.logger, LogFactory.getLog(getClass())); - this.finished = true; - } - } - } - - boolean isFinished() { - synchronized (this.monitor) { - return this.finished; - } } private LeakSafeThread getLeakSafeThread() { @@ -404,77 +223,12 @@ private LeakSafeThread getLeakSafeThread() { } } - public Object getOrAddAttribute(String name, final ObjectFactory objectFactory) { - Object value = this.attributes.get(name); - if (value == null) { - value = objectFactory.getObject(); - this.attributes.put(name, value); - } - return value; - } - - public Object removeAttribute(String name) { - return this.attributes.remove(name); - } - - /** - * Return the initial set of URLs as configured by the {@link RestartInitializer}. - * @return the initial URLs or {@code null} - */ - public URL[] getInitialUrls() { - return this.initialUrls; - } - - /** - * Initialize and disable restart support. - */ - public static void disable() { - initialize(NO_ARGS, false, RestartInitializer.NONE); - getInstance().setEnabled(false); - } - - /** - * Initialize restart support. See - * {@link #initialize(String[], boolean, RestartInitializer)} for details. - * @param args main application arguments - * @see #initialize(String[], boolean, RestartInitializer) - */ public static void initialize(String[] args) { - initialize(args, false, new DefaultRestartInitializer()); + initialize(args, new DefaultRestartInitializer()); } - /** - * Initialize restart support. See - * {@link #initialize(String[], boolean, RestartInitializer)} for details. - * @param args main application arguments - * @param initializer the restart initializer - * @see #initialize(String[], boolean, RestartInitializer) - */ public static void initialize(String[] args, RestartInitializer initializer) { - initialize(args, false, initializer, true); - } - - /** - * Initialize restart support. See - * {@link #initialize(String[], boolean, RestartInitializer)} for details. - * @param args main application arguments - * @param forceReferenceCleanup if forcing of soft/weak reference should happen on - * @see #initialize(String[], boolean, RestartInitializer) - */ - public static void initialize(String[] args, boolean forceReferenceCleanup) { - initialize(args, forceReferenceCleanup, new DefaultRestartInitializer()); - } - - /** - * Initialize restart support. See - * {@link #initialize(String[], boolean, RestartInitializer, boolean)} for details. - * @param args main application arguments - * @param forceReferenceCleanup if forcing of soft/weak reference should happen on - * @param initializer the restart initializer - * @see #initialize(String[], boolean, RestartInitializer) - */ - public static void initialize(String[] args, boolean forceReferenceCleanup, RestartInitializer initializer) { - initialize(args, forceReferenceCleanup, initializer, true); + initialize(args, initializer, true); } /** @@ -483,18 +237,16 @@ public static void initialize(String[] args, boolean forceReferenceCleanup, Rest * application arguments are not the same as those passed to the * {@link SpringApplication}. * @param args main application arguments - * @param forceReferenceCleanup if forcing of soft/weak reference should happen on - * each restart. This will slow down restarts and is intended primarily for testing * @param initializer the restart initializer * @param restartOnInitialize if the restarter should be restarted immediately when * the {@link RestartInitializer} returns non {@code null} results */ - public static void initialize(String[] args, boolean forceReferenceCleanup, RestartInitializer initializer, - boolean restartOnInitialize) { + public static void initialize(String[] args, RestartInitializer initializer, + boolean restartOnInitialize) { Restarter localInstance = null; synchronized (INSTANCE_MONITOR) { if (instance == null) { - localInstance = new Restarter(Thread.currentThread(), args, forceReferenceCleanup, initializer); + localInstance = new Restarter(Thread.currentThread(), args, initializer); instance = localInstance; } } @@ -515,26 +267,6 @@ public static Restarter getInstance() { } } - /** - * Set the restarter instance (useful for testing). - * @param instance the instance to set - */ - static void setInstance(Restarter instance) { - synchronized (INSTANCE_MONITOR) { - Restarter.instance = instance; - } - } - - /** - * Clear the instance. Primarily provided for tests and not usually used in - * application code. - */ - public static void clearInstance() { - synchronized (INSTANCE_MONITOR) { - instance = null; - } - } - /** * Thread that is created early so not to retain the {@link RestartClassLoader}. */ diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt new file mode 100644 index 0000000..4d77390 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt @@ -0,0 +1,5 @@ +package dev.freya02.botcommands.internal.restart + +interface RestartListener { + fun beforeStop() +} \ No newline at end of file From 74cf5086758097f4fc00970ae542d9c7732b0003 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 30 May 2025 16:18:16 +0200 Subject: [PATCH 04/68] Publish source JAR, add kotlin-logging --- build.gradle.kts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 07e89cc..fc8497f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -13,6 +13,8 @@ repositories { } dependencies { + implementation("io.github.oshai:kotlin-logging-jvm:7.0.3") + implementation("org.springframework.boot:spring-boot-starter:3.4.2") implementation("org.springframework.boot:spring-boot-devtools:3.4.2") @@ -26,6 +28,8 @@ tasks.test { java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 + + withSourcesJar() } kotlin { From 10603d50712f7ce31b68e0c636bff0a6e0761c7e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 30 May 2025 16:18:31 +0200 Subject: [PATCH 05/68] Rewrite Restarter + LeakSafeExecutor --- .../internal/restart/Restarter.java | 335 ------------------ .../internal/restart/LeakSafeExecutor.kt | 69 ++++ .../botcommands/internal/restart/Restarter.kt | 112 ++++++ 3 files changed, 181 insertions(+), 335 deletions(-) delete mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java b/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java deleted file mode 100644 index 343c062..0000000 --- a/src/main/java/dev/freya02/botcommands/internal/restart/Restarter.java +++ /dev/null @@ -1,335 +0,0 @@ -/* - * Copyright 2012-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.freya02.botcommands.internal.restart; - -import org.apache.commons.logging.Log; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.devtools.restart.FailureHandler; -import org.springframework.boot.devtools.restart.FailureHandler.Outcome; -import org.springframework.boot.devtools.restart.RestartApplicationListener; -import org.springframework.boot.devtools.restart.RestartInitializer; -import org.springframework.boot.devtools.restart.classloader.ClassLoaderFiles; -import org.springframework.boot.devtools.restart.classloader.RestartClassLoader; -import org.springframework.boot.logging.DeferredLog; -import org.springframework.util.Assert; - -import java.lang.Thread.UncaughtExceptionHandler; -import java.net.URL; -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.*; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -public class Restarter { - - private static final Object INSTANCE_MONITOR = new Object(); - - private static Restarter instance; - - private final Set urls = new LinkedHashSet<>(); - - private final ClassLoaderFiles classLoaderFiles = new ClassLoaderFiles(); - - private final BlockingDeque leakSafeThreads = new LinkedBlockingDeque<>(); - - private final Lock stopLock = new ReentrantLock(); - - private Log logger = new DeferredLog(); - - private final URL[] initialUrls; - - private final String mainClassName; - - private final ClassLoader applicationClassLoader; - - private final String[] args; - - private final UncaughtExceptionHandler exceptionHandler; - - private final List listeners = new CopyOnWriteArrayList<>(); - - /** - * Internal constructor to create a new {@link Restarter} instance. - * @param thread the source thread - * @param args the application arguments - * @param initializer the restart initializer - * @see #initialize(String[]) - */ - protected Restarter(Thread thread, String[] args, RestartInitializer initializer) { - Assert.notNull(thread, "Thread must not be null"); - Assert.notNull(args, "Args must not be null"); - Assert.notNull(initializer, "Initializer must not be null"); - if (logger.isDebugEnabled()) { - logger.debug("Creating new Restarter for thread " + thread); - } - SilentExitExceptionHandler.setup(thread); - this.initialUrls = initializer.getInitialUrls(thread); - this.mainClassName = getMainClassName(thread); - this.applicationClassLoader = thread.getContextClassLoader(); - this.args = args; - this.exceptionHandler = thread.getUncaughtExceptionHandler(); - this.leakSafeThreads.add(new LeakSafeThread()); - } - - public void addListener(RestartListener listener) { - listeners.add(listener); - } - - private String getMainClassName(Thread thread) { - try { - return new MainMethod(thread).getDeclaringClassName(); - } - catch (Exception ex) { - return null; - } - } - - protected void initialize(boolean restartOnInitialize) { - if (this.initialUrls != null) { - this.urls.addAll(Arrays.asList(this.initialUrls)); - if (restartOnInitialize) { - this.logger.debug("Immediately restarting application"); - immediateRestart(); - } - } - } - - private void immediateRestart() { - try { - getLeakSafeThread().callAndWait(() -> { - start(FailureHandler.NONE); - return null; - }); - } - catch (Exception ex) { - this.logger.warn("Unable to initialize restarter", ex); - } - SilentExitExceptionHandler.exitCurrentThread(); - } - - /** - * Return a {@link ThreadFactory} that can be used to create leak safe threads. - * @return a leak safe thread factory - */ - public ThreadFactory getThreadFactory() { - return new LeakSafeThreadFactory(); - } - - /** - * Restart the running application. - */ - public void restart() { - restart(FailureHandler.NONE); - } - - /** - * Restart the running application. - * @param failureHandler a failure handler to deal with application that doesn't start - */ - public void restart(FailureHandler failureHandler) { - this.logger.debug("Restarting application"); - getLeakSafeThread().call(() -> { - Restarter.this.stop(); - Restarter.this.start(failureHandler); - return null; - }); - } - - /** - * Start the application. - * @param failureHandler a failure handler for application that won't start - * @throws Exception in case of errors - */ - protected void start(FailureHandler failureHandler) throws Exception { - do { - Throwable error = doStart(); - if (error == null) { - return; - } - if (failureHandler.handle(error) == Outcome.ABORT) { - return; - } - } - while (true); - } - - private Throwable doStart() throws Exception { - Assert.notNull(this.mainClassName, "Unable to find the main class to restart"); - URL[] urls = this.urls.toArray(new URL[0]); - ClassLoaderFiles updatedFiles = new ClassLoaderFiles(this.classLoaderFiles); - ClassLoader classLoader = new RestartClassLoader(this.applicationClassLoader, urls, updatedFiles); - if (this.logger.isDebugEnabled()) { - this.logger.debug("Starting application " + this.mainClassName + " with URLs " + Arrays.asList(urls)); - } - return relaunch(classLoader); - } - - /** - * Relaunch the application using the specified classloader. - * @param classLoader the classloader to use - * @return any exception that caused the launch to fail or {@code null} - * @throws Exception in case of errors - */ - protected Throwable relaunch(ClassLoader classLoader) throws Exception { - RestartLauncher launcher = new RestartLauncher(classLoader, this.mainClassName, this.args, - this.exceptionHandler); - launcher.start(); - launcher.join(); - return launcher.getError(); - } - - /** - * Stop the application. - */ - protected void stop() { - this.logger.debug("Stopping application"); - this.stopLock.lock(); - try { - for (RestartListener listener : listeners) { - listener.beforeStop(); - } - listeners.clear(); - } - finally { - this.stopLock.unlock(); - } - System.gc(); - } - - private LeakSafeThread getLeakSafeThread() { - try { - return this.leakSafeThreads.takeFirst(); - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(ex); - } - } - - public static void initialize(String[] args) { - initialize(args, new DefaultRestartInitializer()); - } - - public static void initialize(String[] args, RestartInitializer initializer) { - initialize(args, initializer, true); - } - - /** - * Initialize restart support for the current application. Called automatically by - * {@link RestartApplicationListener} but can also be called directly if main - * application arguments are not the same as those passed to the - * {@link SpringApplication}. - * @param args main application arguments - * @param initializer the restart initializer - * @param restartOnInitialize if the restarter should be restarted immediately when - * the {@link RestartInitializer} returns non {@code null} results - */ - public static void initialize(String[] args, RestartInitializer initializer, - boolean restartOnInitialize) { - Restarter localInstance = null; - synchronized (INSTANCE_MONITOR) { - if (instance == null) { - localInstance = new Restarter(Thread.currentThread(), args, initializer); - instance = localInstance; - } - } - if (localInstance != null) { - localInstance.initialize(restartOnInitialize); - } - } - - /** - * Return the active {@link Restarter} instance. Cannot be called before - * {@link #initialize(String[]) initialization}. - * @return the restarter - */ - public static Restarter getInstance() { - synchronized (INSTANCE_MONITOR) { - Assert.state(instance != null, "Restarter has not been initialized"); - return instance; - } - } - - /** - * Thread that is created early so not to retain the {@link RestartClassLoader}. - */ - private class LeakSafeThread extends Thread { - - private Callable callable; - - private Object result; - - LeakSafeThread() { - setDaemon(false); - } - - void call(Callable callable) { - this.callable = callable; - start(); - } - - @SuppressWarnings("unchecked") - V callAndWait(Callable callable) { - this.callable = callable; - start(); - try { - join(); - return (V) this.result; - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - throw new IllegalStateException(ex); - } - } - - @Override - public void run() { - // We are safe to refresh the ActionThread (and indirectly call - // AccessController.getContext()) since our stack doesn't include the - // RestartClassLoader - try { - Restarter.this.leakSafeThreads.put(new LeakSafeThread()); - this.result = this.callable.call(); - } - catch (Exception ex) { - ex.printStackTrace(); - System.exit(1); - } - } - - } - - /** - * {@link ThreadFactory} that creates a leak safe thread. - */ - private final class LeakSafeThreadFactory implements ThreadFactory { - - @Override - public Thread newThread(Runnable runnable) { - return getLeakSafeThread().callAndWait(() -> { - Thread thread = new Thread(runnable); - thread.setContextClassLoader(Restarter.this.applicationClassLoader); - return thread; - }); - } - - } - -} diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt new file mode 100644 index 0000000..7207ec4 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt @@ -0,0 +1,69 @@ +package dev.freya02.botcommands.internal.restart + +import org.springframework.boot.devtools.restart.classloader.RestartClassLoader +import java.util.concurrent.BlockingDeque +import java.util.concurrent.LinkedBlockingDeque +import kotlin.system.exitProcess + +internal class LeakSafeExecutor internal constructor() { + + // As we can only use a Thread once, we put a single LeakSafeThread in a blocking queue, + // then, when a code block runs, a LeakSafeThread is removed from the queue, + // and the LeakSafeThread recreates a new one for the next code block. + // We use a blocking queue to prevent trying to get a LeakSafeThread between the moment it was retrieved and when it'll be added back + private val leakSafeThreads: BlockingDeque = LinkedBlockingDeque() + + init { + leakSafeThreads += LeakSafeThread() + } + + fun call(callable: () -> Unit): Unit = getLeakSafeThread().call(callable) + + fun callAndWait(callable: () -> V): V = getLeakSafeThread().callAndWait(callable) + + private fun getLeakSafeThread(): LeakSafeThread { + return leakSafeThreads.takeFirst() + } + + /** + * Thread that is created early so not to retain the [RestartClassLoader]. + */ + private inner class LeakSafeThread : Thread() { + + private var callable: (() -> Any?)? = null + + private var result: Any? = null + + init { + isDaemon = false + } + + fun call(callable: () -> Unit) { + this.callable = callable + start() + } + + @Suppress("UNCHECKED_CAST") + fun callAndWait(callable: () -> V): V { + this.callable = callable + start() + try { + join() + return this.result as V + } catch (ex: InterruptedException) { + currentThread().interrupt() + throw IllegalStateException(ex) + } + } + + override fun run() { + try { + this@LeakSafeExecutor.leakSafeThreads.put(LeakSafeThread()) + this.result = this.callable!!.invoke() + } catch (ex: Exception) { + ex.printStackTrace() + exitProcess(1) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt new file mode 100644 index 0000000..259c5f7 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -0,0 +1,112 @@ +package dev.freya02.botcommands.internal.restart + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.boot.devtools.restart.classloader.RestartClassLoader +import java.io.File +import java.lang.management.ManagementFactory +import java.net.URL +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +private val logger = KotlinLogging.logger { } + +class Restarter private constructor( + private val args: Array, +) { + + private val appClassLoader: ClassLoader + private val classpathUrls: Array + + private val mainClassName: String + + private val uncaughtExceptionHandler: Thread.UncaughtExceptionHandler + + private val stopLock: Lock = ReentrantLock() + private val listeners: MutableList = arrayListOf() + + private val leakSafeExecutor = LeakSafeExecutor() + + init { + val thread = Thread.currentThread() + SilentExitExceptionHandler.setup(thread) + + appClassLoader = thread.contextClassLoader + classpathUrls = ManagementFactory.getRuntimeMXBean().classPath + .split(File.pathSeparator) + .map(::File) + .filter { it.isDirectory } + .map { it.toURI().toURL() } + .toTypedArray() + + mainClassName = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .walk { stream -> stream.filter { it.methodName == "main" }.toList().last() } + .declaringClass.name + + uncaughtExceptionHandler = thread.uncaughtExceptionHandler + } + + fun addListener(listener: RestartListener) { + listeners += listener + } + + private fun initialize(): Nothing { + val throwable = leakSafeExecutor.callAndWait { start() } + if (throwable != null) + throw throwable + SilentExitExceptionHandler.exitCurrentThread() + error("Should have thrown") + } + + /** + * Runs each [RestartListener.beforeStop] and then starts a new instance of the main class, + * if the new instance fails, the [Throwable] is returned. + */ + fun restart(): Throwable? { + logger.debug { "Restarting application in '$mainClassName'" } + // Do it from the original class loader, so the context is the same as for the initial restart + return leakSafeExecutor.callAndWait { + stop() + start() + } + } + + private fun stop() { + stopLock.withLock { + listeners.forEach { it.beforeStop() } + listeners.clear() + } + // All threads should be stopped at that point + // so the GC should be able to remove all the previous loaded classes + System.gc() + } + + /** + * Starts a new instance of the main class, or returns a [Throwable] if it failed. + */ + private fun start(): Throwable? { + val restartClassLoader = RestartClassLoader(appClassLoader, classpathUrls) + val launcher = RestartLauncher(restartClassLoader, mainClassName, args, uncaughtExceptionHandler) + launcher.start() + launcher.join() + return launcher.error + } + + companion object { + + private val instanceLock: Lock = ReentrantLock() + lateinit var instance: Restarter + private set + + fun initialize(args: Array) { + var newInstance: Restarter? = null + instanceLock.withLock { + if (::instance.isInitialized.not()) { + newInstance = Restarter(args) + instance = newInstance + } + } + newInstance?.initialize() + } + } +} From ee7e1cc2e6b24779e71c6db8e1c040c345d5604b Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 30 May 2025 18:50:37 +0200 Subject: [PATCH 06/68] Rewrite SilentExitExceptionHandler --- .../internal/restart/ChangeableUrls.java | 187 ------------------ .../restart/DefaultRestartInitializer.java | 83 -------- .../internal/restart/MainMethod.java | 90 --------- .../internal/restart/RestartLauncher.java | 62 ------ .../restart/SilentExitExceptionHandler.java | 99 ---------- .../restart/ImmediateRestartException.kt | 27 +++ .../botcommands/internal/restart/Restarter.kt | 23 ++- 7 files changed, 43 insertions(+), 528 deletions(-) delete mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java delete mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java delete mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java delete mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java delete mode 100644 src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java b/src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java deleted file mode 100644 index 0b683e3..0000000 --- a/src/main/java/dev/freya02/botcommands/internal/restart/ChangeableUrls.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.freya02.botcommands.internal.restart; - -import org.apache.commons.logging.Log; -import org.springframework.boot.devtools.logger.DevToolsLogFactory; -import org.springframework.boot.devtools.settings.DevToolsSettings; -import org.springframework.core.log.LogMessage; -import org.springframework.util.StringUtils; - -import java.io.File; -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.util.*; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.stream.Stream; - -/** - * A filtered collection of URLs which can change after the application has started. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -final class ChangeableUrls implements Iterable { - - private static final Log logger = DevToolsLogFactory.getLog(ChangeableUrls.class); - - private final List urls; - - private ChangeableUrls(URL... urls) { - DevToolsSettings settings = DevToolsSettings.get(); - List reloadableUrls = new ArrayList<>(urls.length); - for (URL url : urls) { - if ((settings.isRestartInclude(url) || isDirectoryUrl(url.toString())) && !settings.isRestartExclude(url)) { - reloadableUrls.add(url); - } - } - if (logger.isDebugEnabled()) { - logger.debug("Matching URLs for reloading : " + reloadableUrls); - } - this.urls = Collections.unmodifiableList(reloadableUrls); - } - - private boolean isDirectoryUrl(String urlString) { - return urlString.startsWith("file:") && urlString.endsWith("/"); - } - - @Override - public Iterator iterator() { - return this.urls.iterator(); - } - - int size() { - return this.urls.size(); - } - - URL[] toArray() { - return this.urls.toArray(new URL[0]); - } - - List toList() { - return Collections.unmodifiableList(this.urls); - } - - @Override - public String toString() { - return this.urls.toString(); - } - - static ChangeableUrls fromClassLoader(ClassLoader classLoader) { - List urls = new ArrayList<>(); - for (URL url : urlsFromClassLoader(classLoader)) { - urls.add(url); - urls.addAll(getUrlsFromClassPathOfJarManifestIfPossible(url)); - } - return fromUrls(urls); - } - - private static URL[] urlsFromClassLoader(ClassLoader classLoader) { - if (classLoader instanceof URLClassLoader urlClassLoader) { - return urlClassLoader.getURLs(); - } - return Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) - .map(ChangeableUrls::toURL) - .toArray(URL[]::new); - } - - private static URL toURL(String classPathEntry) { - try { - return new File(classPathEntry).toURI().toURL(); - } - catch (MalformedURLException ex) { - throw new IllegalArgumentException("URL could not be created from '" + classPathEntry + "'", ex); - } - } - - private static List getUrlsFromClassPathOfJarManifestIfPossible(URL url) { - try { - File file = new File(url.toURI()); - if (file.isFile()) { - try (JarFile jarFile = new JarFile(file)) { - try { - return getUrlsFromManifestClassPathAttribute(url, jarFile); - } - catch (IOException ex) { - throw new IllegalStateException( - "Failed to read Class-Path attribute from manifest of jar " + url, ex); - } - } - } - } - catch (Exception ex) { - // Assume it's not a jar and continue - } - return Collections.emptyList(); - } - - private static List getUrlsFromManifestClassPathAttribute(URL jarUrl, JarFile jarFile) throws IOException { - Manifest manifest = jarFile.getManifest(); - if (manifest == null) { - return Collections.emptyList(); - } - String classPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); - if (!StringUtils.hasText(classPath)) { - return Collections.emptyList(); - } - String[] entries = StringUtils.delimitedListToStringArray(classPath, " "); - List urls = new ArrayList<>(entries.length); - List nonExistentEntries = new ArrayList<>(); - for (String entry : entries) { - try { - URL referenced = new URL(jarUrl, entry); - if (new File(referenced.getFile()).exists()) { - urls.add(referenced); - } - else { - referenced = new URL(jarUrl, URLDecoder.decode(entry, StandardCharsets.UTF_8)); - if (new File(referenced.getFile()).exists()) { - urls.add(referenced); - } - else { - nonExistentEntries.add(referenced); - } - } - } - catch (MalformedURLException ex) { - throw new IllegalStateException("Class-Path attribute contains malformed URL", ex); - } - } - if (!nonExistentEntries.isEmpty()) { - logger.info(LogMessage.of(() -> "The Class-Path manifest attribute in " + jarFile.getName() - + " referenced one or more files that do not exist: " - + StringUtils.collectionToCommaDelimitedString(nonExistentEntries))); - } - return urls; - } - - static ChangeableUrls fromUrls(Collection urls) { - return fromUrls(new ArrayList<>(urls).toArray(new URL[urls.size()])); - } - - static ChangeableUrls fromUrls(URL... urls) { - return new ChangeableUrls(urls); - } - -} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java b/src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java deleted file mode 100644 index 73f4627..0000000 --- a/src/main/java/dev/freya02/botcommands/internal/restart/DefaultRestartInitializer.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.freya02.botcommands.internal.restart; - -import org.springframework.boot.devtools.restart.RestartInitializer; - -import java.net.URL; - -/** - * Default {@link RestartInitializer} that only enable initial restart when running a - * standard "main" method. Skips initialization when running "fat" jars (included - * exploded) or when running from a test. - * - * @author Phillip Webb - * @author Andy Wilkinson - * @since 1.3.0 - */ -public class DefaultRestartInitializer implements RestartInitializer { - - @Override - public URL[] getInitialUrls(Thread thread) { - return getUrls(thread); - } - - /** - * Returns if the thread is for a main invocation. By default {@link #isMain(Thread) - * checks the name of the thread} and {@link #isDevelopmentClassLoader(ClassLoader) - * the context classloader}. - * @param thread the thread to check - * @return {@code true} if the thread is a main invocation - * @see #isMainThread - * @see #isDevelopmentClassLoader(ClassLoader) - */ - protected boolean isMain(Thread thread) { - return isMainThread(thread) && isDevelopmentClassLoader(thread.getContextClassLoader()); - } - - /** - * Returns whether the given {@code thread} is considered to be the main thread. - * @param thread the thread to check - * @return {@code true} if it's the main thread, otherwise {@code false} - * @since 2.4.0 - */ - protected boolean isMainThread(Thread thread) { - return thread.getName().equals("main"); - } - - /** - * Returns whether the given {@code classLoader} is one that is typically used during - * development. - * @param classLoader the ClassLoader to check - * @return {@code true} if it's a ClassLoader typically used during development, - * otherwise {@code false} - * @since 2.4.0 - */ - protected boolean isDevelopmentClassLoader(ClassLoader classLoader) { - return classLoader.getClass().getName().contains("AppClassLoader"); - } - - /** - * Return the URLs that should be used with initialization. - * @param thread the source thread - * @return the URLs - */ - protected URL[] getUrls(Thread thread) { - return ChangeableUrls.fromClassLoader(thread.getContextClassLoader()).toArray(); - } - -} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java b/src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java deleted file mode 100644 index fe3e92b..0000000 --- a/src/main/java/dev/freya02/botcommands/internal/restart/MainMethod.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright 2012-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.freya02.botcommands.internal.restart; - -import org.springframework.util.Assert; - -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; - -/** - * The "main" method located from a running thread. - * - * @author Phillip Webb - */ -class MainMethod { - - private final Method method; - - MainMethod() { - this(Thread.currentThread()); - } - - MainMethod(Thread thread) { - Assert.notNull(thread, "Thread must not be null"); - this.method = getMainMethod(thread); - } - - private Method getMainMethod(Thread thread) { - StackTraceElement[] stackTrace = thread.getStackTrace(); - for (int i = stackTrace.length - 1; i >= 0; i--) { - StackTraceElement element = stackTrace[i]; - if ("main".equals(element.getMethodName()) && !isLoaderClass(element.getClassName())) { - Method method = getMainMethod(element); - if (method != null) { - return method; - } - } - } - throw new IllegalStateException("Unable to find main method"); - } - - private boolean isLoaderClass(String className) { - return className.startsWith("org.springframework.boot.loader."); - } - - private Method getMainMethod(StackTraceElement element) { - try { - Class elementClass = Class.forName(element.getClassName()); - Method method = elementClass.getDeclaredMethod("main", String[].class); - if (Modifier.isStatic(method.getModifiers())) { - return method; - } - } - catch (Exception ex) { - // Ignore - } - return null; - } - - /** - * Returns the actual main method. - * @return the main method - */ - Method getMethod() { - return this.method; - } - - /** - * Return the name of the declaring class. - * @return the declaring class name - */ - String getDeclaringClassName() { - return this.method.getDeclaringClass().getName(); - } - -} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java b/src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java deleted file mode 100644 index b30fe5f..0000000 --- a/src/main/java/dev/freya02/botcommands/internal/restart/RestartLauncher.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2012-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.freya02.botcommands.internal.restart; - -import java.lang.reflect.Method; - -/** - * Thread used to launch a restarted application. - * - * @author Phillip Webb - */ -class RestartLauncher extends Thread { - - private final String mainClassName; - - private final String[] args; - - private Throwable error; - - RestartLauncher(ClassLoader classLoader, String mainClassName, String[] args, - UncaughtExceptionHandler exceptionHandler) { - this.mainClassName = mainClassName; - this.args = args; - setName("restartedMain"); - setUncaughtExceptionHandler(exceptionHandler); - setDaemon(false); - setContextClassLoader(classLoader); - } - - @Override - public void run() { - try { - Class mainClass = Class.forName(this.mainClassName, false, getContextClassLoader()); - Method mainMethod = mainClass.getDeclaredMethod("main", String[].class); - mainMethod.setAccessible(true); - mainMethod.invoke(null, new Object[] { this.args }); - } - catch (Throwable ex) { - this.error = ex; - getUncaughtExceptionHandler().uncaughtException(this, ex); - } - } - - Throwable getError() { - return this.error; - } - -} diff --git a/src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java b/src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java deleted file mode 100644 index e0621ce..0000000 --- a/src/main/java/dev/freya02/botcommands/internal/restart/SilentExitExceptionHandler.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright 2012-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package dev.freya02.botcommands.internal.restart; - -import java.lang.Thread.UncaughtExceptionHandler; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; - -/** - * {@link UncaughtExceptionHandler} decorator that allows a thread to exit silently. - * - * @author Phillip Webb - * @author Andy Wilkinson - */ -class SilentExitExceptionHandler implements UncaughtExceptionHandler { - - private final UncaughtExceptionHandler delegate; - - SilentExitExceptionHandler(UncaughtExceptionHandler delegate) { - this.delegate = delegate; - } - - @Override - public void uncaughtException(Thread thread, Throwable exception) { - if (exception instanceof SilentExitException || (exception instanceof InvocationTargetException targetException - && targetException.getTargetException() instanceof SilentExitException)) { - if (isJvmExiting(thread)) { - preventNonZeroExitCode(); - } - return; - } - if (this.delegate != null) { - this.delegate.uncaughtException(thread, exception); - } - } - - private boolean isJvmExiting(Thread exceptionThread) { - for (Thread thread : getAllThreads()) { - if (thread != exceptionThread && thread.isAlive() && !thread.isDaemon()) { - return false; - } - } - return true; - } - - protected Thread[] getAllThreads() { - ThreadGroup rootThreadGroup = getRootThreadGroup(); - Thread[] threads = new Thread[32]; - int count = rootThreadGroup.enumerate(threads); - while (count == threads.length) { - threads = new Thread[threads.length * 2]; - count = rootThreadGroup.enumerate(threads); - } - return Arrays.copyOf(threads, count); - } - - private ThreadGroup getRootThreadGroup() { - ThreadGroup candidate = Thread.currentThread().getThreadGroup(); - while (candidate.getParent() != null) { - candidate = candidate.getParent(); - } - return candidate; - } - - protected void preventNonZeroExitCode() { - System.exit(0); - } - - static void setup(Thread thread) { - UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler(); - if (!(handler instanceof SilentExitExceptionHandler)) { - handler = new SilentExitExceptionHandler(handler); - thread.setUncaughtExceptionHandler(handler); - } - } - - static void exitCurrentThread() { - throw new SilentExitException(); - } - - private static final class SilentExitException extends RuntimeException { - - } - -} diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt new file mode 100644 index 0000000..d6a6612 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt @@ -0,0 +1,27 @@ +package dev.freya02.botcommands.internal.restart + +class ImmediateRestartException internal constructor() : RuntimeException("Dummy exception to stop the execution of the first main thread") { + + internal companion object { + internal fun throwAndHandle(): Nothing { + val currentThread = Thread.currentThread() + currentThread.uncaughtExceptionHandler = ExpectedReloadExceptionHandler(currentThread.uncaughtExceptionHandler) + throw ImmediateRestartException() + } + } + + private class ExpectedReloadExceptionHandler(private val delegate: Thread.UncaughtExceptionHandler?) : Thread.UncaughtExceptionHandler { + + override fun uncaughtException(t: Thread, e: Throwable) { + if (e is ImmediateRestartException) { + return + } + + if (delegate != null) { + delegate.uncaughtException(t, e) + } else { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt index 259c5f7..9559178 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -7,6 +7,7 @@ import java.lang.management.ManagementFactory import java.net.URL import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.thread import kotlin.concurrent.withLock private val logger = KotlinLogging.logger { } @@ -29,7 +30,6 @@ class Restarter private constructor( init { val thread = Thread.currentThread() - SilentExitExceptionHandler.setup(thread) appClassLoader = thread.contextClassLoader classpathUrls = ManagementFactory.getRuntimeMXBean().classPath @@ -54,8 +54,7 @@ class Restarter private constructor( val throwable = leakSafeExecutor.callAndWait { start() } if (throwable != null) throw throwable - SilentExitExceptionHandler.exitCurrentThread() - error("Should have thrown") + ImmediateRestartException.throwAndHandle() } /** @@ -86,10 +85,20 @@ class Restarter private constructor( */ private fun start(): Throwable? { val restartClassLoader = RestartClassLoader(appClassLoader, classpathUrls) - val launcher = RestartLauncher(restartClassLoader, mainClassName, args, uncaughtExceptionHandler) - launcher.start() - launcher.join() - return launcher.error + var error: Throwable? = null + val launchThreads = thread(name = "restartedMain", isDaemon = false, contextClassLoader = restartClassLoader) { + try { + val mainClass = Class.forName(mainClassName, false, restartClassLoader) + val mainMethod = mainClass.getDeclaredMethod("main", Array::class.java) + mainMethod.isAccessible = true + mainMethod.invoke(null, args) + } catch (ex: Throwable) { + error = ex + } + } + launchThreads.join() + + return error } companion object { From 720d17110a70a6b933929641968a52d7535f1802 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 12:08:46 +0200 Subject: [PATCH 07/68] Add classpath watching classes, small refactor --- .../internal/restart/RestartClassLoader.kt | 116 ++++++++++++++++++ .../botcommands/internal/restart/Restarter.kt | 21 ++-- .../restart/sources/SourceDirectories.kt | 44 +++++++ .../sources/SourceDirectoriesListener.kt | 5 + .../restart/sources/SourceDirectory.kt | 80 ++++++++++++ .../sources/SourceDirectoryListener.kt | 5 + .../internal/restart/sources/SourceFile.kt | 12 ++ .../internal/restart/sources/SourceFiles.kt | 16 +++ .../internal/restart/utils/AppClasspath.kt | 17 +++ .../botcommands/internal/restart/utils/NIO.kt | 66 ++++++++++ 10 files changed, 369 insertions(+), 13 deletions(-) create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt new file mode 100644 index 0000000..605a0b4 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt @@ -0,0 +1,116 @@ +package dev.freya02.botcommands.internal.restart + +import dev.freya02.botcommands.internal.restart.sources.DeletedSourceFile +import dev.freya02.botcommands.internal.restart.sources.SourceDirectories +import dev.freya02.botcommands.internal.restart.sources.SourceFile +import java.io.InputStream +import java.net.URL +import java.net.URLClassLoader +import java.net.URLConnection +import java.net.URLStreamHandler +import java.util.* + +internal class RestartClassLoader internal constructor( + urls: List, + parent: ClassLoader, + private val sourceDirectories: SourceDirectories, +) : URLClassLoader(urls.toTypedArray(), parent) { + + override fun getResources(name: String): Enumeration { + val resources = parent.getResources(name) + val updatedFile = sourceDirectories.getFile(name) + + if (updatedFile != null) { + if (resources.hasMoreElements()) { + resources.nextElement() + } + if (updatedFile is SourceFile) { + return MergedEnumeration(createFileUrl(name, updatedFile), resources) + } + } + + return resources + } + + override fun getResource(name: String): URL? { + val updatedFile = sourceDirectories.getFile(name) + if (updatedFile is DeletedSourceFile) { + return null + } + + return findResource(name) ?: super.getResource(name) + } + + override fun findResource(name: String): URL? { + val updatedFile = sourceDirectories.getFile(name) + ?: return super.findResource(name) + return (updatedFile as? SourceFile)?.let { createFileUrl(name, it) } + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + val path = "${name.replace('.', '/')}.class" + val updatedFile = sourceDirectories.getFile(path) + if (updatedFile is DeletedSourceFile) + throw ClassNotFoundException(name) + + return synchronized(getClassLoadingLock(name)) { + val loadedClass = findLoadedClass(name) ?: try { + findClass(name) + } catch (_: ClassNotFoundException) { + Class.forName(name, false, parent) + } + if (resolve) resolveClass(loadedClass) + loadedClass + } + } + + override fun findClass(name: String): Class<*> { + val path = "${name.replace('.', '/')}.class" + val updatedFile = sourceDirectories.getFile(path) + ?: return super.findClass(name) + if (updatedFile is DeletedSourceFile) + throw ClassNotFoundException(name) + + updatedFile as SourceFile + return defineClass(name, updatedFile.bytes, 0, updatedFile.bytes.size) + } + + @Suppress("DEPRECATION") // We target Java 17 but JDK 20 deprecates the URL constructors + private fun createFileUrl(name: String, file: SourceFile): URL { + return URL("reloaded", null, -1, "/$name", ClasspathFileURLStreamHandler(file)) + } + + private class ClasspathFileURLStreamHandler( + private val file: SourceFile, + ) : URLStreamHandler() { + + override fun openConnection(u: URL): URLConnection = Connection(u) + + private inner class Connection(url: URL): URLConnection(url) { + + override fun connect() {} + + override fun getInputStream(): InputStream = file.bytes.inputStream() + + override fun getLastModified(): Long = file.lastModified.toEpochMilli() + + override fun getContentLengthLong(): Long = file.bytes.size.toLong() + } + } + + private class MergedEnumeration(private val first: E, private val rest: Enumeration) : Enumeration { + + private var hasConsumedFirst = false + + override fun hasMoreElements(): Boolean = !hasConsumedFirst || rest.hasMoreElements() + + override fun nextElement(): E? { + if (!hasConsumedFirst) { + hasConsumedFirst = true + return first + } else { + return rest.nextElement() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt index 9559178..c4dca60 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -1,9 +1,8 @@ package dev.freya02.botcommands.internal.restart +import dev.freya02.botcommands.internal.restart.sources.SourceDirectories +import dev.freya02.botcommands.internal.restart.utils.AppClasspath import io.github.oshai.kotlinlogging.KotlinLogging -import org.springframework.boot.devtools.restart.classloader.RestartClassLoader -import java.io.File -import java.lang.management.ManagementFactory import java.net.URL import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock @@ -14,10 +13,11 @@ private val logger = KotlinLogging.logger { } class Restarter private constructor( private val args: Array, + private val sourceDirectories: SourceDirectories, ) { private val appClassLoader: ClassLoader - private val classpathUrls: Array + val appClasspathUrls: List private val mainClassName: String @@ -32,12 +32,7 @@ class Restarter private constructor( val thread = Thread.currentThread() appClassLoader = thread.contextClassLoader - classpathUrls = ManagementFactory.getRuntimeMXBean().classPath - .split(File.pathSeparator) - .map(::File) - .filter { it.isDirectory } - .map { it.toURI().toURL() } - .toTypedArray() + appClasspathUrls = AppClasspath.getPaths().map { it.toUri().toURL() } mainClassName = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) .walk { stream -> stream.filter { it.methodName == "main" }.toList().last() } @@ -84,7 +79,7 @@ class Restarter private constructor( * Starts a new instance of the main class, or returns a [Throwable] if it failed. */ private fun start(): Throwable? { - val restartClassLoader = RestartClassLoader(appClassLoader, classpathUrls) + val restartClassLoader = RestartClassLoader(appClasspathUrls, appClassLoader, sourceDirectories) var error: Throwable? = null val launchThreads = thread(name = "restartedMain", isDaemon = false, contextClassLoader = restartClassLoader) { try { @@ -107,11 +102,11 @@ class Restarter private constructor( lateinit var instance: Restarter private set - fun initialize(args: Array) { + fun initialize(args: Array, sourceDirectories: SourceDirectories) { var newInstance: Restarter? = null instanceLock.withLock { if (::instance.isInitialized.not()) { - newInstance = Restarter(args) + newInstance = Restarter(args, sourceDirectories) instance = newInstance } } diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt new file mode 100644 index 0000000..dc6881c --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt @@ -0,0 +1,44 @@ +package dev.freya02.botcommands.internal.restart.sources + +import java.nio.file.Path + +class SourceDirectories internal constructor() { + private val directories: MutableMap = hashMapOf() + + internal fun getFile(path: String): ISourceFile? { + return directories.firstNotNullOfOrNull { it.value.files[path] } + } + + internal fun setSource(source: SourceDirectory) { + directories[source.directory] = source + } + + internal fun replaceSource(key: Path, directory: SourceDirectory) { + check(key in directories) + + directories[key] = directory + } +} + +fun SourceDirectories(directories: List, listener: SourceDirectoriesListener): SourceDirectories { + val sourceDirectories = SourceDirectories() + + fun onSourceDirectoryUpdate(directory: Path, sourceFilesFactory: () -> SourceFiles) { + // The command is called when restarting + // so we don't make snapshots before all changes went through + listener.onChange(command = { + val newSourceDirectory = SourceDirectory( + directory, + sourceFilesFactory(), + listener = { onSourceDirectoryUpdate(directory, it) } + ) + sourceDirectories.replaceSource(directory, newSourceDirectory) + }) + } + + directories.forEach { directory -> + sourceDirectories.setSource(SourceDirectory(directory, listener = { onSourceDirectoryUpdate(directory, it) })) + } + + return sourceDirectories +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt new file mode 100644 index 0000000..25ed48d --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt @@ -0,0 +1,5 @@ +package dev.freya02.botcommands.internal.restart.sources + +fun interface SourceDirectoriesListener { + fun onChange(command: () -> Unit) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt new file mode 100644 index 0000000..ccbdde1 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt @@ -0,0 +1,80 @@ +package dev.freya02.botcommands.internal.restart.sources + +import dev.freya02.botcommands.internal.restart.utils.walkDirectories +import dev.freya02.botcommands.internal.restart.utils.walkFiles +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds.* +import kotlin.concurrent.thread +import kotlin.io.path.* + +private val logger = KotlinLogging.logger { } + +@OptIn(ExperimentalPathApi::class) +internal class SourceDirectory internal constructor( + val directory: Path, + val files: SourceFiles, + private val listener: SourceDirectoryListener, +) { + + init { + require(directory.isDirectory()) + + logger.debug { "Listening to ${directory.absolutePathString()}" } + + val watchService = directory.fileSystem.newWatchService() + directory.walkDirectories { path, attributes -> + path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) + } + + thread(name = "Classpath watcher of '${directory.fileName}'", isDaemon = true) { + watchService.take() // Wait for a change + watchService.close() + + listener.onChange(sourcesFilesFactory = { + val snapshot = directory.takeSnapshot() + + // Exclude deleted files so they don't count as being deleted again + val deletedPaths = files.withoutDeletes().keys - snapshot.keys + if (deletedPaths.isNotEmpty()) { + logger.info { "Deleted files in ${directory.absolutePathString()}: $deletedPaths" } + return@onChange deletedPaths.associateWith { DeletedSourceFile } + snapshot + } + + // Exclude deleted files so they count as being added back + val addedPaths = snapshot.keys - files.withoutDeletes().keys + if (addedPaths.isNotEmpty()) { + logger.info { "Added files in ${directory.absolutePathString()}: $addedPaths" } + return@onChange files + snapshot + } + + val modifiedFiles = snapshot.keys.filter { key -> + val actual = snapshot[key] ?: error("Key from map is missing a value somehow") + val expected = files[key] ?: error("Expected file is missing, should have been detected as deleted") + + // File was deleted (on the 2nd build for example) and got recreated (on the 3rd build for example) + if (expected is DeletedSourceFile) error("Expected file was registered as deleted, should have been detected as added") + expected as SourceFile + + actual as SourceFile // Assertion + + actual.lastModified != expected.lastModified + } + if (modifiedFiles.isNotEmpty()) { + logger.info { "Timestamp changed in ${directory.absolutePathString()}: $modifiedFiles" } + return@onChange files + snapshot + } + + error("Received a file system event but no changes were detected") + }) + } + } +} + +internal fun SourceDirectory(directory: Path, listener: SourceDirectoryListener): SourceDirectory { + return SourceDirectory(directory, directory.takeSnapshot(), listener) +} + +private fun Path.takeSnapshot(): SourceFiles = walkFiles().associate { (it, attrs) -> + it.relativeTo(this).pathString to SourceFile(attrs.lastModifiedTime().toInstant(), it.readBytes()) +}.let(::SourceFiles) diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt new file mode 100644 index 0000000..5350211 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt @@ -0,0 +1,5 @@ +package dev.freya02.botcommands.internal.restart.sources + +internal fun interface SourceDirectoryListener { + fun onChange(sourcesFilesFactory: () -> SourceFiles) +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt new file mode 100644 index 0000000..a1cc39b --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt @@ -0,0 +1,12 @@ +package dev.freya02.botcommands.internal.restart.sources + +import java.time.Instant + +internal sealed interface ISourceFile + +internal class SourceFile( + val lastModified: Instant, + val bytes: ByteArray, +) : ISourceFile + +internal object DeletedSourceFile : ISourceFile \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt new file mode 100644 index 0000000..2ded125 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt @@ -0,0 +1,16 @@ +package dev.freya02.botcommands.internal.restart.sources + +internal class SourceFiles internal constructor( + internal val files: Map, +) { + + val keys: Set get() = files.keys + + internal operator fun get(path: String): ISourceFile? = files[path] + + internal fun withoutDeletes(): SourceFiles = SourceFiles(files.filterValues { it !is DeletedSourceFile }) + + internal operator fun plus(other: SourceFiles): SourceFiles = SourceFiles(files + other.files) +} + +internal operator fun Map.plus(other: SourceFiles): SourceFiles = SourceFiles(this + other.files) \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt new file mode 100644 index 0000000..11ccb5c --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt @@ -0,0 +1,17 @@ +package dev.freya02.botcommands.internal.restart.utils + +import java.io.File +import java.lang.management.ManagementFactory +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.isDirectory + +object AppClasspath { + + fun getPaths(): List { + return ManagementFactory.getRuntimeMXBean().classPath + .split(File.pathSeparator) + .map(::Path) + .filter { it.isDirectory() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt new file mode 100644 index 0000000..cbcbb7d --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt @@ -0,0 +1,66 @@ +package dev.freya02.botcommands.internal.restart.utils + +import java.io.IOException +import java.nio.file.FileVisitResult +import java.nio.file.FileVisitor +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.attribute.BasicFileAttributes + +// Optimization of Path#walk, cuts CPU usage by 4 +// mostly by eliminating duplicate calls to file attributes +internal fun Path.walkFiles(): List> { + return buildList { + Files.walkFileTree(this@walkFiles, object : FileVisitor { + override fun preVisitDirectory( + dir: Path, + attrs: BasicFileAttributes + ): FileVisitResult = FileVisitResult.CONTINUE + + override fun visitFile( + file: Path, + attrs: BasicFileAttributes + ): FileVisitResult { + add(file to attrs) + return FileVisitResult.CONTINUE + } + + override fun visitFileFailed( + file: Path, + exc: IOException + ): FileVisitResult = FileVisitResult.CONTINUE + + override fun postVisitDirectory( + dir: Path, + exc: IOException? + ): FileVisitResult = FileVisitResult.CONTINUE + }) + } +} + +internal fun Path.walkDirectories(block: (Path, BasicFileAttributes) -> Unit) { + Files.walkFileTree(this@walkDirectories, object : FileVisitor { + override fun preVisitDirectory( + dir: Path, + attrs: BasicFileAttributes + ): FileVisitResult { + block(dir, attrs) + return FileVisitResult.CONTINUE + } + + override fun visitFile( + file: Path, + attrs: BasicFileAttributes + ): FileVisitResult = FileVisitResult.CONTINUE + + override fun visitFileFailed( + file: Path, + exc: IOException + ): FileVisitResult = FileVisitResult.CONTINUE + + override fun postVisitDirectory( + dir: Path, + exc: IOException? + ): FileVisitResult = FileVisitResult.CONTINUE + }) +} \ No newline at end of file From 2341c480d84836652ed8a7f10f8138d000ee0b71 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 12:09:17 +0200 Subject: [PATCH 08/68] Add ClasspathListener Restarts the application after receiving classpath change commands --- .../restart/watcher/ClasspathListener.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt new file mode 100644 index 0000000..102f26c --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt @@ -0,0 +1,38 @@ +package dev.freya02.botcommands.internal.restart.watcher + +import dev.freya02.botcommands.internal.restart.Restarter +import dev.freya02.botcommands.internal.restart.sources.SourceDirectoriesListener +import io.github.oshai.kotlinlogging.KotlinLogging +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import kotlin.time.Duration + +private val logger = KotlinLogging.logger { } + +class ClasspathListener( + private val delay: Duration +) : SourceDirectoriesListener { + + private val scheduler = Executors.newSingleThreadScheduledExecutor() + private lateinit var scheduledRestart: ScheduledFuture<*> + + private val commands: MutableList<() -> Unit> = arrayListOf() + + override fun onChange(command: () -> Unit) { + commands += command + if (::scheduledRestart.isInitialized) scheduledRestart.cancel(false) + + scheduledRestart = scheduler.schedule({ + commands.forEach { it.invoke() } + commands.clear() + + try { + Restarter.instance.restart() + } catch (e: Exception) { + logger.error(e) { "Restart failed, waiting for the next build" } + } + scheduler.shutdown() + }, delay.inWholeMilliseconds, TimeUnit.MILLISECONDS) + } +} \ No newline at end of file From f717a395d73e345fe55cf619903dd04fccdc9c9d Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 14:23:02 +0200 Subject: [PATCH 09/68] Clean up --- build.gradle.kts | 3 --- .../botcommands/internal/restart/LeakSafeExecutor.kt | 8 -------- 2 files changed, 11 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fc8497f..5e3e4b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,9 +15,6 @@ repositories { dependencies { implementation("io.github.oshai:kotlin-logging-jvm:7.0.3") - implementation("org.springframework.boot:spring-boot-starter:3.4.2") - implementation("org.springframework.boot:spring-boot-devtools:3.4.2") - testImplementation(kotlin("test")) } diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt index 7207ec4..b36bbbe 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt @@ -1,6 +1,5 @@ package dev.freya02.botcommands.internal.restart -import org.springframework.boot.devtools.restart.classloader.RestartClassLoader import java.util.concurrent.BlockingDeque import java.util.concurrent.LinkedBlockingDeque import kotlin.system.exitProcess @@ -17,8 +16,6 @@ internal class LeakSafeExecutor internal constructor() { leakSafeThreads += LeakSafeThread() } - fun call(callable: () -> Unit): Unit = getLeakSafeThread().call(callable) - fun callAndWait(callable: () -> V): V = getLeakSafeThread().callAndWait(callable) private fun getLeakSafeThread(): LeakSafeThread { @@ -38,11 +35,6 @@ internal class LeakSafeExecutor internal constructor() { isDaemon = false } - fun call(callable: () -> Unit) { - this.callable = callable - start() - } - @Suppress("UNCHECKED_CAST") fun callAndWait(callable: () -> V): V { this.callable = callable From 48bff1be65054e71a03a1229d4c7111a318567c5 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 14:27:12 +0200 Subject: [PATCH 10/68] Take List of arguments instead of Array --- .../dev/freya02/botcommands/internal/restart/Restarter.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt index c4dca60..98a2dfb 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -12,8 +12,8 @@ import kotlin.concurrent.withLock private val logger = KotlinLogging.logger { } class Restarter private constructor( - private val args: Array, private val sourceDirectories: SourceDirectories, + private val args: List, ) { private val appClassLoader: ClassLoader @@ -86,7 +86,7 @@ class Restarter private constructor( val mainClass = Class.forName(mainClassName, false, restartClassLoader) val mainMethod = mainClass.getDeclaredMethod("main", Array::class.java) mainMethod.isAccessible = true - mainMethod.invoke(null, args) + mainMethod.invoke(null, args.toTypedArray()) } catch (ex: Throwable) { error = ex } @@ -102,7 +102,7 @@ class Restarter private constructor( lateinit var instance: Restarter private set - fun initialize(args: Array, sourceDirectories: SourceDirectories) { + fun initialize(args: List) { var newInstance: Restarter? = null instanceLock.withLock { if (::instance.isInitialized.not()) { From f50ae09dcd22f46686f34568e63bdb4f79996b8e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 14:28:54 +0200 Subject: [PATCH 11/68] Remove SourceDirectories argument, replace RestartClassLoader by URLClassLoader We don't really need full snapshots as we recreate a new ClassLoader on each restart, which always reloads the classes, see the notes --- .../botcommands/internal/restart/Restarter.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt index 98a2dfb..b06da20 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -1,9 +1,9 @@ package dev.freya02.botcommands.internal.restart -import dev.freya02.botcommands.internal.restart.sources.SourceDirectories import dev.freya02.botcommands.internal.restart.utils.AppClasspath import io.github.oshai.kotlinlogging.KotlinLogging import java.net.URL +import java.net.URLClassLoader import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.thread @@ -12,7 +12,6 @@ import kotlin.concurrent.withLock private val logger = KotlinLogging.logger { } class Restarter private constructor( - private val sourceDirectories: SourceDirectories, private val args: List, ) { @@ -79,7 +78,16 @@ class Restarter private constructor( * Starts a new instance of the main class, or returns a [Throwable] if it failed. */ private fun start(): Throwable? { - val restartClassLoader = RestartClassLoader(appClasspathUrls, appClassLoader, sourceDirectories) + // We use a regular URLClassLoader instead of [[RestartClassLoader]], + // as classpath changes will trigger a restart and thus recreate a new ClassLoader, + // meaning live updating the classes is pointless. + // In contrast, Spring needs their RestartClassLoader because it can override classes remotely, + // but we don't have such a use case. + // However, not using [[RestartClassLoader]], which uses snapshots, has an issue, + // trying to load deleted classes (most likely on shutdown) will fail, + // Spring also has that issue, but it will only happen on classes out of its component scan, + // BC just needs to make sure to at least load the classes on its path too. + val restartClassLoader = URLClassLoader(appClasspathUrls.toTypedArray(), appClassLoader) var error: Throwable? = null val launchThreads = thread(name = "restartedMain", isDaemon = false, contextClassLoader = restartClassLoader) { try { @@ -106,7 +114,7 @@ class Restarter private constructor( var newInstance: Restarter? = null instanceLock.withLock { if (::instance.isInitialized.not()) { - newInstance = Restarter(args, sourceDirectories) + newInstance = Restarter(args) instance = newInstance } } From 8bb9f295098d018b7f51b10e746ae6ede7a333e8 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 14:29:07 +0200 Subject: [PATCH 12/68] Don't read class data when making a snapshot --- .../internal/restart/sources/SourceDirectory.kt | 2 +- .../botcommands/internal/restart/sources/SourceFile.kt | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt index ccbdde1..aef2d90 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt @@ -76,5 +76,5 @@ internal fun SourceDirectory(directory: Path, listener: SourceDirectoryListener) } private fun Path.takeSnapshot(): SourceFiles = walkFiles().associate { (it, attrs) -> - it.relativeTo(this).pathString to SourceFile(attrs.lastModifiedTime().toInstant(), it.readBytes()) + it.relativeTo(this).pathString to SourceFile(attrs.lastModifiedTime().toInstant()) }.let(::SourceFiles) diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt index a1cc39b..648bb57 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt @@ -6,7 +6,10 @@ internal sealed interface ISourceFile internal class SourceFile( val lastModified: Instant, - val bytes: ByteArray, -) : ISourceFile +) : ISourceFile { + + val bytes: ByteArray + get() = throw UnsupportedOperationException("Class data is no longer retained as RestartClassLoader is not used yet") +} internal object DeletedSourceFile : ISourceFile \ No newline at end of file From e03ffc886c3c8d6ee9f5eb8ced5ad0127de0ad8c Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 14:36:26 +0200 Subject: [PATCH 13/68] Add services to self-manage this extension --- build.gradle.kts | 3 +- .../RestarterApplicationStartListener.kt | 16 ++++++++ .../restart/services/RestarterService.kt | 39 +++++++++++++++++++ .../restart/sources/SourceDirectories.kt | 8 +++- .../sources/SourceDirectoriesListener.kt | 4 +- .../restart/sources/SourceDirectory.kt | 16 ++++++-- .../internal/restart/utils/AppClasspath.kt | 2 +- .../restart/watcher/ClasspathListener.kt | 6 ++- 8 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 5e3e4b1..ad655e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,10 +10,11 @@ version = "3.0.0-beta.2_DEV" repositories { mavenCentral() + mavenLocal() } dependencies { - implementation("io.github.oshai:kotlin-logging-jvm:7.0.3") + implementation("io.github.freya022:BotCommands:3.0.0-beta.2_DEV") testImplementation(kotlin("test")) } diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt new file mode 100644 index 0000000..63886c3 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt @@ -0,0 +1,16 @@ +package dev.freya02.botcommands.internal.restart.services + +import dev.freya02.botcommands.internal.restart.Restarter +import io.github.freya022.botcommands.api.core.events.ApplicationStartListener +import io.github.freya022.botcommands.api.core.events.BApplicationStartEvent +import io.github.freya022.botcommands.api.core.service.annotations.BService +import io.github.freya022.botcommands.api.core.service.annotations.RequiresDefaultInjection + +@BService +@RequiresDefaultInjection +internal class RestarterApplicationStartListener : ApplicationStartListener { + + override fun onApplicationStart(event: BApplicationStartEvent) { + Restarter.initialize(event.args) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt new file mode 100644 index 0000000..c7326f6 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt @@ -0,0 +1,39 @@ +package dev.freya02.botcommands.internal.restart.services + +import dev.freya02.botcommands.internal.restart.RestartListener +import dev.freya02.botcommands.internal.restart.Restarter +import dev.freya02.botcommands.internal.restart.sources.SourceDirectories +import dev.freya02.botcommands.internal.restart.utils.AppClasspath +import dev.freya02.botcommands.internal.restart.watcher.ClasspathListener +import io.github.freya022.botcommands.api.core.BContext +import io.github.freya022.botcommands.api.core.annotations.BEventListener +import io.github.freya022.botcommands.api.core.config.BHotReloadConfig +import io.github.freya022.botcommands.api.core.events.BShutdownEvent +import io.github.freya022.botcommands.api.core.service.annotations.BService +import io.github.freya022.botcommands.api.core.service.annotations.RequiresDefaultInjection + +@BService +@RequiresDefaultInjection +internal class RestarterService internal constructor ( + context: BContext, +) { + + init { + Restarter.instance.addListener(object : RestartListener { + override fun beforeStop() { + context.shutdown() + context.awaitShutdown() + } + }) + } + + @BService + internal fun sourceDirectories(config: BHotReloadConfig): SourceDirectories { + return SourceDirectories(AppClasspath.getPaths(), ClasspathListener(config.restartDelay)) + } + + @BEventListener(priority = Int.MAX_VALUE) + internal fun onShutdown(event: BShutdownEvent, sourceDirectories: SourceDirectories) { + sourceDirectories.close() + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt index dc6881c..2dc84c9 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt @@ -2,7 +2,7 @@ package dev.freya02.botcommands.internal.restart.sources import java.nio.file.Path -class SourceDirectories internal constructor() { +internal class SourceDirectories internal constructor() { private val directories: MutableMap = hashMapOf() internal fun getFile(path: String): ISourceFile? { @@ -18,9 +18,13 @@ class SourceDirectories internal constructor() { directories[key] = directory } + + internal fun close() { + directories.values.forEach { it.close() } + } } -fun SourceDirectories(directories: List, listener: SourceDirectoriesListener): SourceDirectories { +internal fun SourceDirectories(directories: List, listener: SourceDirectoriesListener): SourceDirectories { val sourceDirectories = SourceDirectories() fun onSourceDirectoryUpdate(directory: Path, sourceFilesFactory: () -> SourceFiles) { diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt index 25ed48d..75b9f72 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt @@ -1,5 +1,7 @@ package dev.freya02.botcommands.internal.restart.sources -fun interface SourceDirectoriesListener { +internal interface SourceDirectoriesListener { fun onChange(command: () -> Unit) + + fun onCancel() } \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt index aef2d90..cc5ab38 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt @@ -17,18 +17,24 @@ internal class SourceDirectory internal constructor( private val listener: SourceDirectoryListener, ) { + private val thread: Thread + init { require(directory.isDirectory()) - logger.debug { "Listening to ${directory.absolutePathString()}" } + logger.trace { "Listening to ${directory.absolutePathString()}" } val watchService = directory.fileSystem.newWatchService() directory.walkDirectories { path, attributes -> path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) } - thread(name = "Classpath watcher of '${directory.fileName}'", isDaemon = true) { - watchService.take() // Wait for a change + thread = thread(name = "Classpath watcher of '${directory.fileName}'", isDaemon = true) { + try { + watchService.take() // Wait for a change + } catch (_: InterruptedException) { + return@thread logger.trace { "Interrupted watching ${directory.absolutePathString()}" } + } watchService.close() listener.onChange(sourcesFilesFactory = { @@ -69,6 +75,10 @@ internal class SourceDirectory internal constructor( }) } } + + internal fun close() { + thread.interrupt() + } } internal fun SourceDirectory(directory: Path, listener: SourceDirectoryListener): SourceDirectory { diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt index 11ccb5c..39eb256 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt @@ -6,7 +6,7 @@ import java.nio.file.Path import kotlin.io.path.Path import kotlin.io.path.isDirectory -object AppClasspath { +internal object AppClasspath { fun getPaths(): List { return ManagementFactory.getRuntimeMXBean().classPath diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt index 102f26c..6805bd4 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt @@ -10,7 +10,7 @@ import kotlin.time.Duration private val logger = KotlinLogging.logger { } -class ClasspathListener( +internal class ClasspathListener internal constructor( private val delay: Duration ) : SourceDirectoriesListener { @@ -35,4 +35,8 @@ class ClasspathListener( scheduler.shutdown() }, delay.inWholeMilliseconds, TimeUnit.MILLISECONDS) } + + override fun onCancel() { + scheduler.shutdownNow() + } } \ No newline at end of file From df0098f70b53eb72a29030aa9915481d105c6305 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 15:07:25 +0200 Subject: [PATCH 14/68] Update names --- .../botcommands/internal/restart/services/RestarterService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt index c7326f6..2ab58f5 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt @@ -7,7 +7,7 @@ import dev.freya02.botcommands.internal.restart.utils.AppClasspath import dev.freya02.botcommands.internal.restart.watcher.ClasspathListener import io.github.freya022.botcommands.api.core.BContext import io.github.freya022.botcommands.api.core.annotations.BEventListener -import io.github.freya022.botcommands.api.core.config.BHotReloadConfig +import io.github.freya022.botcommands.api.core.config.BRestartConfig import io.github.freya022.botcommands.api.core.events.BShutdownEvent import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.service.annotations.RequiresDefaultInjection @@ -28,7 +28,7 @@ internal class RestarterService internal constructor ( } @BService - internal fun sourceDirectories(config: BHotReloadConfig): SourceDirectories { + internal fun sourceDirectories(config: BRestartConfig): SourceDirectories { return SourceDirectories(AppClasspath.getPaths(), ClasspathListener(config.restartDelay)) } From cc72423ef6ae8a05aec6e913962783b9a212af5d Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 18:43:15 +0200 Subject: [PATCH 15/68] Override ClassLoader used by ClassGraph after restart --- .../botcommands/internal/restart/Restarter.kt | 4 +++- .../services/RestarterClassGraphConfigurer.kt | 17 +++++++++++++++++ ...tcommands.internal.core.ClassGraphConfigurer | 1 + 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt create mode 100644 src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt index b06da20..fe5f684 100644 --- a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -89,7 +89,7 @@ class Restarter private constructor( // BC just needs to make sure to at least load the classes on its path too. val restartClassLoader = URLClassLoader(appClasspathUrls.toTypedArray(), appClassLoader) var error: Throwable? = null - val launchThreads = thread(name = "restartedMain", isDaemon = false, contextClassLoader = restartClassLoader) { + val launchThreads = thread(name = RESTARTED_THREAD_NAME, isDaemon = false, contextClassLoader = restartClassLoader) { try { val mainClass = Class.forName(mainClassName, false, restartClassLoader) val mainMethod = mainClass.getDeclaredMethod("main", Array::class.java) @@ -106,6 +106,8 @@ class Restarter private constructor( companion object { + const val RESTARTED_THREAD_NAME = "restartedMain" + private val instanceLock: Lock = ReentrantLock() lateinit var instance: Restarter private set diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt new file mode 100644 index 0000000..f285807 --- /dev/null +++ b/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt @@ -0,0 +1,17 @@ +package dev.freya02.botcommands.internal.restart.services + +import dev.freya02.botcommands.internal.restart.Restarter +import io.github.classgraph.ClassGraph +import io.github.freya022.botcommands.internal.core.ClassGraphConfigurer + +internal class RestarterClassGraphConfigurer : ClassGraphConfigurer { + + override fun ClassGraph.configure(arguments: ClassGraphConfigurer.Arguments) { + val thread = Thread.currentThread() + val classLoader = thread.contextClassLoader + if (thread.name != Restarter.RESTARTED_THREAD_NAME) return + + // [[Restarter]] will read from the mutable classes first then delegate to the app class loader (immutable) + overrideClassLoaders(classLoader) + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer b/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer new file mode 100644 index 0000000..cfe6781 --- /dev/null +++ b/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer @@ -0,0 +1 @@ +dev.freya02.botcommands.internal.restart.services.RestarterClassGraphConfigurer \ No newline at end of file From 8ef3fe66ac4cf1db1d8acc550f7a46cf03f1a441 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 31 May 2025 19:52:49 +0200 Subject: [PATCH 16/68] Use BC snapshot --- build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index ad655e1..f991d93 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,11 +10,12 @@ version = "3.0.0-beta.2_DEV" repositories { mavenCentral() + maven("https://jitpack.io") mavenLocal() } dependencies { - implementation("io.github.freya022:BotCommands:3.0.0-beta.2_DEV") + implementation("io.github.freya022:BotCommands:80c4a99412") testImplementation(kotlin("test")) } From 658a29ecc06b36be68c8f7050c0d6dc929366616 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sun, 1 Jun 2025 13:51:25 +0200 Subject: [PATCH 17/68] Move to module --- buildSrc/build.gradle.kts | 13 +++++++ ...tCommands-Restarter-conventions.gradle.kts | 35 +++++++------------ gradle/libs.versions.toml | 6 ++++ restarter/build.gradle.kts | 20 +++++++++++ .../restart/ImmediateRestartException.kt | 0 .../internal/restart/LeakSafeExecutor.kt | 0 .../internal/restart/RestartClassLoader.kt | 0 .../internal/restart/RestartListener.kt | 0 .../botcommands/internal/restart/Restarter.kt | 0 .../RestarterApplicationStartListener.kt | 0 .../services/RestarterClassGraphConfigurer.kt | 0 .../restart/services/RestarterService.kt | 0 .../restart/sources/SourceDirectories.kt | 0 .../sources/SourceDirectoriesListener.kt | 0 .../restart/sources/SourceDirectory.kt | 0 .../sources/SourceDirectoryListener.kt | 0 .../internal/restart/sources/SourceFile.kt | 0 .../internal/restart/sources/SourceFiles.kt | 0 .../internal/restart/utils/AppClasspath.kt | 0 .../botcommands/internal/restart/utils/NIO.kt | 0 .../restart/watcher/ClasspathListener.kt | 0 ...ommands.internal.core.ClassGraphConfigurer | 0 settings.gradle.kts | 6 +++- 23 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 buildSrc/build.gradle.kts rename build.gradle.kts => buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts (50%) create mode 100644 gradle/libs.versions.toml create mode 100644 restarter/build.gradle.kts rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt (100%) rename {src => restarter/src}/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt (100%) rename {src => restarter/src}/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer (100%) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..f0dc19e --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + gradlePluginPortal() +} + +dependencies { + // Change in version catalog too + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") +} \ No newline at end of file diff --git a/build.gradle.kts b/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts similarity index 50% rename from build.gradle.kts rename to buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts index f991d93..ed17d27 100644 --- a/build.gradle.kts +++ b/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts @@ -1,46 +1,35 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - kotlin("jvm") version "2.1.20" - `maven-publish` + kotlin("jvm") } group = "dev.freya02" version = "3.0.0-beta.2_DEV" -repositories { - mavenCentral() - maven("https://jitpack.io") - mavenLocal() -} +java { + toolchain { + languageVersion = JavaLanguageVersion.of(24) + } -dependencies { - implementation("io.github.freya022:BotCommands:80c4a99412") + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 - testImplementation(kotlin("test")) + withSourcesJar() } -tasks.test { - useJUnitPlatform() +repositories { + mavenCentral() } -java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 +dependencies { - withSourcesJar() } kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_21 - } -} -publishing { - publications { - create("maven") { - from(components["java"]) - } + freeCompilerArgs.addAll("-Xjsr305=strict") } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..5a8629e --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,6 @@ +[versions] +kotlin = "2.1.20" # Also change in buildSrc +botcommands = "80c4a99412" + +[libraries] +botcommands = { module = "io.github.freya022:BotCommands", version.ref = "botcommands" } \ No newline at end of file diff --git a/restarter/build.gradle.kts b/restarter/build.gradle.kts new file mode 100644 index 0000000..09a0eae --- /dev/null +++ b/restarter/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + id("BotCommands-Restarter-conventions") + `maven-publish` +} + +repositories { + maven("https://jitpack.io") +} + +dependencies { + implementation(libs.botcommands) +} + +publishing { + publications { + create("maven") { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/LeakSafeExecutor.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartListener.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterApplicationStartListener.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectories.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoriesListener.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectory.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceDirectoryListener.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFile.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/sources/SourceFiles.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/AppClasspath.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/utils/NIO.kt diff --git a/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt similarity index 100% rename from src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt rename to restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathListener.kt diff --git a/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer b/restarter/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer similarity index 100% rename from src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer rename to restarter/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer diff --git a/settings.gradle.kts b/settings.gradle.kts index 632066d..10eefa2 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,5 @@ -rootProject.name = "BotCommands-Restarter" \ No newline at end of file +rootProject.name = "BotCommands-Restarter" + +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +include("restarter") \ No newline at end of file From 242fab93488c39d668b95643a1ef547b35aa8cda Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:14:45 +0200 Subject: [PATCH 18/68] Use Kotlin 2.2.0-RC Required for Java 24 support --- buildSrc/build.gradle.kts | 2 +- gradle/libs.versions.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index f0dc19e..2e6893b 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,5 +9,5 @@ repositories { dependencies { // Change in version catalog too - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.0-RC") } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5a8629e..ed81fd7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.1.20" # Also change in buildSrc +kotlin = "2.2.0-RC" # Also change in buildSrc botcommands = "80c4a99412" [libraries] From 84d9eeafb440dc94370d8efc1f5a7a369f334506 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:16:04 +0200 Subject: [PATCH 19/68] Add JDA cache agent, transform JDABuilder and JDAService with basic tracking --- gradle/libs.versions.toml | 12 +- restarter-jda-cache/build.gradle.kts | 64 ++++++++++ .../jda/cache/AbstractClassFileTransformer.kt | 25 ++++ .../botcommands/restart/jda/cache/Agent.kt | 13 ++ .../restart/jda/cache/JDABuilderSession.kt | 57 +++++++++ .../jda/cache/JDABuilderTransformer.kt | 116 ++++++++++++++++++ .../jda/cache/JDAServiceTransformer.kt | 109 ++++++++++++++++ .../botcommands/restart/jda/cache/utils.kt | 11 ++ .../botcommands/restart/jda/cache/Test.java | 36 ++++++ .../jda/cache/JDABuilderTransformerTest.kt | 79 ++++++++++++ .../jda/cache/JDAServiceTransformerTest.kt | 41 +++++++ .../src/test/resources/logback-test.xml | 14 +++ settings.gradle.kts | 3 +- 13 files changed, 578 insertions(+), 2 deletions(-) create mode 100644 restarter-jda-cache/build.gradle.kts create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt create mode 100644 restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java create mode 100644 restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt create mode 100644 restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt create mode 100644 restarter-jda-cache/src/test/resources/logback-test.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed81fd7..6bd8d06 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,16 @@ [versions] kotlin = "2.2.0-RC" # Also change in buildSrc botcommands = "80c4a99412" +jda = "5.5.1" +mockk = "1.13.16" +bytebuddy = "1.17.5" +logback-classic = "1.5.18" +kotlin-logging = "7.0.3" [libraries] -botcommands = { module = "io.github.freya022:BotCommands", version.ref = "botcommands" } \ No newline at end of file +botcommands = { module = "io.github.freya022:BotCommands", version.ref = "botcommands" } +jda = { module = "net.dv8tion:JDA", version.ref = "jda" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +bytebuddy = { module = "net.bytebuddy:byte-buddy", version.ref = "bytebuddy" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback-classic" } +kotlin-logging = { module = "io.github.oshai:kotlin-logging-jvm", version.ref = "kotlin-logging" } \ No newline at end of file diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts new file mode 100644 index 0000000..cfd3ccc --- /dev/null +++ b/restarter-jda-cache/build.gradle.kts @@ -0,0 +1,64 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + id("BotCommands-Restarter-conventions") + `java-library` + `maven-publish` +} + +repositories { + maven("https://jitpack.io") +} + +dependencies { + implementation(projects.restarter) + implementation(libs.kotlin.logging) + implementation(libs.jda) + + testImplementation(kotlin("test")) + testImplementation(libs.mockk) + testImplementation(libs.bytebuddy) + testImplementation(libs.logback.classic) + testImplementation(libs.botcommands) +} + +java { + sourceCompatibility = JavaVersion.VERSION_24 + targetCompatibility = JavaVersion.VERSION_24 +} + +kotlin { + compilerOptions { + jvmTarget = JvmTarget.JVM_24 + } +} + +val jar by tasks.getting(Jar::class) { + manifest { + attributes( + "Premain-Class" to "dev.freya02.botcommands.restart.jda.cache.Agent", + ) + } +} + +tasks.withType { + useJUnitPlatform() + + jvmArgs("-javaagent:${jar.archiveFile.get().asFile.absolutePath}") +} + +//val copyForAgent by tasks.registering(Copy::class) { +// from(jar) +// into(layout.buildDirectory.dir("libs")) +// rename { "BotCommands-Restarter-JDA-Cache.jar" } +//} +// +//jar.finalizedBy(copyForAgent) + +publishing { + publications { + create("maven") { + from(components["java"]) + } + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt new file mode 100644 index 0000000..f0bbbac --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt @@ -0,0 +1,25 @@ +package dev.freya02.botcommands.restart.jda.cache + +import java.lang.instrument.ClassFileTransformer +import java.security.ProtectionDomain + +internal abstract class AbstractClassFileTransformer protected constructor(private val target: String) : ClassFileTransformer { + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain, + classfileBuffer: ByteArray + ): ByteArray? { + if (className == target) return try { + transform(classfileBuffer) + } catch (e: Throwable) { + e.printStackTrace() + null + } + return null + } + + protected abstract fun transform(classData: ByteArray): ByteArray +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt new file mode 100644 index 0000000..07f5d0f --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt @@ -0,0 +1,13 @@ +package dev.freya02.botcommands.restart.jda.cache + +import java.lang.instrument.Instrumentation + +object Agent { + + @JvmStatic + fun premain(agentArgs: String?, inst: Instrumentation) { + println("Agent args: $agentArgs") + inst.addTransformer(JDABuilderTransformer) + inst.addTransformer(JDAServiceTransformer) + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt new file mode 100644 index 0000000..ce15860 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -0,0 +1,57 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.github.oshai.kotlinlogging.KotlinLogging +import net.dv8tion.jda.api.OnlineStatus + +private val logger = KotlinLogging.logger { } + +class JDABuilderSession { + + private var isIncompatible = false + private val builderValues: MutableMap = hashMapOf() + + // So we can track the initial token and intents, the constructor will be instrumented and call this method + // The user overriding the values using token/intent setters should not be an issue + fun onInit(token: String?, intents: Int) { + builderValues[ValueType.TOKEN] = token + builderValues[ValueType.INTENTS] = intents + } + + fun markIncompatible() { + isIncompatible = true + } + + fun setStatus(status: OnlineStatus) { + builderValues[ValueType.STATUS] = status + } + + enum class ValueType { + TOKEN, + INTENTS, + STATUS, + } + + companion object { + private val _currentSession: ThreadLocal = + ThreadLocal.withInitial { error("No JDABuilderSession exists for this thread") } + + @JvmStatic + fun currentSession(): JDABuilderSession { + return _currentSession.get() + } + + @JvmStatic + fun withBuilderSession( + // Use Java function types to make codegen a bit more reliable + block: Runnable + ) { + val session = JDABuilderSession() + _currentSession.set(session) + try { + block.run() + } finally { + _currentSession.remove() + } + } + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt new file mode 100644 index 0000000..609b2a7 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt @@ -0,0 +1,116 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.lang.classfile.* +import java.lang.constant.ConstantDescs.* +import java.lang.constant.MethodTypeDesc +import java.lang.reflect.AccessFlag + +private val logger = KotlinLogging.logger { } + +internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tion/jda/api/JDABuilder") { + + override fun transform(classData: ByteArray): ByteArray { + val classFile = ClassFile.of() + return classFile.transformClass( + classFile.parse(classData), + PublicInstanceMethodTransform() + .andThen(ConstructorTransform()) + ) + } +} + +private class ConstructorTransform : ClassTransform { + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString("")) return classBuilder.retain(classElement) + + val methodType = methodModel.methodTypeSymbol() + if (methodType.parameterList() != listOf(CD_String, CD_int)) { + // TODO not sure about the exception model yet, + // maybe we should just disable the JDA cache instead of being completely incompatible + throw IllegalArgumentException("Incompatible JDABuilder constructor: $methodType") + } + + logger.trace { "Transforming JDABuilder's constructor" } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val tokenSlot = codeBuilder.parameterSlot(0) + val intentsSlot = codeBuilder.parameterSlot(1) + + // JDABuilderSession session = JDABuilderSession.currentSession(); + codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(builderSessionSlot) + + // session.onInit(token, intents); + codeBuilder.aload(builderSessionSlot) + codeBuilder.aload(tokenSlot) + codeBuilder.iload(intentsSlot) + codeBuilder.invokevirtual(classDesc(), "onInit", MethodTypeDesc.of(CD_void, CD_String, CD_int)) + + // Add existing instructions + codeModel.forEach { codeBuilder.with(it) } + } + } + } +} + +private class PublicInstanceMethodTransform : ClassTransform { + + private val builderSessionMethods: Map = ClassFile.of() + .parse(JDABuilderSession::class.java.getResourceAsStream("JDABuilderSession.class")!!.readAllBytes()) + .methods() + .associateBy { it.methodTypeSymbol() } + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.flags().has(AccessFlag.PUBLIC)) return classBuilder.retain(classElement) + if (methodModel.flags().has(AccessFlag.STATIC)) return classBuilder.retain(classElement) + + logger.trace { "Transforming ${methodModel.methodName().stringValue()}" } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) + + val hasBuilderSessionMethod = methodModel.methodTypeSymbol().changeReturnType(CD_void) in builderSessionMethods + methodBuilder.withCode { codeBuilder -> + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // JDABuilderSession session = JDABuilderSession.currentSession(); + codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(builderSessionSlot) + + if (hasBuilderSessionMethod) { + logger.trace { "Registering $methodModel as a cache-compatible method" } + + val methodName = methodModel.methodName().stringValue() + // Set return type to "void" because our method won't return JDABuilder, and it doesn't matter anyway + val methodType = methodModel.methodTypeSymbol().changeReturnType(CD_void) + + // session.theMethod(parameters); + codeBuilder.aload(builderSessionSlot) + methodType.parameterList().forEachIndexed { index, parameter -> + val typeKind = TypeKind.fromDescriptor(parameter.descriptorString()) + val slot = codeBuilder.parameterSlot(index) + codeBuilder.loadLocal(typeKind, slot) + } + codeBuilder.invokevirtual(classDesc(), methodName, methodType) + } else { + logger.trace { "Skipping $methodModel as it does not have an equivalent method handler" } + + // session.markIncompatible() + codeBuilder.aload(builderSessionSlot) + codeBuilder.invokevirtual(classDesc(), "markIncompatible", MethodTypeDesc.of(CD_void)) + } + + // Add existing instructions + codeModel.forEach { codeBuilder.with(it) } + } + } + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt new file mode 100644 index 0000000..5e94f41 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt @@ -0,0 +1,109 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassFile.* +import java.lang.classfile.CodeModel +import java.lang.classfile.MethodModel +import java.lang.constant.* +import java.lang.constant.ConstantDescs.CD_String +import java.lang.constant.ConstantDescs.CD_void +import java.lang.invoke.* + +private val logger = KotlinLogging.logger { } + +internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/api/core/JDAService") { + + private const val TARGET_METHOD_NAME = "onReadyEvent\$BotCommands" + private const val TARGET_METHOD_SIGNATURE = "(Lio/github/freya022/botcommands/api/core/events/BReadyEvent;Lnet/dv8tion/jda/api/hooks/IEventManager;)V" + + override fun transform(classData: ByteArray): ByteArray { + val classFile = ClassFile.of() + var hasModifiedMethod = false + val newBytes = classFile.transformClass(classFile.parse(classData)) { classBuilder, classElement -> + val methodModel = classElement as? MethodModel ?: return@transformClass classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString(TARGET_METHOD_NAME)) return@transformClass classBuilder.retain(classElement) + if (!methodModel.methodType().equalsString(TARGET_METHOD_SIGNATURE)) return@transformClass classBuilder.retain(classElement) + + hasModifiedMethod = true + + // Put the original code of onReadyEvent in the lambda, + // it will be fired by JDABuilderSession.withBuilderSession in onReadyEvent + val lambdaName = "lambda\$onReadyEvent\$BotCommands\$withBuilderSession" + classBuilder.withMethodBody( + lambdaName, + MethodTypeDesc.ofDescriptor(TARGET_METHOD_SIGNATURE), + ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL + ) { codeBuilder -> + + val codeModel = methodModel.code().get() + codeModel.forEach { codeBuilder.with(it) } + } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + val readyEventSlot = codeBuilder.parameterSlot(0) + val eventManagerSlot = codeBuilder.parameterSlot(1) + + codeBuilder.aload(thisSlot) + codeBuilder.aload(readyEventSlot) + codeBuilder.aload(eventManagerSlot) + codeBuilder.invokedynamic(DynamicCallSiteDesc.of( + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + classDesc(), + "metafactory", + MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) + ), + // The following parameters are from [[LambdaMetafactory#metafactory]] + // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", + // the method name in Runnable is "run" + "run", + // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", + // the return type is the implemented interface, + // while the parameters are the captured variables + MethodTypeDesc.of(classDesc(), JDAServiceClassDesc, BReadyEventClassDesc, IEventManagerClassDesc), + // Bootstrap arguments (see `javap -c -v ` from a working .java sample) + // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", + // which is the signature of the implemented method, in this case, void Runnable.run() + MethodTypeDesc.of(CD_void), + // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", + // this is the method to be called when invoking the lambda, + // with the captured variables and parameters + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + JDAServiceClassDesc, + lambdaName, + MethodTypeDesc.of(CD_void, BReadyEventClassDesc, IEventManagerClassDesc) + ), + // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", + // this is "the signature and return type to be enforced dynamically at invocation type" + // This is usually the same as "interfaceMethodType" + MethodTypeDesc.of(CD_void), + )) + + codeBuilder.invokestatic(classDesc(), "withBuilderSession", MethodTypeDesc.of(CD_void, classDesc())) + codeBuilder.return_() + } + } + } + + check(hasModifiedMethod) { + "Could not find JDAService#onReadyEvent(BReadyEvent, IEventManager)" + } + + return newBytes + } + + private val JDAServiceClassDesc: ClassDesc + get() = ClassDesc.ofDescriptor("Lio/github/freya022/botcommands/api/core/JDAService;") + + private val BReadyEventClassDesc: ClassDesc + get() = ClassDesc.ofDescriptor("Lio/github/freya022/botcommands/api/core/events/BReadyEvent;") + + private val IEventManagerClassDesc: ClassDesc + get() = ClassDesc.ofDescriptor("Lnet/dv8tion/jda/api/hooks/IEventManager;") +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt new file mode 100644 index 0000000..89a74ae --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt @@ -0,0 +1,11 @@ +package dev.freya02.botcommands.restart.jda.cache + +import java.lang.classfile.ClassFileBuilder +import java.lang.classfile.ClassFileElement +import java.lang.constant.ClassDesc + +internal inline fun classDesc(): ClassDesc = ClassDesc.of(T::class.java.name) + +internal fun ClassFileBuilder.retain(element: E) { + with(element) +} \ No newline at end of file diff --git a/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java b/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java new file mode 100644 index 0000000..f531159 --- /dev/null +++ b/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java @@ -0,0 +1,36 @@ +package dev.freya02.botcommands.restart.jda.cache; + +import io.github.freya022.botcommands.api.core.JDAService; +import io.github.freya022.botcommands.api.core.events.BReadyEvent; +import net.dv8tion.jda.api.hooks.IEventManager; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.cache.CacheFlag; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public class Test extends JDAService { + + @NotNull + @Override + public Set getIntents() { + return Set.of(); + } + + @NotNull + @Override + public Set getCacheFlags() { + return Set.of(); + } + + @Override + protected void createJDA(@NotNull BReadyEvent bReadyEvent, @NotNull IEventManager iEventManager) { + System.out.println("Test"); + } + + void something(BReadyEvent bReadyEvent, IEventManager iEventManager) { + JDABuilderSession.withBuilderSession(() -> { + + }); + } +} diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt new file mode 100644 index 0000000..63d1cf6 --- /dev/null +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt @@ -0,0 +1,79 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.mockk.* +import net.dv8tion.jda.api.JDABuilder +import net.dv8tion.jda.api.OnlineStatus +import net.dv8tion.jda.api.requests.GatewayIntent +import okhttp3.OkHttpClient +import kotlin.test.Test + +class JDABuilderTransformerTest { + + @Test + fun `Constructor is instrumented`() { + val builderSession = mockk { + every { onInit(any(), any()) } just runs + every { markIncompatible() } just runs + } + + mockkObject(JDABuilderSession) + every { JDABuilderSession.currentSession() } answers { builderSession } + + JDABuilder.create("MY_TOKEN", setOf(GatewayIntent.GUILD_MEMBERS)) + + verify(exactly = 1) { builderSession.onInit("MY_TOKEN", GatewayIntent.getRaw(GatewayIntent.GUILD_MEMBERS)) } + } + + @Test + fun `Unsupported instance method invalidates cache`() { + // Initial set up, this *may* call "markIncompatible" so we need to do it before really mocking + val builder = createJDABuilder() + + // Actual test + val builderSession = mockk { + every { onInit(any(), any()) } just runs + every { markIncompatible() } just runs + } + + mockkObject(JDABuilderSession) + every { JDABuilderSession.currentSession() } returns builderSession + + builder.setHttpClientBuilder(OkHttpClient.Builder()) + + verify(exactly = 1) { builderSession.markIncompatible() } + } + + @Test + fun `Instance method is instrumented`() { + // Initial set up, this *may* call "markIncompatible" so we need to do it before really mocking + val builder = createJDABuilder() + + // Actual test + val builderSession = mockk { + every { onInit(any(), any()) } just runs + every { setStatus(any()) } just runs + } + + mockkObject(JDABuilderSession) + every { JDABuilderSession.currentSession() } returns builderSession + + builder.setStatus(OnlineStatus.DO_NOT_DISTURB) + + verify(exactly = 1) { builderSession.setStatus(OnlineStatus.DO_NOT_DISTURB) } + } + + /** + * Creates a basic JDABuilder, + * call this on the first line to not record any mocking data before doing the actual test. + */ + private fun createJDABuilder(): JDABuilder { + lateinit var builder: JDABuilder + mockkObject(JDABuilderSession) { + every { JDABuilderSession.currentSession() } returns mockk(relaxUnitFun = true) + + builder = JDABuilder.create("MY_TOKEN", emptySet()) + } + + return builder + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt new file mode 100644 index 0000000..1a462cf --- /dev/null +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt @@ -0,0 +1,41 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.github.freya022.botcommands.api.core.JDAService +import io.github.freya022.botcommands.api.core.events.BReadyEvent +import io.mockk.* +import net.dv8tion.jda.api.hooks.IEventManager +import net.dv8tion.jda.api.requests.GatewayIntent +import net.dv8tion.jda.api.utils.cache.CacheFlag +import kotlin.test.Test + +class JDAServiceTransformerTest { + + class Bot : JDAService() { + + override val intents: Set = emptySet() + override val cacheFlags: Set = emptySet() + + public override fun createJDA(event: BReadyEvent, eventManager: IEventManager) { + println("createJDA") + } + } + + @Test + fun `Event listener is instrumented`() { + mockkObject(JDABuilderSession) + every { JDABuilderSession.withBuilderSession(any()) } answers { callOriginal() } // Will call onReadyEvent + + val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) + val bot = mockk { + every { createJDA(any(), any()) } just runs + every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call createJDA + } + + val readyEvent = mockk() + val eventManager = mockk() + + onReadyEvent.invoke(bot, readyEvent, eventManager) + + verify(exactly = 1) { bot.createJDA(readyEvent, eventManager) } + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/test/resources/logback-test.xml b/restarter-jda-cache/src/test/resources/logback-test.xml new file mode 100644 index 0000000..4a6e03c --- /dev/null +++ b/restarter-jda-cache/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + + %d{HH:mm:ss.SSS} %boldCyan(%-26.-26thread) %boldYellow(%-20.-20logger{0}) %highlight(%-6level) %msg%n%throwable + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 10eefa2..f611fa7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,4 +2,5 @@ rootProject.name = "BotCommands-Restarter" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") -include("restarter") \ No newline at end of file +include("restarter") +include("restarter-jda-cache") \ No newline at end of file From aa622be8e14bc628cb95b7bfd760360b3091e0e5 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:19:45 +0200 Subject: [PATCH 20/68] Clean up build.gradle.kts --- restarter-jda-cache/build.gradle.kts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts index cfd3ccc..2ddfe89 100644 --- a/restarter-jda-cache/build.gradle.kts +++ b/restarter-jda-cache/build.gradle.kts @@ -2,7 +2,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { id("BotCommands-Restarter-conventions") - `java-library` `maven-publish` } @@ -47,14 +46,6 @@ tasks.withType { jvmArgs("-javaagent:${jar.archiveFile.get().asFile.absolutePath}") } -//val copyForAgent by tasks.registering(Copy::class) { -// from(jar) -// into(layout.buildDirectory.dir("libs")) -// rename { "BotCommands-Restarter-JDA-Cache.jar" } -//} -// -//jar.finalizedBy(copyForAgent) - publishing { publications { create("maven") { From 25e3b4bb66d399351bc5ab03156a6d7f2e4e4623 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Tue, 3 Jun 2025 22:38:16 +0200 Subject: [PATCH 21/68] Start adding builder caching, getting a cache key --- ...tCommands-Restarter-conventions.gradle.kts | 1 + gradle/libs.versions.toml | 2 +- restarter-jda-cache/build.gradle.kts | 3 +- .../restart/jda/cache/JDABuilderSession.kt | 31 ++++++++++- .../jda/cache/JDAServiceTransformer.kt | 40 ++++++++++++++- .../botcommands/restart/jda/cache/Test.java | 8 ++- .../jda/cache/JDAServiceTransformerTest.kt | 51 +++++++++++++++++-- 7 files changed, 125 insertions(+), 11 deletions(-) diff --git a/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts b/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts index ed17d27..93d88bb 100644 --- a/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts @@ -20,6 +20,7 @@ java { repositories { mavenCentral() + mavenLocal() } dependencies { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6bd8d06..bbf0343 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] kotlin = "2.2.0-RC" # Also change in buildSrc -botcommands = "80c4a99412" +botcommands = "3.0.0-beta.2_DEV" jda = "5.5.1" mockk = "1.13.16" bytebuddy = "1.17.5" diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts index 2ddfe89..6aacec1 100644 --- a/restarter-jda-cache/build.gradle.kts +++ b/restarter-jda-cache/build.gradle.kts @@ -12,13 +12,12 @@ repositories { dependencies { implementation(projects.restarter) implementation(libs.kotlin.logging) - implementation(libs.jda) + implementation(libs.botcommands) testImplementation(kotlin("test")) testImplementation(libs.mockk) testImplementation(libs.bytebuddy) testImplementation(libs.logback.classic) - testImplementation(libs.botcommands) } java { diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index ce15860..d0e8a79 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -1,13 +1,20 @@ package dev.freya02.botcommands.restart.jda.cache +import io.github.freya022.botcommands.api.core.BContext import io.github.oshai.kotlinlogging.KotlinLogging +import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.JDABuilder import net.dv8tion.jda.api.OnlineStatus private val logger = KotlinLogging.logger { } -class JDABuilderSession { +class JDABuilderSession( + private val key: String, +) { private var isIncompatible = false + var wasBuilt: Boolean = false + private set private val builderValues: MutableMap = hashMapOf() // So we can track the initial token and intents, the constructor will be instrumented and call this method @@ -25,6 +32,17 @@ class JDABuilderSession { builderValues[ValueType.STATUS] = status } + fun onBuild(builder: JDABuilder): JDA { + // TODO use user-provided constant-per-instance cache key + + // If no cached instance is present, save if compatible + // If there is a cached instance, and it is compatible with the current parameters, return existing instance + + wasBuilt = true + + TODO() + } + enum class ValueType { TOKEN, INTENTS, @@ -40,15 +58,24 @@ class JDABuilderSession { return _currentSession.get() } + @JvmStatic + fun getCacheKey(context: BContext): String? { + return context.config.restartConfig.cacheKey + } + @JvmStatic fun withBuilderSession( + key: String, // Use Java function types to make codegen a bit more reliable block: Runnable ) { - val session = JDABuilderSession() + val session = JDABuilderSession(key) _currentSession.set(session) try { block.run() + if (!session.wasBuilt) { + logger.warn { "Could not save/restore any JDA session as none were built" } + } } finally { _currentSession.remove() } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt index 5e94f41..92030b3 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt @@ -1,10 +1,12 @@ package dev.freya02.botcommands.restart.jda.cache +import io.github.freya022.botcommands.api.core.BContext import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.* import java.lang.classfile.CodeModel import java.lang.classfile.MethodModel +import java.lang.classfile.TypeKind import java.lang.constant.* import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void @@ -45,9 +47,35 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ methodBuilder.withCode { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() + val readyEventSlot = codeBuilder.parameterSlot(0) val eventManagerSlot = codeBuilder.parameterSlot(1) + val contextSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val sessionKeySlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val sessionRunnableSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // var context = event.getContext() + // We could inline this to avoid a successive store/load, + // but I think using variables is probably a better practice, let's leave the optimization to the VM + codeBuilder.aload(readyEventSlot) + codeBuilder.invokevirtual(BReadyEventClassDesc, "getContext", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(contextSlot) + + // var key = JDABuilderSession.getCacheKey(context) + codeBuilder.aload(contextSlot) + codeBuilder.invokestatic(classDesc(), "getCacheKey", MethodTypeDesc.of(CD_String, classDesc())) + codeBuilder.astore(sessionKeySlot) + + // THE KEY IS NULLABLE + // If it is, then don't make a session + val nullKeyLabel = codeBuilder.newLabel() + + // if (key == null) -> nullKeyLabel + codeBuilder.aload(sessionKeySlot) + codeBuilder.ifnull(nullKeyLabel) + + // Runnable sessionRunnable = this::[lambdaName] codeBuilder.aload(thisSlot) codeBuilder.aload(readyEventSlot) codeBuilder.aload(eventManagerSlot) @@ -84,8 +112,18 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ // This is usually the same as "interfaceMethodType" MethodTypeDesc.of(CD_void), )) + codeBuilder.astore(sessionRunnableSlot) + + // JDABuilderSession.withBuilderSession(key, this::[lambdaName]) + codeBuilder.aload(sessionKeySlot) + codeBuilder.aload(sessionRunnableSlot) + codeBuilder.invokestatic(classDesc(), "withBuilderSession", MethodTypeDesc.of(CD_void, CD_String, classDesc())) + + // Required + codeBuilder.return_() - codeBuilder.invokestatic(classDesc(), "withBuilderSession", MethodTypeDesc.of(CD_void, classDesc())) + // nullKeyLabel code + codeBuilder.labelBinding(nullKeyLabel) codeBuilder.return_() } } diff --git a/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java b/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java index f531159..9ba8eca 100644 --- a/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java +++ b/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java @@ -29,8 +29,12 @@ protected void createJDA(@NotNull BReadyEvent bReadyEvent, @NotNull IEventManage } void something(BReadyEvent bReadyEvent, IEventManager iEventManager) { - JDABuilderSession.withBuilderSession(() -> { + var a = "a"; + if (a == null) { + System.out.println("null"); + return; + } - }); + System.out.println("not null"); } } diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt index 1a462cf..65fa185 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt @@ -23,19 +23,64 @@ class JDAServiceTransformerTest { @Test fun `Event listener is instrumented`() { mockkObject(JDABuilderSession) - every { JDABuilderSession.withBuilderSession(any()) } answers { callOriginal() } // Will call onReadyEvent + every { JDABuilderSession.withBuilderSession(any(), any()) } answers { callOriginal() } // Will call createJDA val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) val bot = mockk { every { createJDA(any(), any()) } just runs - every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call createJDA + every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call withBuilderSession } - val readyEvent = mockk() + val readyEvent = mockk { + every { context.config.restartConfig.cacheKey } returns "Test" + } val eventManager = mockk() onReadyEvent.invoke(bot, readyEvent, eventManager) verify(exactly = 1) { bot.createJDA(readyEvent, eventManager) } } + + @Test + fun `Cache key enables builder sessions`() { + mockkObject(JDABuilderSession) + every { JDABuilderSession.withBuilderSession(any(), any()) } answers { callOriginal() } + + val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) + val bot = mockk { + every { createJDA(any(), any()) } just runs + every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call withBuilderSession + } + + val readyEvent = mockk { + every { context.config.restartConfig.cacheKey } returns "Test" + } + val eventManager = mockk() + + onReadyEvent.invoke(bot, readyEvent, eventManager) + + verify(exactly = 1) { JDABuilderSession.withBuilderSession(any(), any()) } + } + + @Test + fun `Null cache key disables builder sessions`() { + mockkObject(JDABuilderSession) + every { JDABuilderSession.withBuilderSession(any(), any()) } answers { callOriginal() } + every { JDABuilderSession.getCacheKey(any()) } answers { callOriginal() } + + val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) + val bot = mockk { + every { createJDA(any(), any()) } just runs + every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call withBuilderSession + } + + val readyEvent = mockk { + every { context.config.restartConfig.cacheKey } returns null + } + val eventManager = mockk() + + onReadyEvent.invoke(bot, readyEvent, eventManager) + + verify(exactly = 0) { JDABuilderSession.withBuilderSession(any(), any()) } + } } \ No newline at end of file From f528397551f9fdd9f9cabcf47590f967113baf24 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:04:29 +0200 Subject: [PATCH 22/68] Hook JDABuilder#build to return a cached instance --- .../restart/jda/cache/JDABuilderSession.kt | 27 +++-- .../jda/cache/JDABuilderTransformer.kt | 103 +++++++++++++++++- .../botcommands/restart/jda/cache/JDACache.kt | 16 +++ .../jda/cache/JDABuilderTransformerTest.kt | 16 +++ 4 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index d0e8a79..64cfbc2 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -3,8 +3,8 @@ package dev.freya02.botcommands.restart.jda.cache import io.github.freya022.botcommands.api.core.BContext import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.JDABuilder import net.dv8tion.jda.api.OnlineStatus +import java.util.function.Supplier private val logger = KotlinLogging.logger { } @@ -32,15 +32,28 @@ class JDABuilderSession( builderValues[ValueType.STATUS] = status } - fun onBuild(builder: JDABuilder): JDA { - // TODO use user-provided constant-per-instance cache key - - // If no cached instance is present, save if compatible - // If there is a cached instance, and it is compatible with the current parameters, return existing instance + fun onBuild(buildFunction: Supplier): JDA { + val jda: JDA + if (isIncompatible) { + logger.debug { "Configured JDABuilder is incompatible, building a new JDA instance with key '$key'" } + jda = buildFunction.get() + JDACache[key] = jda + } else if (key in JDACache) { + logger.debug { "Reusing JDA instance with key '$key'" } + jda = JDACache[key]!! + // TODO need to set the event manager to the new instance + // Pass the new IEventManager to session constructor + // then wrap and set it here + } else { + logger.debug { "Saving a new JDA instance with key '$key'" } + jda = buildFunction.get() + JDACache[key] = jda + // TODO wrap event manager and set it + } wasBuilt = true - TODO() + return jda } enum class ValueType { diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt index 609b2a7..f940d20 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt @@ -2,12 +2,18 @@ package dev.freya02.botcommands.restart.jda.cache import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* +import java.lang.classfile.ClassFile.* +import java.lang.constant.* import java.lang.constant.ConstantDescs.* -import java.lang.constant.MethodTypeDesc +import java.lang.invoke.* import java.lang.reflect.AccessFlag +import java.util.function.Supplier private val logger = KotlinLogging.logger { } +private val JDADesc = ClassDesc.of("net.dv8tion.jda.api.JDA") +private val JDABuilderDesc = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") + internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tion/jda/api/JDABuilder") { override fun transform(classData: ByteArray): ByteArray { @@ -16,6 +22,7 @@ internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tio classFile.parse(classData), PublicInstanceMethodTransform() .andThen(ConstructorTransform()) + .andThen(BuildTransform()) ) } } @@ -60,6 +67,100 @@ private class ConstructorTransform : ClassTransform { } } +private class BuildTransform : ClassTransform { + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString("build")) return classBuilder.retain(classElement) + + val methodType = methodModel.methodTypeSymbol() + if (methodType.parameterList() != emptyList()) { + // TODO not sure about the exception model yet, + // maybe we should just disable the JDA cache instead of being completely incompatible + throw IllegalArgumentException("Incompatible JDABuilder build method: $methodType") + } + + logger.trace { "Transforming JDABuilder's build() method" } + + val newBuildMethodName = "doBuild" + classBuilder.withMethod( + newBuildMethodName, + MethodTypeDesc.of(JDADesc), + ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL + ) { methodBuilder -> + val codeModel = methodModel.code().get() + + methodBuilder.withCode { codeBuilder -> + // Move the build() code to doBuild() + codeModel.forEach { codeBuilder.with(it) } + } + } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val doBuildSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val jdaSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // Supplier doBuild = this::doBuild + codeBuilder.aload(thisSlot) + codeBuilder.invokedynamic(DynamicCallSiteDesc.of( + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + classDesc(), + "metafactory", + MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) + ), + // The following parameters are from [[LambdaMetafactory#metafactory]] + // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", + // the method name in Supplier is "get" + "get", + // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", + // the return type is the implemented interface, + // while the parameters are the captured variables (incl. receiver) + MethodTypeDesc.of(classDesc>(), JDABuilderDesc), + // Bootstrap arguments (see `javap -c -v ` from a working .java sample) + // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", + // which is the signature of the implemented method, in this case, Object get() + MethodTypeDesc.of(CD_Object), + // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", + // this is the method to be called when invoking the lambda, + // with the captured variables and parameters + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + JDABuilderDesc, + newBuildMethodName, + MethodTypeDesc.of(JDADesc) + ), + // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", + // this is "the signature and return type to be enforced dynamically at invocation type" + // This is usually the same as "interfaceMethodType" + MethodTypeDesc.of(CD_Object), + )) + codeBuilder.astore(doBuildSlot) + + // JDABuilderSession session = JDABuilderSession.currentSession(); + codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(builderSessionSlot) + + // var jda = session.onBuild(this::doBuild); + codeBuilder.aload(builderSessionSlot) + codeBuilder.aload(doBuildSlot) + codeBuilder.invokevirtual(classDesc(), "onBuild", MethodTypeDesc.of(JDADesc, classDesc>())) + // Again, prefer using a variable for clarity + codeBuilder.astore(jdaSlot) + + codeBuilder.aload(jdaSlot) + codeBuilder.areturn() + } + } + } +} + private class PublicInstanceMethodTransform : ClassTransform { private val builderSessionMethods: Map = ClassFile.of() diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt new file mode 100644 index 0000000..9f9ec47 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt @@ -0,0 +1,16 @@ +package dev.freya02.botcommands.restart.jda.cache + +import net.dv8tion.jda.api.JDA + +internal object JDACache { + + private val cache: MutableMap = hashMapOf() + + internal operator fun contains(key: String): Boolean = key in cache + + internal operator fun get(key: String): JDA? = cache[key] + + internal operator fun set(key: String, instance: JDA) { + cache[key] = instance + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt index 63d1cf6..425c54a 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt @@ -62,6 +62,22 @@ class JDABuilderTransformerTest { verify(exactly = 1) { builderSession.setStatus(OnlineStatus.DO_NOT_DISTURB) } } + @Test + fun `Build method is instrumented`() { + val builderSession = mockk { + every { onInit(any(), any()) } just runs + every { markIncompatible() } just runs + every { onBuild(any()) } returns mockk() + } + + mockkObject(JDABuilderSession) + every { JDABuilderSession.currentSession() } returns builderSession + + JDABuilder.createDefault("MY_TOKEN").build() + + verify(exactly = 1) { builderSession.onBuild(any()) } + } + /** * Creates a basic JDABuilder, * call this on the first line to not record any mocking data before doing the actual test. From 7bced2a556b1bd867a0fecb35a8f5b74352cda69 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 4 Jun 2025 13:04:49 +0200 Subject: [PATCH 23/68] Add notes --- .../restart/jda/cache/JDABuilderSession.kt | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 64cfbc2..070952f 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -8,6 +8,19 @@ import java.util.function.Supplier private val logger = KotlinLogging.logger { } +// TODO there may be an issue with REST requests, +// as the instance will not get shut down, the requester will still run any request currently queued +// so we should find a way to cancel the tasks in the rate limiter + +// TODO a similar feature exists at https://github.com/LorittaBot/DeviousJDA/blob/master/src/examples/java/SessionCheckpointAndGatewayResumeExample.kt +// however as it is a JDA fork, users will not be able to use the latest features, +// there is also a risk that the saved data (checkpoint) could miss fields + +// TODO another way of building this feature is to have the user use an external gateway proxy, such as https://github.com/Gelbpunkt/gateway-proxy +// however such a solution introduces a lot of friction, +// requiring to set up JDA manually, though not complicated, but also docker and that container's config +// An hybrid way would require rewriting that proxy, +// so our module can hook into JDA and set the gateway URL to the proxy's class JDABuilderSession( private val key: String, ) { @@ -25,6 +38,7 @@ class JDABuilderSession( } fun markIncompatible() { + // TODO log which method is incompatible, pass the method name using codegen isIncompatible = true } @@ -32,6 +46,14 @@ class JDABuilderSession( builderValues[ValueType.STATUS] = status } + // TODO make onShutdown(shutdownFunction: Runnable) + // Do nothing initially, save the callback + // When building the new instance, shutdown if the new instance is incompatible + // If it is compatible then swap the event manager and send the events + // This may actually not be an actual swap, we could use a SPI to provide our own IEventManager implementation, + // which we use on all instances, this way we can control exactly when to buffer events + // and when to release them to the actual event manager + fun onBuild(buildFunction: Supplier): JDA { val jda: JDA if (isIncompatible) { @@ -76,6 +98,7 @@ class JDABuilderSession( return context.config.restartConfig.cacheKey } + // TODO maybe we should pass the IEventManager so we can set it on the new/current event manager @JvmStatic fun withBuilderSession( key: String, From b32a5c594142833a70c295a333c427c6a1a8a1b2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:09:20 +0200 Subject: [PATCH 24/68] Hook JDAImpl#shutdown() to store the shutdown code So we can run it when an instance is incompatible --- .../botcommands/restart/jda/cache/Agent.kt | 1 + .../restart/jda/cache/JDABuilderSession.kt | 7 ++ .../restart/jda/cache/JDAImplTransformer.kt | 114 ++++++++++++++++++ .../jda/cache/JDAImplTransformerTest.kt | 26 ++++ 4 files changed, 148 insertions(+) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt create mode 100644 restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt index 07f5d0f..5317c0d 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt @@ -9,5 +9,6 @@ object Agent { println("Agent args: $agentArgs") inst.addTransformer(JDABuilderTransformer) inst.addTransformer(JDAServiceTransformer) + inst.addTransformer(JDAImplTransformer) } } \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 070952f..d65dfa6 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -29,6 +29,7 @@ class JDABuilderSession( var wasBuilt: Boolean = false private set private val builderValues: MutableMap = hashMapOf() + private lateinit var shutdownFunction: Runnable // So we can track the initial token and intents, the constructor will be instrumented and call this method // The user overriding the values using token/intent setters should not be an issue @@ -53,6 +54,9 @@ class JDABuilderSession( // This may actually not be an actual swap, we could use a SPI to provide our own IEventManager implementation, // which we use on all instances, this way we can control exactly when to buffer events // and when to release them to the actual event manager + fun onShutdown(shutdownFunction: Runnable) { + this.shutdownFunction = shutdownFunction + } fun onBuild(buildFunction: Supplier): JDA { val jda: JDA @@ -85,6 +89,9 @@ class JDABuilderSession( } companion object { + // TODO maybe we should switch to a "create" function which store a session into a map where the key is the session key + // so we don't need the JDACache object, and we can also store the event manager directly + // Then replace currentSession() with getSession(cacheKey: String) private val _currentSession: ThreadLocal = ThreadLocal.withInitial { error("No JDABuilderSession exists for this thread") } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt new file mode 100644 index 0000000..3e245be --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt @@ -0,0 +1,114 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.github.oshai.kotlinlogging.KotlinLogging +import java.lang.classfile.* +import java.lang.classfile.ClassFile.* +import java.lang.constant.* +import java.lang.constant.ConstantDescs.CD_String +import java.lang.constant.ConstantDescs.CD_void +import java.lang.invoke.* + +private val logger = KotlinLogging.logger { } + +private val JDAImplDesc = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") + +internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { + + override fun transform(classData: ByteArray): ByteArray { + val classFile = ClassFile.of() + return classFile.transformClass( + classFile.parse(classData), + ShutdownTransform() + ) + } +} + +private class ShutdownTransform : ClassTransform { + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString("shutdown")) return classBuilder.retain(classElement) + + val methodType = methodModel.methodTypeSymbol() + if (methodType.parameterList() != emptyList()) { + // TODO not sure about the exception model yet, + // maybe we should just disable the JDA cache instead of being completely incompatible + throw IllegalArgumentException("Incompatible JDAImpl shutdown method: $methodType") + } + + logger.trace { "Transforming JDABuilder's build() method" } + + val newShutdownMethodName = "doShutdown" + classBuilder.withMethod( + newShutdownMethodName, + MethodTypeDesc.of(CD_void), + ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL + ) { methodBuilder -> + val codeModel = methodModel.code().get() + + methodBuilder.withCode { codeBuilder -> + // Move the shutdown() code to doShutdown() + codeModel.forEach { codeBuilder.with(it) } + } + } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val doShutdownSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // Runnable doShutdown = this::doShutdown + codeBuilder.aload(thisSlot) + codeBuilder.invokedynamic(DynamicCallSiteDesc.of( + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + classDesc(), + "metafactory", + MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) + ), + // The following parameters are from [[LambdaMetafactory#metafactory]] + // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", + // the method name in Supplier is "get" + "run", + // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", + // the return type is the implemented interface, + // while the parameters are the captured variables (incl. receiver) + MethodTypeDesc.of(classDesc(), JDAImplDesc), + // Bootstrap arguments (see `javap -c -v ` from a working .java sample) + // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", + // which is the signature of the implemented method, in this case, void run() + MethodTypeDesc.of(CD_void), + // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", + // this is the method to be called when invoking the lambda, + // with the captured variables and parameters + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + JDAImplDesc, + newShutdownMethodName, + MethodTypeDesc.of(CD_void) + ), + // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", + // this is "the signature and return type to be enforced dynamically at invocation type" + // This is usually the same as "interfaceMethodType" + MethodTypeDesc.of(CD_void), + )) + codeBuilder.astore(doShutdownSlot) + + // JDABuilderSession session = JDABuilderSession.currentSession(); + codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(builderSessionSlot) + + // session.onShutdown(this::doShutdown); + codeBuilder.aload(builderSessionSlot) + codeBuilder.aload(doShutdownSlot) + codeBuilder.invokevirtual(classDesc(), "onShutdown", MethodTypeDesc.of(CD_void, classDesc())) + + codeBuilder.return_() + } + } + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt new file mode 100644 index 0000000..98f1182 --- /dev/null +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt @@ -0,0 +1,26 @@ +package dev.freya02.botcommands.restart.jda.cache + +import io.mockk.* +import net.dv8tion.jda.internal.JDAImpl +import kotlin.test.Test + +class JDAImplTransformerTest { + + @Test + fun `Shutdown method is instrumented`() { + val builderSession = mockk { + every { onShutdown(any()) } just runs + } + + mockkObject(JDABuilderSession) + every { JDABuilderSession.currentSession() } returns builderSession + + val jda = mockk { + every { shutdown() } answers { callOriginal() } + } + + jda.shutdown() + + verify(exactly = 1) { builderSession.onShutdown(any()) } + } +} \ No newline at end of file From 40105b52e993026856882a568540dbdcbdd24038 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 4 Jun 2025 17:44:01 +0200 Subject: [PATCH 25/68] Move ClassFileTransformer(s) to package, use naming scheme of the JDK for ClassDesc(s) --- .../botcommands/restart/jda/cache/Agent.kt | 3 ++ .../AbstractClassFileTransformer.kt | 6 ++-- .../JDABuilderTransformer.kt | 18 +++++++----- .../{ => transformer}/JDAImplTransformer.kt | 10 ++++--- .../JDAServiceTransformer.kt | 29 +++++++++---------- .../jda/cache/{ => transformer}/utils.kt | 2 +- 6 files changed, 37 insertions(+), 31 deletions(-) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/AbstractClassFileTransformer.kt (83%) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/JDABuilderTransformer.kt (95%) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/JDAImplTransformer.kt (93%) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/JDAServiceTransformer.kt (88%) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/utils.kt (83%) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt index 5317c0d..c3852b0 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt @@ -1,5 +1,8 @@ package dev.freya02.botcommands.restart.jda.cache +import dev.freya02.botcommands.restart.jda.cache.transformer.JDABuilderTransformer +import dev.freya02.botcommands.restart.jda.cache.transformer.JDAImplTransformer +import dev.freya02.botcommands.restart.jda.cache.transformer.JDAServiceTransformer import java.lang.instrument.Instrumentation object Agent { diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/AbstractClassFileTransformer.kt similarity index 83% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/AbstractClassFileTransformer.kt index f0bbbac..39aa209 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/AbstractClassFileTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/AbstractClassFileTransformer.kt @@ -1,9 +1,11 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer import java.lang.instrument.ClassFileTransformer import java.security.ProtectionDomain -internal abstract class AbstractClassFileTransformer protected constructor(private val target: String) : ClassFileTransformer { +internal abstract class AbstractClassFileTransformer protected constructor( + private val target: String +) : ClassFileTransformer { override fun transform( loader: ClassLoader?, diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt similarity index 95% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index f940d20..502b200 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -1,5 +1,6 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* @@ -11,8 +12,9 @@ import java.util.function.Supplier private val logger = KotlinLogging.logger { } -private val JDADesc = ClassDesc.of("net.dv8tion.jda.api.JDA") -private val JDABuilderDesc = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") +// Avoid importing BC and JDA classes +private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") +private val CD_JDABuilder = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tion/jda/api/JDABuilder") { @@ -85,7 +87,7 @@ private class BuildTransform : ClassTransform { val newBuildMethodName = "doBuild" classBuilder.withMethod( newBuildMethodName, - MethodTypeDesc.of(JDADesc), + MethodTypeDesc.of(CD_JDA), ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL ) { methodBuilder -> val codeModel = methodModel.code().get() @@ -122,7 +124,7 @@ private class BuildTransform : ClassTransform { // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", // the return type is the implemented interface, // while the parameters are the captured variables (incl. receiver) - MethodTypeDesc.of(classDesc>(), JDABuilderDesc), + MethodTypeDesc.of(classDesc>(), CD_JDABuilder), // Bootstrap arguments (see `javap -c -v ` from a working .java sample) // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", // which is the signature of the implemented method, in this case, Object get() @@ -132,9 +134,9 @@ private class BuildTransform : ClassTransform { // with the captured variables and parameters MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, - JDABuilderDesc, + CD_JDABuilder, newBuildMethodName, - MethodTypeDesc.of(JDADesc) + MethodTypeDesc.of(CD_JDA) ), // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", // this is "the signature and return type to be enforced dynamically at invocation type" @@ -150,7 +152,7 @@ private class BuildTransform : ClassTransform { // var jda = session.onBuild(this::doBuild); codeBuilder.aload(builderSessionSlot) codeBuilder.aload(doBuildSlot) - codeBuilder.invokevirtual(classDesc(), "onBuild", MethodTypeDesc.of(JDADesc, classDesc>())) + codeBuilder.invokevirtual(classDesc(), "onBuild", MethodTypeDesc.of(CD_JDA, classDesc>())) // Again, prefer using a variable for clarity codeBuilder.astore(jdaSlot) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt similarity index 93% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 3e245be..28aa08f 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -1,5 +1,6 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* @@ -10,7 +11,8 @@ import java.lang.invoke.* private val logger = KotlinLogging.logger { } -private val JDAImplDesc = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") +// Avoid importing BC and JDA classes +private val CD_JDAImpl = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { @@ -77,7 +79,7 @@ private class ShutdownTransform : ClassTransform { // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", // the return type is the implemented interface, // while the parameters are the captured variables (incl. receiver) - MethodTypeDesc.of(classDesc(), JDAImplDesc), + MethodTypeDesc.of(classDesc(), CD_JDAImpl), // Bootstrap arguments (see `javap -c -v ` from a working .java sample) // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", // which is the signature of the implemented method, in this case, void run() @@ -87,7 +89,7 @@ private class ShutdownTransform : ClassTransform { // with the captured variables and parameters MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, - JDAImplDesc, + CD_JDAImpl, newShutdownMethodName, MethodTypeDesc.of(CD_void) ), diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt similarity index 88% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index 92030b3..b9d7161 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -1,6 +1,6 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer -import io.github.freya022.botcommands.api.core.BContext +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.* @@ -14,6 +14,12 @@ import java.lang.invoke.* private val logger = KotlinLogging.logger { } +// Avoid importing BC and JDA classes +private val CD_BContext = ClassDesc.of("io.github.freya022.botcommands.api.core.BContext") +private val CD_JDAService = ClassDesc.of("io.github.freya022.botcommands.api.core.JDAService") +private val CD_BReadyEvent = ClassDesc.of("io.github.freya022.botcommands.api.core.events.BReadyEvent") +private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventManager") + internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/api/core/JDAService") { private const val TARGET_METHOD_NAME = "onReadyEvent\$BotCommands" @@ -59,12 +65,12 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ // We could inline this to avoid a successive store/load, // but I think using variables is probably a better practice, let's leave the optimization to the VM codeBuilder.aload(readyEventSlot) - codeBuilder.invokevirtual(BReadyEventClassDesc, "getContext", MethodTypeDesc.of(classDesc())) + codeBuilder.invokevirtual(CD_BReadyEvent, "getContext", MethodTypeDesc.of(CD_BContext)) codeBuilder.astore(contextSlot) // var key = JDABuilderSession.getCacheKey(context) codeBuilder.aload(contextSlot) - codeBuilder.invokestatic(classDesc(), "getCacheKey", MethodTypeDesc.of(CD_String, classDesc())) + codeBuilder.invokestatic(classDesc(), "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) codeBuilder.astore(sessionKeySlot) // THE KEY IS NULLABLE @@ -93,7 +99,7 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", // the return type is the implemented interface, // while the parameters are the captured variables - MethodTypeDesc.of(classDesc(), JDAServiceClassDesc, BReadyEventClassDesc, IEventManagerClassDesc), + MethodTypeDesc.of(classDesc(), CD_JDAService, CD_BReadyEvent, CD_IEventManager), // Bootstrap arguments (see `javap -c -v ` from a working .java sample) // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", // which is the signature of the implemented method, in this case, void Runnable.run() @@ -103,9 +109,9 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ // with the captured variables and parameters MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, - JDAServiceClassDesc, + CD_JDAService, lambdaName, - MethodTypeDesc.of(CD_void, BReadyEventClassDesc, IEventManagerClassDesc) + MethodTypeDesc.of(CD_void, CD_BReadyEvent, CD_IEventManager) ), // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", // this is "the signature and return type to be enforced dynamically at invocation type" @@ -135,13 +141,4 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ return newBytes } - - private val JDAServiceClassDesc: ClassDesc - get() = ClassDesc.ofDescriptor("Lio/github/freya022/botcommands/api/core/JDAService;") - - private val BReadyEventClassDesc: ClassDesc - get() = ClassDesc.ofDescriptor("Lio/github/freya022/botcommands/api/core/events/BReadyEvent;") - - private val IEventManagerClassDesc: ClassDesc - get() = ClassDesc.ofDescriptor("Lnet/dv8tion/jda/api/hooks/IEventManager;") } \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt similarity index 83% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt index 89a74ae..3222ece 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt @@ -1,4 +1,4 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer import java.lang.classfile.ClassFileBuilder import java.lang.classfile.ClassFileElement From 8a397986c23b26aa45e2a7d7cf09475b5602df35 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Wed, 4 Jun 2025 22:26:10 +0200 Subject: [PATCH 26/68] Refactor out JDABuilder configuration, store more data Also add `@DynamicCall` marker on methods used by codegen --- .../restart/jda/cache/DynamicCall.kt | 10 ++ .../jda/cache/JDABuilderConfiguration.kt | 41 +++++++ .../restart/jda/cache/JDABuilderSession.kt | 113 ++++++++---------- .../botcommands/restart/jda/cache/JDACache.kt | 16 ++- .../transformer/JDABuilderTransformer.kt | 35 +++--- .../cache/transformer/JDAImplTransformer.kt | 64 ++++++++-- .../jda/cache/JDABuilderTransformerTest.kt | 32 ++--- .../jda/cache/JDAImplTransformerTest.kt | 9 +- 8 files changed, 208 insertions(+), 112 deletions(-) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/DynamicCall.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/DynamicCall.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/DynamicCall.kt new file mode 100644 index 0000000..8c393a4 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/DynamicCall.kt @@ -0,0 +1,10 @@ +package dev.freya02.botcommands.restart.jda.cache + +/** + * This member is used by generated code and as such is not directly referenced. + * + * This member must be `public`. + */ +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER) +internal annotation class DynamicCall diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt new file mode 100644 index 0000000..432ebcd --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -0,0 +1,41 @@ +package dev.freya02.botcommands.restart.jda.cache + +import net.dv8tion.jda.api.OnlineStatus + +class JDABuilderConfiguration internal constructor() { + + var hasUnsupportedValues = false + private set + + private val builderValues: MutableMap = hashMapOf() + + // So we can track the initial token and intents, the constructor will be instrumented and call this method + // The user overriding the values using token/intent setters should not be an issue + @DynamicCall + fun onInit(token: String?, intents: Int) { + builderValues[ValueType.TOKEN] = token + builderValues[ValueType.INTENTS] = intents + } + + @DynamicCall + fun markUnsupportedValue() { + // TODO log which method is incompatible, pass the method name using codegen + hasUnsupportedValues = true + } + + @DynamicCall + fun setStatus(status: OnlineStatus) { + builderValues[ValueType.STATUS] = status + } + + infix fun isSameAs(other: JDABuilderConfiguration): Boolean { + // TODO: implement + return super.equals(other) + } + + private enum class ValueType { + TOKEN, + INTENTS, + STATUS, + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index d65dfa6..a082eea 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -3,7 +3,6 @@ package dev.freya02.botcommands.restart.jda.cache import io.github.freya022.botcommands.api.core.BContext import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.JDA -import net.dv8tion.jda.api.OnlineStatus import java.util.function.Supplier private val logger = KotlinLogging.logger { } @@ -25,102 +24,88 @@ class JDABuilderSession( private val key: String, ) { - private var isIncompatible = false + @get:DynamicCall + val configuration = JDABuilderConfiguration() var wasBuilt: Boolean = false private set - private val builderValues: MutableMap = hashMapOf() - private lateinit var shutdownFunction: Runnable - - // So we can track the initial token and intents, the constructor will be instrumented and call this method - // The user overriding the values using token/intent setters should not be an issue - fun onInit(token: String?, intents: Int) { - builderValues[ValueType.TOKEN] = token - builderValues[ValueType.INTENTS] = intents - } - - fun markIncompatible() { - // TODO log which method is incompatible, pass the method name using codegen - isIncompatible = true - } - fun setStatus(status: OnlineStatus) { - builderValues[ValueType.STATUS] = status + @DynamicCall + fun onShutdown(instance: JDA, shutdownFunction: Runnable) { + JDACache[key] = JDACache.Data(configuration, instance, shutdownFunction) } - // TODO make onShutdown(shutdownFunction: Runnable) - // Do nothing initially, save the callback - // When building the new instance, shutdown if the new instance is incompatible - // If it is compatible then swap the event manager and send the events - // This may actually not be an actual swap, we could use a SPI to provide our own IEventManager implementation, - // which we use on all instances, this way we can control exactly when to buffer events - // and when to release them to the actual event manager - fun onShutdown(shutdownFunction: Runnable) { - this.shutdownFunction = shutdownFunction + @DynamicCall + fun onBuild(buildFunction: Supplier): JDA { + val jda = buildOrReuse(buildFunction) + wasBuilt = true + return jda } - fun onBuild(buildFunction: Supplier): JDA { - val jda: JDA - if (isIncompatible) { - logger.debug { "Configured JDABuilder is incompatible, building a new JDA instance with key '$key'" } - jda = buildFunction.get() - JDACache[key] = jda - } else if (key in JDACache) { - logger.debug { "Reusing JDA instance with key '$key'" } - jda = JDACache[key]!! - // TODO need to set the event manager to the new instance - // Pass the new IEventManager to session constructor - // then wrap and set it here - } else { - logger.debug { "Saving a new JDA instance with key '$key'" } - jda = buildFunction.get() - JDACache[key] = jda - // TODO wrap event manager and set it + private fun buildOrReuse(buildFunction: Supplier): JDA { + if (configuration.hasUnsupportedValues) { + logger.debug { "Configured JDABuilder has unsupported values, building a new JDA instance (key '$key')" } + val jda = buildFunction.get() + val oldInstanceData = JDACache.remove(key) + oldInstanceData?.doShutdown?.run() + // TODO: Get event manager then wrap it into our special one + return jda } - wasBuilt = true + fun createNewInstance(): JDA { + val jda = buildFunction.get() + // TODO: Get event manager then wrap it into our special one + return jda + } - return jda - } + val cachedData = JDACache[key] + if (cachedData == null) { + logger.debug { "Creating a new JDA instance (key '$key')" } + return createNewInstance() + } - enum class ValueType { - TOKEN, - INTENTS, - STATUS, + if (cachedData.configuration isSameAs configuration) { + logger.debug { "Reusing JDA instance with compatible configuration (key '$key')" } + val jda = JDACache[key]!!.jda + // TODO: Get event manager, cast it to our special one, + // set the delegate to value of JDA#getEventManager, + // then flush the event cache to it + // TODO: Also send start up events again + return jda + } else { + logger.debug { "Creating a new JDA instance as its configuration changed (key '$key')" } + return createNewInstance() + } } companion object { - // TODO maybe we should switch to a "create" function which store a session into a map where the key is the session key - // so we don't need the JDACache object, and we can also store the event manager directly - // Then replace currentSession() with getSession(cacheKey: String) - private val _currentSession: ThreadLocal = + // I would store them in a Map, but JDABuilder has no idea what the key is + private val activeSession: ThreadLocal = ThreadLocal.withInitial { error("No JDABuilderSession exists for this thread") } @JvmStatic - fun currentSession(): JDABuilderSession { - return _currentSession.get() - } + @DynamicCall + fun currentSession(): JDABuilderSession = activeSession.get() @JvmStatic - fun getCacheKey(context: BContext): String? { - return context.config.restartConfig.cacheKey - } + @DynamicCall + fun getCacheKey(context: BContext): String? = context.config.restartConfig.cacheKey - // TODO maybe we should pass the IEventManager so we can set it on the new/current event manager @JvmStatic + @DynamicCall fun withBuilderSession( key: String, // Use Java function types to make codegen a bit more reliable block: Runnable ) { val session = JDABuilderSession(key) - _currentSession.set(session) + activeSession.set(session) try { block.run() if (!session.wasBuilt) { logger.warn { "Could not save/restore any JDA session as none were built" } } } finally { - _currentSession.remove() + activeSession.remove() } } } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt index 9f9ec47..7ec4ac1 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt @@ -4,13 +4,21 @@ import net.dv8tion.jda.api.JDA internal object JDACache { - private val cache: MutableMap = hashMapOf() + private val cache: MutableMap = hashMapOf() internal operator fun contains(key: String): Boolean = key in cache - internal operator fun get(key: String): JDA? = cache[key] + internal operator fun get(key: String): Data? = cache[key] - internal operator fun set(key: String, instance: JDA) { - cache[key] = instance + internal operator fun set(key: String, data: Data) { + cache[key] = data } + + internal fun remove(key: String): Data? = cache.remove(key) + + internal class Data internal constructor( + val configuration: JDABuilderConfiguration, + val jda: JDA, + val doShutdown: Runnable, + ) } \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 502b200..c7c3ef4 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* @@ -48,19 +49,20 @@ private class ConstructorTransform : ClassTransform { val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) methodBuilder.withCode { codeBuilder -> - val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val builderConfigurationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) val tokenSlot = codeBuilder.parameterSlot(0) val intentsSlot = codeBuilder.parameterSlot(1) - // JDABuilderSession session = JDABuilderSession.currentSession(); + // JDABuilderConfiguration configuration = JDABuilderSession.currentSession().getConfiguration(); codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) - codeBuilder.astore(builderSessionSlot) + codeBuilder.invokevirtual(classDesc(), "getConfiguration", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(builderConfigurationSlot) - // session.onInit(token, intents); - codeBuilder.aload(builderSessionSlot) + // configuration.onInit(token, intents); + codeBuilder.aload(builderConfigurationSlot) codeBuilder.aload(tokenSlot) codeBuilder.iload(intentsSlot) - codeBuilder.invokevirtual(classDesc(), "onInit", MethodTypeDesc.of(CD_void, CD_String, CD_int)) + codeBuilder.invokevirtual(classDesc(), "onInit", MethodTypeDesc.of(CD_void, CD_String, CD_int)) // Add existing instructions codeModel.forEach { codeBuilder.with(it) } @@ -166,7 +168,7 @@ private class BuildTransform : ClassTransform { private class PublicInstanceMethodTransform : ClassTransform { private val builderSessionMethods: Map = ClassFile.of() - .parse(JDABuilderSession::class.java.getResourceAsStream("JDABuilderSession.class")!!.readAllBytes()) + .parse(JDABuilderConfiguration::class.java.getResourceAsStream("JDABuilderConfiguration.class")!!.readAllBytes()) .methods() .associateBy { it.methodTypeSymbol() } @@ -182,11 +184,12 @@ private class PublicInstanceMethodTransform : ClassTransform { val hasBuilderSessionMethod = methodModel.methodTypeSymbol().changeReturnType(CD_void) in builderSessionMethods methodBuilder.withCode { codeBuilder -> - val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val builderConfigurationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - // JDABuilderSession session = JDABuilderSession.currentSession(); + // JDABuilderConfiguration configuration = JDABuilderSession.currentSession().getConfiguration(); codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) - codeBuilder.astore(builderSessionSlot) + codeBuilder.invokevirtual(classDesc(), "getConfiguration", MethodTypeDesc.of(classDesc())) + codeBuilder.astore(builderConfigurationSlot) if (hasBuilderSessionMethod) { logger.trace { "Registering $methodModel as a cache-compatible method" } @@ -195,20 +198,20 @@ private class PublicInstanceMethodTransform : ClassTransform { // Set return type to "void" because our method won't return JDABuilder, and it doesn't matter anyway val methodType = methodModel.methodTypeSymbol().changeReturnType(CD_void) - // session.theMethod(parameters); - codeBuilder.aload(builderSessionSlot) + // configuration.theMethod(parameters); + codeBuilder.aload(builderConfigurationSlot) methodType.parameterList().forEachIndexed { index, parameter -> val typeKind = TypeKind.fromDescriptor(parameter.descriptorString()) val slot = codeBuilder.parameterSlot(index) codeBuilder.loadLocal(typeKind, slot) } - codeBuilder.invokevirtual(classDesc(), methodName, methodType) + codeBuilder.invokevirtual(classDesc(), methodName, methodType) } else { logger.trace { "Skipping $methodModel as it does not have an equivalent method handler" } - // session.markIncompatible() - codeBuilder.aload(builderSessionSlot) - codeBuilder.invokevirtual(classDesc(), "markIncompatible", MethodTypeDesc.of(CD_void)) + // configuration.markUnsupportedValue() + codeBuilder.aload(builderConfigurationSlot) + codeBuilder.invokevirtual(classDesc(), "markUnsupportedValue", MethodTypeDesc.of(CD_void)) } // Add existing instructions diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 28aa08f..55d309a 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -12,16 +12,60 @@ import java.lang.invoke.* private val logger = KotlinLogging.logger { } // Avoid importing BC and JDA classes +private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") private val CD_JDAImpl = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") +private val CD_JDABuilderSession = classDesc() + +private const val builderSessionFieldName = "builderSession" internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { override fun transform(classData: ByteArray): ByteArray { val classFile = ClassFile.of() - return classFile.transformClass( - classFile.parse(classData), - ShutdownTransform() - ) + val classModel = classFile.parse(classData) + return classFile.build(classModel.thisClass().asSymbol()) { classBuilder -> + classBuilder.withField(builderSessionFieldName, CD_JDABuilderSession, ACC_PRIVATE or ACC_FINAL) + + classBuilder.withMethod("getBuilderSession", MethodTypeDesc.of(CD_JDABuilderSession), ACC_PUBLIC) { methodBuilder -> + methodBuilder.withCode { codeBuilder -> + codeBuilder.aload(codeBuilder.receiverSlot()) + codeBuilder.getfield(CD_JDAImpl, "builderSession", CD_JDABuilderSession) + codeBuilder.areturn() + } + } + + val transform = CaptureSessionKeyTransform() + .andThen(ShutdownTransform()) + classBuilder.transform(classModel, transform) + } + } +} + +private class CaptureSessionKeyTransform : ClassTransform { + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString("")) return classBuilder.retain(classElement) + + // No need to check the signature, we can assign the field in all constructors + + logger.trace { "Transforming (one of) JDAImpl's constructor" } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + // this.builderSession = JDABuilderSession.currentSession() + codeBuilder.aload(thisSlot) + codeBuilder.invokestatic(CD_JDABuilderSession, "currentSession", MethodTypeDesc.of(CD_JDABuilderSession)) + codeBuilder.putfield(CD_JDAImpl, builderSessionFieldName, CD_JDABuilderSession) + + // Add existing instructions + codeModel.forEach { codeBuilder.with(it) } + } + } } } @@ -60,8 +104,8 @@ private class ShutdownTransform : ClassTransform { methodBuilder.withCode { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() - val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) val doShutdownSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) // Runnable doShutdown = this::doShutdown codeBuilder.aload(thisSlot) @@ -100,14 +144,16 @@ private class ShutdownTransform : ClassTransform { )) codeBuilder.astore(doShutdownSlot) - // JDABuilderSession session = JDABuilderSession.currentSession(); - codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) + // var builderSession = getBuilderSession() + codeBuilder.aload(thisSlot) + codeBuilder.invokevirtual(CD_JDAImpl, "getBuilderSession", MethodTypeDesc.of(CD_JDABuilderSession)) codeBuilder.astore(builderSessionSlot) - // session.onShutdown(this::doShutdown); + // builderSession.onShutdown(this, this::doShutdown); codeBuilder.aload(builderSessionSlot) + codeBuilder.aload(thisSlot) codeBuilder.aload(doShutdownSlot) - codeBuilder.invokevirtual(classDesc(), "onShutdown", MethodTypeDesc.of(CD_void, classDesc())) + codeBuilder.invokevirtual(CD_JDABuilderSession, "onShutdown", MethodTypeDesc.of(CD_void, CD_JDA, classDesc())) codeBuilder.return_() } diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt index 425c54a..a3318e8 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt @@ -11,17 +11,17 @@ class JDABuilderTransformerTest { @Test fun `Constructor is instrumented`() { - val builderSession = mockk { + val builderConfiguration = mockk { every { onInit(any(), any()) } just runs - every { markIncompatible() } just runs + every { markUnsupportedValue() } just runs } mockkObject(JDABuilderSession) - every { JDABuilderSession.currentSession() } answers { builderSession } + every { JDABuilderSession.currentSession().configuration } answers { builderConfiguration } JDABuilder.create("MY_TOKEN", setOf(GatewayIntent.GUILD_MEMBERS)) - verify(exactly = 1) { builderSession.onInit("MY_TOKEN", GatewayIntent.getRaw(GatewayIntent.GUILD_MEMBERS)) } + verify(exactly = 1) { builderConfiguration.onInit("MY_TOKEN", GatewayIntent.getRaw(GatewayIntent.GUILD_MEMBERS)) } } @Test @@ -30,17 +30,17 @@ class JDABuilderTransformerTest { val builder = createJDABuilder() // Actual test - val builderSession = mockk { + val builderConfiguration = mockk { every { onInit(any(), any()) } just runs - every { markIncompatible() } just runs + every { markUnsupportedValue() } just runs } mockkObject(JDABuilderSession) - every { JDABuilderSession.currentSession() } returns builderSession + every { JDABuilderSession.currentSession().configuration } returns builderConfiguration builder.setHttpClientBuilder(OkHttpClient.Builder()) - verify(exactly = 1) { builderSession.markIncompatible() } + verify(exactly = 1) { builderConfiguration.markUnsupportedValue() } } @Test @@ -49,25 +49,29 @@ class JDABuilderTransformerTest { val builder = createJDABuilder() // Actual test - val builderSession = mockk { + val builderConfiguration = mockk { every { onInit(any(), any()) } just runs every { setStatus(any()) } just runs } mockkObject(JDABuilderSession) - every { JDABuilderSession.currentSession() } returns builderSession + every { JDABuilderSession.currentSession().configuration } returns builderConfiguration builder.setStatus(OnlineStatus.DO_NOT_DISTURB) - verify(exactly = 1) { builderSession.setStatus(OnlineStatus.DO_NOT_DISTURB) } + verify(exactly = 1) { builderConfiguration.setStatus(OnlineStatus.DO_NOT_DISTURB) } } @Test fun `Build method is instrumented`() { - val builderSession = mockk { + val builderConfiguration = mockk { every { onInit(any(), any()) } just runs - every { markIncompatible() } just runs + every { markUnsupportedValue() } just runs + } + + val builderSession = mockk { every { onBuild(any()) } returns mockk() + every { configuration } returns builderConfiguration } mockkObject(JDABuilderSession) @@ -85,7 +89,7 @@ class JDABuilderTransformerTest { private fun createJDABuilder(): JDABuilder { lateinit var builder: JDABuilder mockkObject(JDABuilderSession) { - every { JDABuilderSession.currentSession() } returns mockk(relaxUnitFun = true) + every { JDABuilderSession.currentSession().configuration } returns mockk(relaxUnitFun = true) builder = JDABuilder.create("MY_TOKEN", emptySet()) } diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt index 98f1182..55a6db6 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt @@ -9,18 +9,17 @@ class JDAImplTransformerTest { @Test fun `Shutdown method is instrumented`() { val builderSession = mockk { - every { onShutdown(any()) } just runs + every { onShutdown(any(), any()) } just runs } - mockkObject(JDABuilderSession) - every { JDABuilderSession.currentSession() } returns builderSession - val jda = mockk { + // If this getter is missing, then the codegen changed + every { this@mockk["getBuilderSession"]() } returns builderSession every { shutdown() } answers { callOriginal() } } jda.shutdown() - verify(exactly = 1) { builderSession.onShutdown(any()) } + verify(exactly = 1) { builderSession.onShutdown(jda, any()) } } } \ No newline at end of file From 1b398f09d270b1cbb99532fe8e64b1b55f76c282 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:40:15 +0200 Subject: [PATCH 27/68] Use multi-dollar interpolation --- .../restart/jda/cache/transformer/JDAServiceTransformer.kt | 4 ++-- .../restart/jda/cache/JDAServiceTransformerTest.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index b9d7161..65a836a 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -22,7 +22,7 @@ private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventMan internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/api/core/JDAService") { - private const val TARGET_METHOD_NAME = "onReadyEvent\$BotCommands" + private const val TARGET_METHOD_NAME = $$"onReadyEvent$BotCommands" private const val TARGET_METHOD_SIGNATURE = "(Lio/github/freya022/botcommands/api/core/events/BReadyEvent;Lnet/dv8tion/jda/api/hooks/IEventManager;)V" override fun transform(classData: ByteArray): ByteArray { @@ -37,7 +37,7 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ // Put the original code of onReadyEvent in the lambda, // it will be fired by JDABuilderSession.withBuilderSession in onReadyEvent - val lambdaName = "lambda\$onReadyEvent\$BotCommands\$withBuilderSession" + val lambdaName = $$"lambda$onReadyEvent$BotCommands$withBuilderSession" classBuilder.withMethodBody( lambdaName, MethodTypeDesc.ofDescriptor(TARGET_METHOD_SIGNATURE), diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt index 65fa185..e0169aa 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt @@ -25,7 +25,7 @@ class JDAServiceTransformerTest { mockkObject(JDABuilderSession) every { JDABuilderSession.withBuilderSession(any(), any()) } answers { callOriginal() } // Will call createJDA - val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) + val onReadyEvent = JDAService::class.java.getDeclaredMethod($$"onReadyEvent$BotCommands", BReadyEvent::class.java, IEventManager::class.java) val bot = mockk { every { createJDA(any(), any()) } just runs every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call withBuilderSession @@ -46,7 +46,7 @@ class JDAServiceTransformerTest { mockkObject(JDABuilderSession) every { JDABuilderSession.withBuilderSession(any(), any()) } answers { callOriginal() } - val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) + val onReadyEvent = JDAService::class.java.getDeclaredMethod($$"onReadyEvent$BotCommands", BReadyEvent::class.java, IEventManager::class.java) val bot = mockk { every { createJDA(any(), any()) } just runs every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call withBuilderSession @@ -68,7 +68,7 @@ class JDAServiceTransformerTest { every { JDABuilderSession.withBuilderSession(any(), any()) } answers { callOriginal() } every { JDABuilderSession.getCacheKey(any()) } answers { callOriginal() } - val onReadyEvent = JDAService::class.java.getDeclaredMethod("onReadyEvent\$BotCommands", BReadyEvent::class.java, IEventManager::class.java) + val onReadyEvent = JDAService::class.java.getDeclaredMethod($$"onReadyEvent$BotCommands", BReadyEvent::class.java, IEventManager::class.java) val bot = mockk { every { createJDA(any(), any()) } just runs every { onReadyEvent.invoke(this@mockk, any(), any()) } answers { callOriginal() } // Will call withBuilderSession From 0ad8e21362a322cde9d2d5088bed7bb92a66e9eb Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:50:33 +0200 Subject: [PATCH 28/68] Consider the method name when matching JDABuilder setter hooks Fixes build() being matched with an unrelated no-arg method --- .../cache/transformer/JDABuilderTransformer.kt | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index c7c3ef4..5372e8f 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -167,10 +167,10 @@ private class BuildTransform : ClassTransform { private class PublicInstanceMethodTransform : ClassTransform { - private val builderSessionMethods: Map = ClassFile.of() + private val builderSessionMethods: Set = ClassFile.of() .parse(JDABuilderConfiguration::class.java.getResourceAsStream("JDABuilderConfiguration.class")!!.readAllBytes()) .methods() - .associateBy { it.methodTypeSymbol() } + .mapTo(hashSetOf(), ::MethodDesc) override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) @@ -182,7 +182,7 @@ private class PublicInstanceMethodTransform : ClassTransform { classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) - val hasBuilderSessionMethod = methodModel.methodTypeSymbol().changeReturnType(CD_void) in builderSessionMethods + val hasBuilderSessionMethod = methodModel.let(::MethodDesc) in builderSessionMethods methodBuilder.withCode { codeBuilder -> val builderConfigurationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) @@ -219,4 +219,15 @@ private class PublicInstanceMethodTransform : ClassTransform { } } } + + // Utility to match methods using their name and parameters, but not return type + private data class MethodDesc( + val name: String, + val paramTypes: List + ) { + constructor(methodModel: MethodModel) : this( + methodModel.methodName().stringValue(), + methodModel.methodTypeSymbol().parameterList(), + ) + } } \ No newline at end of file From 0edc61904a427efd820d7d2e0f8085e2a0d4a861 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 16:26:05 +0200 Subject: [PATCH 29/68] Replace the event manager after JDABuilder#build with ours --- .../jda/cache/BufferingEventManager.kt | 63 +++++++++++++++++++ .../jda/cache/JDABuilderConfiguration.kt | 9 +++ .../restart/jda/cache/JDABuilderSession.kt | 29 +++++---- .../transformer/JDABuilderTransformer.kt | 42 +++++++++++++ .../botcommands/restart/jda/cache/Test.java | 8 +-- .../jda/cache/JDABuilderTransformerTest.kt | 44 +++++++++++++ 6 files changed, 176 insertions(+), 19 deletions(-) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt new file mode 100644 index 0000000..0275ee3 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt @@ -0,0 +1,63 @@ +package dev.freya02.botcommands.restart.jda.cache + +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.hooks.IEventManager +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +internal class BufferingEventManager @DynamicCall constructor( + delegate: IEventManager, +) : IEventManager { + + private val lock = ReentrantLock() + private val eventBuffer: MutableList = arrayListOf() + + private var delegate: IEventManager? = delegate + + internal fun setDelegate(delegate: IEventManager) { + lock.withLock { + check(delegate !is BufferingEventManager) { + "Tried to delegate to a BufferingEventManager!" + } + + this.delegate = delegate + eventBuffer.forEach(::handle) + } + } + + internal fun detach() { + lock.withLock { + delegate = null + } + } + + override fun register(listener: Any) { + lock.withLock { + val delegate = delegate ?: error("Should not happen, implement a listener queue if necessary") + delegate.register(listener) + } + } + + override fun unregister(listener: Any) { + lock.withLock { + val delegate = delegate ?: error("Should not happen, implement a listener queue if necessary") + delegate.unregister(listener) + } + } + + override fun handle(event: GenericEvent) { + lock.withLock { + val delegate = delegate + if (delegate != null) return delegate.handle(event) + + eventBuffer += event + } + } + + override fun getRegisteredListeners(): List { + lock.withLock { + val delegate = delegate ?: error("Should not happen, implement a listener queue if necessary") + return delegate.registeredListeners + } + } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index 432ebcd..6377891 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -1,6 +1,8 @@ package dev.freya02.botcommands.restart.jda.cache import net.dv8tion.jda.api.OnlineStatus +import net.dv8tion.jda.api.hooks.IEventManager +import net.dv8tion.jda.api.hooks.InterfacedEventManager class JDABuilderConfiguration internal constructor() { @@ -8,6 +10,8 @@ class JDABuilderConfiguration internal constructor() { private set private val builderValues: MutableMap = hashMapOf() + private var _eventManager: IEventManager? = null + val eventManager: IEventManager get() = _eventManager ?: InterfacedEventManager() // So we can track the initial token and intents, the constructor will be instrumented and call this method // The user overriding the values using token/intent setters should not be an issue @@ -28,6 +32,11 @@ class JDABuilderConfiguration internal constructor() { builderValues[ValueType.STATUS] = status } + @DynamicCall + fun setEventManager(eventManager: IEventManager) { + _eventManager = eventManager + } + infix fun isSameAs(other: JDABuilderConfiguration): Boolean { // TODO: implement return super.equals(other) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index a082eea..09eb965 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -31,6 +31,9 @@ class JDABuilderSession( @DynamicCall fun onShutdown(instance: JDA, shutdownFunction: Runnable) { + val eventManager = instance.eventManager as? BufferingEventManager + eventManager?.detach() // If the event manager isn't what we expect, it will be logged when attempting to reuse + JDACache[key] = JDACache.Data(configuration, instance, shutdownFunction) } @@ -42,19 +45,16 @@ class JDABuilderSession( } private fun buildOrReuse(buildFunction: Supplier): JDA { - if (configuration.hasUnsupportedValues) { - logger.debug { "Configured JDABuilder has unsupported values, building a new JDA instance (key '$key')" } + fun createNewInstance(): JDA { val jda = buildFunction.get() val oldInstanceData = JDACache.remove(key) oldInstanceData?.doShutdown?.run() - // TODO: Get event manager then wrap it into our special one return jda } - fun createNewInstance(): JDA { - val jda = buildFunction.get() - // TODO: Get event manager then wrap it into our special one - return jda + if (configuration.hasUnsupportedValues) { + logger.debug { "Configured JDABuilder has unsupported values, building a new JDA instance (key '$key')" } + return createNewInstance() } val cachedData = JDACache[key] @@ -65,11 +65,16 @@ class JDABuilderSession( if (cachedData.configuration isSameAs configuration) { logger.debug { "Reusing JDA instance with compatible configuration (key '$key')" } - val jda = JDACache[key]!!.jda - // TODO: Get event manager, cast it to our special one, - // set the delegate to value of JDA#getEventManager, - // then flush the event cache to it - // TODO: Also send start up events again + val jda = cachedData.jda + val eventManager = jda.eventManager as? BufferingEventManager + ?: run { + logger.warn { "Expected a BufferingEventManager but got a ${jda.eventManager.javaClass.name}, creating a new instance" } + cachedData.doShutdown.run() + return createNewInstance() + } + + eventManager.setDelegate(configuration.eventManager) + // TODO: Send start up events again return jda } else { logger.debug { "Creating a new JDA instance as its configuration changed (key '$key')" } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 5372e8f..13d1468 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.BufferingEventManager import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging @@ -16,6 +17,11 @@ private val logger = KotlinLogging.logger { } // Avoid importing BC and JDA classes private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") private val CD_JDABuilder = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") +private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventManager") + +private val CD_BufferingEventManager = classDesc() + +private val CD_IllegalStateException = ClassDesc.of("java.lang.IllegalStateException") internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tion/jda/api/JDABuilder") { @@ -95,8 +101,44 @@ private class BuildTransform : ClassTransform { val codeModel = methodModel.code().get() methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + val bufferingEventManagerSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // JDABuilder's eventManager is null by default, + // however, the framework mandates setting a framework-provided event manager, + // so let's just throw if it is null. + val nullEventManagerLabel = codeBuilder.newLabel() + codeBuilder.aload(thisSlot) + codeBuilder.getfield(CD_JDABuilder, "eventManager", CD_IEventManager) + codeBuilder.ifnull(nullEventManagerLabel) + + // var bufferingEventManager = new BufferingEventManager + codeBuilder.new_(CD_BufferingEventManager) + codeBuilder.astore(bufferingEventManagerSlot) + + // bufferingEventManager.(eventManager) + codeBuilder.aload(bufferingEventManagerSlot) + codeBuilder.aload(thisSlot) + codeBuilder.getfield(CD_JDABuilder, "eventManager", CD_IEventManager) + codeBuilder.invokespecial(CD_BufferingEventManager, "", MethodTypeDesc.of(CD_void, CD_IEventManager)) + + // this.setEventManager(eventManager) + codeBuilder.aload(thisSlot) + codeBuilder.aload(bufferingEventManagerSlot) + codeBuilder.invokevirtual(CD_JDABuilder, "setEventManager", MethodTypeDesc.of(CD_JDABuilder, CD_IEventManager)) + // Move the build() code to doBuild() codeModel.forEach { codeBuilder.with(it) } + + // Branch when "eventManager" is null + codeBuilder.labelBinding(nullEventManagerLabel) + + codeBuilder.new_(CD_IllegalStateException) + codeBuilder.dup() + codeBuilder.ldc("The event manager must be set using the one provided in JDAService#createJDA" as java.lang.String) + codeBuilder.invokespecial(CD_IllegalStateException, "", MethodTypeDesc.of(CD_void, CD_String)) + codeBuilder.athrow() } } diff --git a/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java b/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java index 9ba8eca..ccef89d 100644 --- a/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java +++ b/restarter-jda-cache/src/test/java/dev/freya02/botcommands/restart/jda/cache/Test.java @@ -29,12 +29,6 @@ protected void createJDA(@NotNull BReadyEvent bReadyEvent, @NotNull IEventManage } void something(BReadyEvent bReadyEvent, IEventManager iEventManager) { - var a = "a"; - if (a == null) { - System.out.println("null"); - return; - } - - System.out.println("not null"); + throw new IllegalStateException("test"); } } diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt index a3318e8..53efdf3 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt @@ -1,10 +1,15 @@ package dev.freya02.botcommands.restart.jda.cache import io.mockk.* +import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDABuilder import net.dv8tion.jda.api.OnlineStatus +import net.dv8tion.jda.api.events.GenericEvent +import net.dv8tion.jda.api.hooks.IEventManager import net.dv8tion.jda.api.requests.GatewayIntent import okhttp3.OkHttpClient +import org.junit.jupiter.api.assertThrows +import java.util.function.Supplier import kotlin.test.Test class JDABuilderTransformerTest { @@ -82,6 +87,32 @@ class JDABuilderTransformerTest { verify(exactly = 1) { builderSession.onBuild(any()) } } + @Test + fun `Build sets our event manager`() { + val builderConfiguration = mockk { + every { onInit(any(), any()) } just runs + every { markUnsupportedValue() } just runs + } + + val builderSession = mockk { + every { onBuild(any()) } answers { arg>(0).get() } + every { configuration } returns builderConfiguration + } + + mockkObject(JDABuilderSession) + every { JDABuilderSession.currentSession() } returns builderSession + + val builder = spyk(JDABuilder.createDefault("MY_TOKEN").setEventManager(DummyEventManager)) + + // The special event manager is set on JDABuilder#build() before any original code is run + // so we'll throw an exception on the first method call of the original code, + // which is checkIntents() + every { builder["checkIntents"]() } throws ExpectedException() + assertThrows { builder.build() } + + verify(exactly = 1) { builder.setEventManager(ofType()) } + } + /** * Creates a basic JDABuilder, * call this on the first line to not record any mocking data before doing the actual test. @@ -96,4 +127,17 @@ class JDABuilderTransformerTest { return builder } + + private object DummyEventManager : IEventManager { + + override fun register(listener: Any) {} + + override fun unregister(listener: Any) {} + + override fun handle(event: GenericEvent) {} + + override fun getRegisteredListeners(): List = emptyList() + } + + private class ExpectedException : RuntimeException() } \ No newline at end of file From 0a4ea24df61e848a8d6a8e7d54ce1e779638c322 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:10:53 +0200 Subject: [PATCH 30/68] Implement JDABuilderConfiguration#isSameAs --- .../restart/jda/cache/JDABuilderConfiguration.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index 6377891..a54c675 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -37,14 +37,14 @@ class JDABuilderConfiguration internal constructor() { _eventManager = eventManager } - infix fun isSameAs(other: JDABuilderConfiguration): Boolean { - // TODO: implement - return super.equals(other) + internal infix fun isSameAs(other: JDABuilderConfiguration): Boolean { + return builderValues == other.builderValues } private enum class ValueType { TOKEN, INTENTS, STATUS, + EVENT_PASSTHROUGH, } } \ No newline at end of file From f88156b5ab92d56aec254a77c08576602606a3ce Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:11:07 +0200 Subject: [PATCH 31/68] Support JDABuilder#setEventPassthrough --- .../botcommands/restart/jda/cache/JDABuilderConfiguration.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index a54c675..c7a31db 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -37,6 +37,11 @@ class JDABuilderConfiguration internal constructor() { _eventManager = eventManager } + @DynamicCall + fun setEventPassthrough(enable: Boolean) { + builderValues[ValueType.EVENT_PASSTHROUGH] = enable + } + internal infix fun isSameAs(other: JDABuilderConfiguration): Boolean { return builderValues == other.builderValues } From b4dfc998da7968d49a498c513dca87ccca7f9fe8 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:11:31 +0200 Subject: [PATCH 32/68] Send StatusChangeEvent, GuildReadyEvent, ReadyEvent after reusing an instance --- .../botcommands/restart/jda/cache/JDABuilderSession.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 09eb965..c90c904 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -3,6 +3,9 @@ package dev.freya02.botcommands.restart.jda.cache import io.github.freya022.botcommands.api.core.BContext import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.JDA +import net.dv8tion.jda.api.events.StatusChangeEvent +import net.dv8tion.jda.api.events.guild.GuildReadyEvent +import net.dv8tion.jda.api.events.session.ReadyEvent import java.util.function.Supplier private val logger = KotlinLogging.logger { } @@ -74,7 +77,9 @@ class JDABuilderSession( } eventManager.setDelegate(configuration.eventManager) - // TODO: Send start up events again + eventManager.handle(StatusChangeEvent(jda, JDA.Status.LOADING_SUBSYSTEMS, JDA.Status.CONNECTED)) + jda.guildCache.forEachUnordered { eventManager.handle(GuildReadyEvent(jda, -1, it)) } + eventManager.handle(ReadyEvent(jda)) return jda } else { logger.debug { "Creating a new JDA instance as its configuration changed (key '$key')" } From 1c7fb020acc271ad744e26f9e20d25c50b68faaf Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:15:54 +0200 Subject: [PATCH 33/68] Fix test --- .../restart/jda/cache/JDABuilderTransformerTest.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt index 53efdf3..25ae2de 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt @@ -89,10 +89,7 @@ class JDABuilderTransformerTest { @Test fun `Build sets our event manager`() { - val builderConfiguration = mockk { - every { onInit(any(), any()) } just runs - every { markUnsupportedValue() } just runs - } + val builderConfiguration = mockk(relaxUnitFun = true) val builderSession = mockk { every { onBuild(any()) } answers { arg>(0).get() } From cdf53c35c91b0c99bea553ab27a4c5db1601221f Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 17:16:03 +0200 Subject: [PATCH 34/68] Small changes --- .../freya02/botcommands/restart/jda/cache/JDABuilderSession.kt | 2 +- .../restart/jda/cache/transformer/JDAImplTransformer.kt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index c90c904..dd15074 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -23,7 +23,7 @@ private val logger = KotlinLogging.logger { } // requiring to set up JDA manually, though not complicated, but also docker and that container's config // An hybrid way would require rewriting that proxy, // so our module can hook into JDA and set the gateway URL to the proxy's -class JDABuilderSession( +internal class JDABuilderSession private constructor( private val key: String, ) { diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 55d309a..6a4d5da 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -18,6 +18,7 @@ private val CD_JDABuilderSession = classDesc() private const val builderSessionFieldName = "builderSession" +// TODO add transform on JDAImpl#login(String, ShardInfo, Compression, boolean, int, GatewayEncoding) internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { override fun transform(classData: ByteArray): ByteArray { From 73a277103cc58b32e50375b220b8133447c6b693 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:56:28 +0200 Subject: [PATCH 35/68] Use RestartClassLoader as a specific type of URLClassLoader Contains no other logic, this is only for the purpose of ClassGraph preferring it over other class loaders --- .../internal/restart/RestartClassLoader.kt | 109 +--------------- .../RestartClassLoaderFull.kt.disabled | 116 ++++++++++++++++++ .../botcommands/internal/restart/Restarter.kt | 7 +- 3 files changed, 120 insertions(+), 112 deletions(-) create mode 100644 restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoaderFull.kt.disabled diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt index 605a0b4..a6b5ff9 100644 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt @@ -1,116 +1,9 @@ package dev.freya02.botcommands.internal.restart -import dev.freya02.botcommands.internal.restart.sources.DeletedSourceFile -import dev.freya02.botcommands.internal.restart.sources.SourceDirectories -import dev.freya02.botcommands.internal.restart.sources.SourceFile -import java.io.InputStream import java.net.URL import java.net.URLClassLoader -import java.net.URLConnection -import java.net.URLStreamHandler -import java.util.* internal class RestartClassLoader internal constructor( urls: List, parent: ClassLoader, - private val sourceDirectories: SourceDirectories, -) : URLClassLoader(urls.toTypedArray(), parent) { - - override fun getResources(name: String): Enumeration { - val resources = parent.getResources(name) - val updatedFile = sourceDirectories.getFile(name) - - if (updatedFile != null) { - if (resources.hasMoreElements()) { - resources.nextElement() - } - if (updatedFile is SourceFile) { - return MergedEnumeration(createFileUrl(name, updatedFile), resources) - } - } - - return resources - } - - override fun getResource(name: String): URL? { - val updatedFile = sourceDirectories.getFile(name) - if (updatedFile is DeletedSourceFile) { - return null - } - - return findResource(name) ?: super.getResource(name) - } - - override fun findResource(name: String): URL? { - val updatedFile = sourceDirectories.getFile(name) - ?: return super.findResource(name) - return (updatedFile as? SourceFile)?.let { createFileUrl(name, it) } - } - - override fun loadClass(name: String, resolve: Boolean): Class<*> { - val path = "${name.replace('.', '/')}.class" - val updatedFile = sourceDirectories.getFile(path) - if (updatedFile is DeletedSourceFile) - throw ClassNotFoundException(name) - - return synchronized(getClassLoadingLock(name)) { - val loadedClass = findLoadedClass(name) ?: try { - findClass(name) - } catch (_: ClassNotFoundException) { - Class.forName(name, false, parent) - } - if (resolve) resolveClass(loadedClass) - loadedClass - } - } - - override fun findClass(name: String): Class<*> { - val path = "${name.replace('.', '/')}.class" - val updatedFile = sourceDirectories.getFile(path) - ?: return super.findClass(name) - if (updatedFile is DeletedSourceFile) - throw ClassNotFoundException(name) - - updatedFile as SourceFile - return defineClass(name, updatedFile.bytes, 0, updatedFile.bytes.size) - } - - @Suppress("DEPRECATION") // We target Java 17 but JDK 20 deprecates the URL constructors - private fun createFileUrl(name: String, file: SourceFile): URL { - return URL("reloaded", null, -1, "/$name", ClasspathFileURLStreamHandler(file)) - } - - private class ClasspathFileURLStreamHandler( - private val file: SourceFile, - ) : URLStreamHandler() { - - override fun openConnection(u: URL): URLConnection = Connection(u) - - private inner class Connection(url: URL): URLConnection(url) { - - override fun connect() {} - - override fun getInputStream(): InputStream = file.bytes.inputStream() - - override fun getLastModified(): Long = file.lastModified.toEpochMilli() - - override fun getContentLengthLong(): Long = file.bytes.size.toLong() - } - } - - private class MergedEnumeration(private val first: E, private val rest: Enumeration) : Enumeration { - - private var hasConsumedFirst = false - - override fun hasMoreElements(): Boolean = !hasConsumedFirst || rest.hasMoreElements() - - override fun nextElement(): E? { - if (!hasConsumedFirst) { - hasConsumedFirst = true - return first - } else { - return rest.nextElement() - } - } - } -} \ No newline at end of file +) : URLClassLoader(urls.toTypedArray(), parent) \ No newline at end of file diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoaderFull.kt.disabled b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoaderFull.kt.disabled new file mode 100644 index 0000000..605a0b4 --- /dev/null +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoaderFull.kt.disabled @@ -0,0 +1,116 @@ +package dev.freya02.botcommands.internal.restart + +import dev.freya02.botcommands.internal.restart.sources.DeletedSourceFile +import dev.freya02.botcommands.internal.restart.sources.SourceDirectories +import dev.freya02.botcommands.internal.restart.sources.SourceFile +import java.io.InputStream +import java.net.URL +import java.net.URLClassLoader +import java.net.URLConnection +import java.net.URLStreamHandler +import java.util.* + +internal class RestartClassLoader internal constructor( + urls: List, + parent: ClassLoader, + private val sourceDirectories: SourceDirectories, +) : URLClassLoader(urls.toTypedArray(), parent) { + + override fun getResources(name: String): Enumeration { + val resources = parent.getResources(name) + val updatedFile = sourceDirectories.getFile(name) + + if (updatedFile != null) { + if (resources.hasMoreElements()) { + resources.nextElement() + } + if (updatedFile is SourceFile) { + return MergedEnumeration(createFileUrl(name, updatedFile), resources) + } + } + + return resources + } + + override fun getResource(name: String): URL? { + val updatedFile = sourceDirectories.getFile(name) + if (updatedFile is DeletedSourceFile) { + return null + } + + return findResource(name) ?: super.getResource(name) + } + + override fun findResource(name: String): URL? { + val updatedFile = sourceDirectories.getFile(name) + ?: return super.findResource(name) + return (updatedFile as? SourceFile)?.let { createFileUrl(name, it) } + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + val path = "${name.replace('.', '/')}.class" + val updatedFile = sourceDirectories.getFile(path) + if (updatedFile is DeletedSourceFile) + throw ClassNotFoundException(name) + + return synchronized(getClassLoadingLock(name)) { + val loadedClass = findLoadedClass(name) ?: try { + findClass(name) + } catch (_: ClassNotFoundException) { + Class.forName(name, false, parent) + } + if (resolve) resolveClass(loadedClass) + loadedClass + } + } + + override fun findClass(name: String): Class<*> { + val path = "${name.replace('.', '/')}.class" + val updatedFile = sourceDirectories.getFile(path) + ?: return super.findClass(name) + if (updatedFile is DeletedSourceFile) + throw ClassNotFoundException(name) + + updatedFile as SourceFile + return defineClass(name, updatedFile.bytes, 0, updatedFile.bytes.size) + } + + @Suppress("DEPRECATION") // We target Java 17 but JDK 20 deprecates the URL constructors + private fun createFileUrl(name: String, file: SourceFile): URL { + return URL("reloaded", null, -1, "/$name", ClasspathFileURLStreamHandler(file)) + } + + private class ClasspathFileURLStreamHandler( + private val file: SourceFile, + ) : URLStreamHandler() { + + override fun openConnection(u: URL): URLConnection = Connection(u) + + private inner class Connection(url: URL): URLConnection(url) { + + override fun connect() {} + + override fun getInputStream(): InputStream = file.bytes.inputStream() + + override fun getLastModified(): Long = file.lastModified.toEpochMilli() + + override fun getContentLengthLong(): Long = file.bytes.size.toLong() + } + } + + private class MergedEnumeration(private val first: E, private val rest: Enumeration) : Enumeration { + + private var hasConsumedFirst = false + + override fun hasMoreElements(): Boolean = !hasConsumedFirst || rest.hasMoreElements() + + override fun nextElement(): E? { + if (!hasConsumedFirst) { + hasConsumedFirst = true + return first + } else { + return rest.nextElement() + } + } + } +} \ No newline at end of file diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt index fe5f684..bcdaa5b 100644 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/Restarter.kt @@ -3,7 +3,6 @@ package dev.freya02.botcommands.internal.restart import dev.freya02.botcommands.internal.restart.utils.AppClasspath import io.github.oshai.kotlinlogging.KotlinLogging import java.net.URL -import java.net.URLClassLoader import java.util.concurrent.locks.Lock import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.thread @@ -78,16 +77,16 @@ class Restarter private constructor( * Starts a new instance of the main class, or returns a [Throwable] if it failed. */ private fun start(): Throwable? { - // We use a regular URLClassLoader instead of [[RestartClassLoader]], + // We use a regular URLClassLoader instead of RestartClassLoaderFull, // as classpath changes will trigger a restart and thus recreate a new ClassLoader, // meaning live updating the classes is pointless. // In contrast, Spring needs their RestartClassLoader because it can override classes remotely, // but we don't have such a use case. - // However, not using [[RestartClassLoader]], which uses snapshots, has an issue, + // However, not using RestartClassLoaderFull, which uses snapshots, has an issue, // trying to load deleted classes (most likely on shutdown) will fail, // Spring also has that issue, but it will only happen on classes out of its component scan, // BC just needs to make sure to at least load the classes on its path too. - val restartClassLoader = URLClassLoader(appClasspathUrls.toTypedArray(), appClassLoader) + val restartClassLoader = RestartClassLoader(appClasspathUrls, appClassLoader) var error: Throwable? = null val launchThreads = thread(name = RESTARTED_THREAD_NAME, isDaemon = false, contextClassLoader = restartClassLoader) { try { From dd1e6ef0f82f311597919bc01796151530dfa958 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:58:08 +0200 Subject: [PATCH 36/68] Log unsupported JDABuilder methods --- .../restart/jda/cache/JDABuilderConfiguration.kt | 10 ++++++++-- .../jda/cache/transformer/JDABuilderTransformer.kt | 5 ++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index c7a31db..0513b96 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -1,11 +1,16 @@ package dev.freya02.botcommands.restart.jda.cache +import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.OnlineStatus import net.dv8tion.jda.api.hooks.IEventManager import net.dv8tion.jda.api.hooks.InterfacedEventManager +private val logger = KotlinLogging.logger { } + class JDABuilderConfiguration internal constructor() { + private val warnedUnsupportedValues: MutableSet = hashSetOf() + var hasUnsupportedValues = false private set @@ -22,8 +27,9 @@ class JDABuilderConfiguration internal constructor() { } @DynamicCall - fun markUnsupportedValue() { - // TODO log which method is incompatible, pass the method name using codegen + fun markUnsupportedValue(signature: String) { + if (warnedUnsupportedValues.add(signature)) + logger.warn { "Unsupported JDABuilder method '$signature', JDA will not be cached between restarts" } hasUnsupportedValues = true } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 13d1468..e00b597 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -251,9 +251,12 @@ private class PublicInstanceMethodTransform : ClassTransform { } else { logger.trace { "Skipping $methodModel as it does not have an equivalent method handler" } + val signature = methodModel.methodName().stringValue() + "(${methodModel.methodTypeSymbol().parameterList().joinToString { it.displayName() }})" + // configuration.markUnsupportedValue() codeBuilder.aload(builderConfigurationSlot) - codeBuilder.invokevirtual(classDesc(), "markUnsupportedValue", MethodTypeDesc.of(CD_void)) + codeBuilder.ldc(signature as java.lang.String) + codeBuilder.invokevirtual(classDesc(), "markUnsupportedValue", MethodTypeDesc.of(CD_void, CD_String)) } // Add existing instructions From 99bfd76c9ebf12456ff49c45e14ee6dd945dc9b1 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:58:36 +0200 Subject: [PATCH 37/68] Ignore JDABuilder#build() --- .../restart/jda/cache/transformer/JDABuilderTransformer.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index e00b597..5bedb43 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -218,6 +218,7 @@ private class PublicInstanceMethodTransform : ClassTransform { val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) if (!methodModel.flags().has(AccessFlag.PUBLIC)) return classBuilder.retain(classElement) if (methodModel.flags().has(AccessFlag.STATIC)) return classBuilder.retain(classElement) + if (methodModel.methodName().stringValue() == "build") return classBuilder.retain(classElement) logger.trace { "Transforming ${methodModel.methodName().stringValue()}" } From d55e004861be401197877696bcf9dfd077c36c0d Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:59:40 +0200 Subject: [PATCH 38/68] Immediately return in JDA#awaitShutdown As they will not be shut down only if the instance needs to be discarded --- .../cache/transformer/JDAImplTransformer.kt | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 6a4d5da..97f97c7 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -37,6 +37,7 @@ internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/j val transform = CaptureSessionKeyTransform() .andThen(ShutdownTransform()) + .andThen(AwaitShutdownTransform()) classBuilder.transform(classModel, transform) } } @@ -160,4 +161,23 @@ private class ShutdownTransform : ClassTransform { } } } +} + +private class AwaitShutdownTransform : ClassTransform { + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString("awaitShutdown")) return classBuilder.retain(classElement) + + logger.trace { "Transforming (one of) JDA's awaitShutdown method" } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + codeBuilder.iconst_0() + codeBuilder.ireturn() + } + } + } } \ No newline at end of file From 0cb83b8847eb91fff1ff5616d9fcf13c65c3daa6 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:59:55 +0200 Subject: [PATCH 39/68] Support more JDABuilder methods --- .../jda/cache/JDABuilderConfiguration.kt | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index 0513b96..6269cb3 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -1,9 +1,16 @@ package dev.freya02.botcommands.restart.jda.cache +import io.github.freya022.botcommands.api.core.utils.enumSetOf +import io.github.freya022.botcommands.api.core.utils.enumSetOfAll import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.OnlineStatus +import net.dv8tion.jda.api.entities.Activity import net.dv8tion.jda.api.hooks.IEventManager import net.dv8tion.jda.api.hooks.InterfacedEventManager +import net.dv8tion.jda.api.utils.ChunkingFilter +import net.dv8tion.jda.api.utils.MemberCachePolicy +import net.dv8tion.jda.api.utils.cache.CacheFlag +import java.util.* private val logger = KotlinLogging.logger { } @@ -24,6 +31,7 @@ class JDABuilderConfiguration internal constructor() { fun onInit(token: String?, intents: Int) { builderValues[ValueType.TOKEN] = token builderValues[ValueType.INTENTS] = intents + builderValues[ValueType.CACHE_FLAGS] = enumSetOfAll() } @DynamicCall @@ -39,7 +47,7 @@ class JDABuilderConfiguration internal constructor() { } @DynamicCall - fun setEventManager(eventManager: IEventManager) { + fun setEventManager(eventManager: IEventManager?) { _eventManager = eventManager } @@ -48,6 +56,50 @@ class JDABuilderConfiguration internal constructor() { builderValues[ValueType.EVENT_PASSTHROUGH] = enable } + @DynamicCall + @Suppress("UNCHECKED_CAST") + fun enableCache(first: CacheFlag, vararg others: CacheFlag) { + (builderValues[ValueType.CACHE_FLAGS] as EnumSet) += enumSetOf(first, *others) + } + + @DynamicCall + @Suppress("UNCHECKED_CAST") + fun enableCache(flags: Collection) { + (builderValues[ValueType.CACHE_FLAGS] as EnumSet) += flags + } + + @DynamicCall + @Suppress("UNCHECKED_CAST") + fun disableCache(first: CacheFlag, vararg others: CacheFlag) { + (builderValues[ValueType.CACHE_FLAGS] as EnumSet) -= enumSetOf(first, *others) + } + + @DynamicCall + @Suppress("UNCHECKED_CAST") + fun disableCache(flags: Collection) { + (builderValues[ValueType.CACHE_FLAGS] as EnumSet) -= flags + } + + @DynamicCall + fun setMemberCachePolicy(memberCachePolicy: MemberCachePolicy?) { + builderValues[ValueType.MEMBER_CACHE_POLICY] = memberCachePolicy + } + + @DynamicCall + fun setChunkingFilter(filter: ChunkingFilter?) { + builderValues[ValueType.CHUNKING_FILTER] = filter + } + + @DynamicCall + fun setLargeThreshold(threshold: Int) { + builderValues[ValueType.LARGE_THRESHOLD] = threshold + } + + @DynamicCall + fun setActivity(activity: Activity?) { + builderValues[ValueType.ACTIVITY] = activity + } + internal infix fun isSameAs(other: JDABuilderConfiguration): Boolean { return builderValues == other.builderValues } @@ -57,5 +109,13 @@ class JDABuilderConfiguration internal constructor() { INTENTS, STATUS, EVENT_PASSTHROUGH, + CACHE_FLAGS, + // These two are interfaces, it's fine to compare them by equality, + // their reference will be the same as they are from the app class loader, + // so if two runs uses MemberCachePolicy#VOICE, it'll still be compatible + MEMBER_CACHE_POLICY, + CHUNKING_FILTER, + LARGE_THRESHOLD, + ACTIVITY } } \ No newline at end of file From 7c6c6752b9415f1ed702c1dc627654242bef1930 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 5 Jun 2025 22:00:07 +0200 Subject: [PATCH 40/68] Set artifact IDs for publishing --- restarter-jda-cache/build.gradle.kts | 2 ++ restarter/build.gradle.kts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts index 6aacec1..3531b50 100644 --- a/restarter-jda-cache/build.gradle.kts +++ b/restarter-jda-cache/build.gradle.kts @@ -49,6 +49,8 @@ publishing { publications { create("maven") { from(components["java"]) + + artifactId = "BotCommands-Restarter-JDA-Cache" } } } \ No newline at end of file diff --git a/restarter/build.gradle.kts b/restarter/build.gradle.kts index 09a0eae..6a31a5d 100644 --- a/restarter/build.gradle.kts +++ b/restarter/build.gradle.kts @@ -15,6 +15,8 @@ publishing { publications { create("maven") { from(components["java"]) + + artifactId = "BotCommands-Restarter" } } } \ No newline at end of file From 336c2e603527fad732f14cef254af7ac14f4a4b0 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:15:03 +0200 Subject: [PATCH 41/68] JDA cache: Remove dependency on base restarter Will allow to use this even with Spring Dev Tools --- restarter-jda-cache/build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts index 3531b50..48a26e8 100644 --- a/restarter-jda-cache/build.gradle.kts +++ b/restarter-jda-cache/build.gradle.kts @@ -10,7 +10,6 @@ repositories { } dependencies { - implementation(projects.restarter) implementation(libs.kotlin.logging) implementation(libs.botcommands) From 6052336da2f616125b3887aa8e49b36f451a489e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:15:47 +0200 Subject: [PATCH 42/68] Restarter: Remove RestarterClassGraphConfigurer Built in ClassGraph --- .../services/RestarterClassGraphConfigurer.kt | 17 ----------------- ...tcommands.internal.core.ClassGraphConfigurer | 1 - 2 files changed, 18 deletions(-) delete mode 100644 restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt delete mode 100644 restarter/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt deleted file mode 100644 index f285807..0000000 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterClassGraphConfigurer.kt +++ /dev/null @@ -1,17 +0,0 @@ -package dev.freya02.botcommands.internal.restart.services - -import dev.freya02.botcommands.internal.restart.Restarter -import io.github.classgraph.ClassGraph -import io.github.freya022.botcommands.internal.core.ClassGraphConfigurer - -internal class RestarterClassGraphConfigurer : ClassGraphConfigurer { - - override fun ClassGraph.configure(arguments: ClassGraphConfigurer.Arguments) { - val thread = Thread.currentThread() - val classLoader = thread.contextClassLoader - if (thread.name != Restarter.RESTARTED_THREAD_NAME) return - - // [[Restarter]] will read from the mutable classes first then delegate to the app class loader (immutable) - overrideClassLoaders(classLoader) - } -} \ No newline at end of file diff --git a/restarter/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer b/restarter/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer deleted file mode 100644 index cfe6781..0000000 --- a/restarter/src/main/resources/META-INF/services/io.github.freya022.botcommands.internal.core.ClassGraphConfigurer +++ /dev/null @@ -1 +0,0 @@ -dev.freya02.botcommands.internal.restart.services.RestarterClassGraphConfigurer \ No newline at end of file From ceea55d60e603c991fde385c3d07042e2269766a Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:14:29 +0200 Subject: [PATCH 43/68] Restarter: Shutdown immediately, await using configured timeout --- .../botcommands/internal/restart/services/RestarterService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt index 2ab58f5..dc5626d 100644 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt @@ -21,8 +21,8 @@ internal class RestarterService internal constructor ( init { Restarter.instance.addListener(object : RestartListener { override fun beforeStop() { - context.shutdown() - context.awaitShutdown() + context.shutdownNow() + context.awaitShutdown(context.config.shutdownTimeout) } }) } From e81f9d0d2dc7147cebfe2b90defea6af0b5a24b3 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:43:02 +0200 Subject: [PATCH 44/68] JDA cache: Immediately run JDA#shutdown if the JVM is shutting down --- .../botcommands/restart/jda/cache/JDABuilderSession.kt | 5 +++++ .../dev/freya02/botcommands/restart/jda/cache/utils.kt | 10 ++++++++++ 2 files changed, 15 insertions(+) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index dd15074..8d15263 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -34,6 +34,11 @@ internal class JDABuilderSession private constructor( @DynamicCall fun onShutdown(instance: JDA, shutdownFunction: Runnable) { + if (isJvmShuttingDown()) { + shutdownFunction.run() + return + } + val eventManager = instance.eventManager as? BufferingEventManager eventManager?.detach() // If the event manager isn't what we expect, it will be logged when attempting to reuse diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt new file mode 100644 index 0000000..08640b6 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt @@ -0,0 +1,10 @@ +package dev.freya02.botcommands.restart.jda.cache + +internal fun isJvmShuttingDown() = try { + Runtime.getRuntime().removeShutdownHook(NullShutdownHook) + false +} catch (_: IllegalStateException) { + true +} + +private object NullShutdownHook : Thread() \ No newline at end of file From 24c08e28f0e76d76570549af2900074ed0bb2108 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 16:44:59 +0200 Subject: [PATCH 45/68] JDA cache: Redirect JDA#shutdownNow to JDABuilderSession#onShutdown --- .../restart/jda/cache/JDABuilderSession.kt | 1 + .../cache/transformer/JDAImplTransformer.kt | 95 ++++++++++++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 8d15263..251865c 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -32,6 +32,7 @@ internal class JDABuilderSession private constructor( var wasBuilt: Boolean = false private set + // May also be shutdownNow @DynamicCall fun onShutdown(instance: JDA, shutdownFunction: Runnable) { if (isJvmShuttingDown()) { diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 97f97c7..0d56b24 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -37,6 +37,7 @@ internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/j val transform = CaptureSessionKeyTransform() .andThen(ShutdownTransform()) + .andThen(ShutdownNowTransform()) .andThen(AwaitShutdownTransform()) classBuilder.transform(classModel, transform) } @@ -84,7 +85,7 @@ private class ShutdownTransform : ClassTransform { throw IllegalArgumentException("Incompatible JDAImpl shutdown method: $methodType") } - logger.trace { "Transforming JDABuilder's build() method" } + logger.trace { "Transforming JDABuilder's shutdown() method" } val newShutdownMethodName = "doShutdown" classBuilder.withMethod( @@ -163,6 +164,98 @@ private class ShutdownTransform : ClassTransform { } } +private class ShutdownNowTransform : ClassTransform { + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.methodName().equalsString("shutdownNow")) return classBuilder.retain(classElement) + + val methodType = methodModel.methodTypeSymbol() + if (methodType.parameterList() != emptyList()) { + // TODO not sure about the exception model yet, + // maybe we should just disable the JDA cache instead of being completely incompatible + throw IllegalArgumentException("Incompatible JDAImpl shutdownNow method: $methodType") + } + + logger.trace { "Transforming JDABuilder's shutdownNow() method" } + + val newShutdownMethodName = "doShutdownNow" + classBuilder.withMethod( + newShutdownMethodName, + MethodTypeDesc.of(CD_void), + ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL + ) { methodBuilder -> + val codeModel = methodModel.code().get() + + methodBuilder.withCode { codeBuilder -> + // Move the shutdownNow() code to doShutdownNow() + codeModel.forEach { codeBuilder.with(it) } + } + } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + val doShutdownNowSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // Runnable doShutdownNow = this::doShutdownNow + codeBuilder.aload(thisSlot) + codeBuilder.invokedynamic(DynamicCallSiteDesc.of( + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + classDesc(), + "metafactory", + MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) + ), + // The following parameters are from [[LambdaMetafactory#metafactory]] + // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", + // the method name in Supplier is "get" + "run", + // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", + // the return type is the implemented interface, + // while the parameters are the captured variables (incl. receiver) + MethodTypeDesc.of(classDesc(), CD_JDAImpl), + // Bootstrap arguments (see `javap -c -v ` from a working .java sample) + // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", + // which is the signature of the implemented method, in this case, void run() + MethodTypeDesc.of(CD_void), + // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", + // this is the method to be called when invoking the lambda, + // with the captured variables and parameters + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + CD_JDAImpl, + newShutdownMethodName, + MethodTypeDesc.of(CD_void) + ), + // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", + // this is "the signature and return type to be enforced dynamically at invocation type" + // This is usually the same as "interfaceMethodType" + MethodTypeDesc.of(CD_void), + )) + codeBuilder.astore(doShutdownNowSlot) + + // var builderSession = getBuilderSession() + codeBuilder.aload(thisSlot) + codeBuilder.invokevirtual(CD_JDAImpl, "getBuilderSession", MethodTypeDesc.of(CD_JDABuilderSession)) + codeBuilder.astore(builderSessionSlot) + + // builderSession.onShutdown(this, this::doShutdownNow); + codeBuilder.aload(builderSessionSlot) + codeBuilder.aload(thisSlot) + codeBuilder.aload(doShutdownNowSlot) + codeBuilder.invokevirtual(CD_JDABuilderSession, "onShutdown", MethodTypeDesc.of(CD_void, CD_JDA, classDesc())) + + codeBuilder.return_() + } + } + } +} + private class AwaitShutdownTransform : ClassTransform { override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { From 9f2c3cbb890fd1b14e79060e7f99bf4f78a2c908 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:47:53 +0200 Subject: [PATCH 46/68] Restarter: Replace SourceDirectories + ClasspathListener with ClasspathWatcher As a lighter alternative, since we don't need the updated data --- .../restart/services/RestarterService.kt | 18 +- .../restart/watcher/ClasspathWatcher.kt | 182 ++++++++++++++++++ 2 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathWatcher.kt diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt index dc5626d..61572b2 100644 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/services/RestarterService.kt @@ -2,13 +2,9 @@ package dev.freya02.botcommands.internal.restart.services import dev.freya02.botcommands.internal.restart.RestartListener import dev.freya02.botcommands.internal.restart.Restarter -import dev.freya02.botcommands.internal.restart.sources.SourceDirectories -import dev.freya02.botcommands.internal.restart.utils.AppClasspath -import dev.freya02.botcommands.internal.restart.watcher.ClasspathListener +import dev.freya02.botcommands.internal.restart.watcher.ClasspathWatcher import io.github.freya022.botcommands.api.core.BContext -import io.github.freya022.botcommands.api.core.annotations.BEventListener import io.github.freya022.botcommands.api.core.config.BRestartConfig -import io.github.freya022.botcommands.api.core.events.BShutdownEvent import io.github.freya022.botcommands.api.core.service.annotations.BService import io.github.freya022.botcommands.api.core.service.annotations.RequiresDefaultInjection @@ -16,6 +12,7 @@ import io.github.freya022.botcommands.api.core.service.annotations.RequiresDefau @RequiresDefaultInjection internal class RestarterService internal constructor ( context: BContext, + config: BRestartConfig, ) { init { @@ -25,15 +22,6 @@ internal class RestarterService internal constructor ( context.awaitShutdown(context.config.shutdownTimeout) } }) - } - - @BService - internal fun sourceDirectories(config: BRestartConfig): SourceDirectories { - return SourceDirectories(AppClasspath.getPaths(), ClasspathListener(config.restartDelay)) - } - - @BEventListener(priority = Int.MAX_VALUE) - internal fun onShutdown(event: BShutdownEvent, sourceDirectories: SourceDirectories) { - sourceDirectories.close() + ClasspathWatcher.initialize(config.restartDelay) } } \ No newline at end of file diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathWatcher.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathWatcher.kt new file mode 100644 index 0000000..7184116 --- /dev/null +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/watcher/ClasspathWatcher.kt @@ -0,0 +1,182 @@ +package dev.freya02.botcommands.internal.restart.watcher + +import dev.freya02.botcommands.internal.restart.Restarter +import dev.freya02.botcommands.internal.restart.sources.DeletedSourceFile +import dev.freya02.botcommands.internal.restart.sources.SourceFile +import dev.freya02.botcommands.internal.restart.sources.SourceFiles +import dev.freya02.botcommands.internal.restart.sources.plus +import dev.freya02.botcommands.internal.restart.utils.AppClasspath +import dev.freya02.botcommands.internal.restart.utils.walkDirectories +import dev.freya02.botcommands.internal.restart.utils.walkFiles +import io.github.freya022.botcommands.api.core.utils.joinAsList +import io.github.oshai.kotlinlogging.KotlinLogging +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.thread +import kotlin.concurrent.withLock +import kotlin.io.path.absolutePathString +import kotlin.io.path.isDirectory +import kotlin.io.path.pathString +import kotlin.io.path.relativeTo +import kotlin.time.Duration + +private val logger = KotlinLogging.logger { } + +// Lightweight, singleton version of [[SourceDirectories]] + [[ClasspathListener]] +internal class ClasspathWatcher private constructor( + private var settings: Settings?, // null = no instance registered = no restart can be scheduled +) { + + private val scheduler = Executors.newSingleThreadScheduledExecutor() + private lateinit var restartFuture: ScheduledFuture<*> + + private val watchService = FileSystems.getDefault().newWatchService() + private val registeredDirectories: MutableSet = ConcurrentHashMap.newKeySet() + private val snapshots: MutableMap = hashMapOf() + + init { + AppClasspath.getPaths().forEach { classRoot -> + require(classRoot.isDirectory()) + + logger.trace { "Creating snapshot of ${classRoot.absolutePathString()}" } + snapshots[classRoot] = classRoot.takeSnapshot() + + logger.trace { "Listening to ${classRoot.absolutePathString()}" } + registerDirectories(classRoot) + } + + thread(name = "Classpath watcher", isDaemon = true) { + while (true) { + val key = try { + watchService.take() // Wait for a change + } catch (_: InterruptedException) { + return@thread logger.trace { "Interrupted watching classpath" } + } + val pollEvents = key.pollEvents() + if (pollEvents.isNotEmpty()) { + logger.trace { + val affectedList = pollEvents.joinAsList { "${it.kind()}: ${it.context()}" } + "Affected files:\n$affectedList" + } + } else { + // Seems to be empty when a directory gets deleted + // The next watch key *should* be an ENTRY_DELETE of that directory + continue + } + if (!key.reset()) { + logger.warn { "${key.watchable()} is no longer valid" } + continue + } + + scheduleRestart() + } + } + } + + private fun scheduleRestart() { + val settings = settings ?: return // Don't schedule a restart until an instance has registered + if (::restartFuture.isInitialized) restartFuture.cancel(false) + restartFuture = scheduler.schedule(::tryRestart, settings.restartDelay.inWholeMilliseconds, TimeUnit.MILLISECONDS) + } + + private fun tryRestart() { + // Can't set to null after restarting, + // as the restart function only returns after the main method ran + val settings = settings ?: return + try { + logger.debug { "Attempting to restart" } + this.settings = null // Wait until the next instance has given its settings + compareSnapshots() + snapshots.keys.forEach { registerDirectories(it) } + Restarter.instance.restart() + } catch (e: Exception) { + logger.error(e) { "Restart failed, waiting for the next build" } + this.settings = settings // Reuse the old settings to reschedule a new restart + } + } + + private fun compareSnapshots() { + val hasChanges = snapshots.any { (directory, files) -> + val snapshot = directory.takeSnapshot() + + // Exclude deleted files so they don't count as being deleted again + val deletedPaths = files.withoutDeletes().keys - snapshot.keys + if (deletedPaths.isNotEmpty()) { + logger.info { "${deletedPaths.size} files were deleted in ${directory.absolutePathString()}: $deletedPaths" } + snapshots[directory] = deletedPaths.associateWith { DeletedSourceFile } + snapshot + // So we can re-register them in case they are recreated + registeredDirectories.removeAll(deletedPaths.map { directory.resolve(it) }) + return@any true + } + + // Exclude deleted files so they count as being added back + val addedPaths = snapshot.keys - files.withoutDeletes().keys + if (addedPaths.isNotEmpty()) { + logger.info { "${addedPaths.size} files were added in ${directory.absolutePathString()}: $addedPaths" } + snapshots[directory] = files + snapshot + return@any true + } + + val modifiedFiles = snapshot.keys.filter { key -> + val actual = snapshot[key] ?: error("Key from map is missing a value somehow") + val expected = files[key] ?: error("Expected file is missing, should have been detected as deleted") + + // File was deleted (on the 2nd build for example) and got recreated (on the 3rd build for example) + if (expected is DeletedSourceFile) error("Expected file was registered as deleted, should have been detected as added") + expected as SourceFile + + actual as SourceFile // Assertion + + actual.lastModified != expected.lastModified + } + if (modifiedFiles.isNotEmpty()) { + logger.info { "${modifiedFiles.size} files were modified in ${directory.absolutePathString()}: $modifiedFiles" } + snapshots[directory] = files + snapshot + return@any true + } + + false + } + + if (!hasChanges) + error("Received a file system event but no changes were detected") + } + + private fun registerDirectories(directory: Path) { + directory.walkDirectories { path, attributes -> + if (registeredDirectories.add(path)) + path.register(watchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE) + } + } + + private class Settings( + val restartDelay: Duration, + ) + + internal companion object { + private val instanceLock = ReentrantLock() + internal lateinit var instance: ClasspathWatcher + private set + + internal fun initialize(restartDelay: Duration) { + instanceLock.withLock { + val settings = Settings(restartDelay) + if (::instance.isInitialized.not()) { + instance = ClasspathWatcher(settings) + } else { + instance.settings = settings + } + } + } + } +} + +private fun Path.takeSnapshot(): SourceFiles = walkFiles().associate { (it, attrs) -> + it.relativeTo(this).pathString to SourceFile(attrs.lastModifiedTime().toInstant()) +}.let(::SourceFiles) \ No newline at end of file From b01e9719878264ba66ce1d8b49eec78b1239ef75 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:48:33 +0200 Subject: [PATCH 47/68] JDA cache: fix test --- .../jda/cache/JDABuilderTransformerTest.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt index 25ae2de..3329192 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt @@ -16,10 +16,7 @@ class JDABuilderTransformerTest { @Test fun `Constructor is instrumented`() { - val builderConfiguration = mockk { - every { onInit(any(), any()) } just runs - every { markUnsupportedValue() } just runs - } + val builderConfiguration = mockk(relaxUnitFun = true) mockkObject(JDABuilderSession) every { JDABuilderSession.currentSession().configuration } answers { builderConfiguration } @@ -37,7 +34,7 @@ class JDABuilderTransformerTest { // Actual test val builderConfiguration = mockk { every { onInit(any(), any()) } just runs - every { markUnsupportedValue() } just runs + every { markUnsupportedValue(any()) } just runs } mockkObject(JDABuilderSession) @@ -45,7 +42,7 @@ class JDABuilderTransformerTest { builder.setHttpClientBuilder(OkHttpClient.Builder()) - verify(exactly = 1) { builderConfiguration.markUnsupportedValue() } + verify(exactly = 1) { builderConfiguration.markUnsupportedValue(any()) } } @Test @@ -69,14 +66,9 @@ class JDABuilderTransformerTest { @Test fun `Build method is instrumented`() { - val builderConfiguration = mockk { - every { onInit(any(), any()) } just runs - every { markUnsupportedValue() } just runs - } - val builderSession = mockk { every { onBuild(any()) } returns mockk() - every { configuration } returns builderConfiguration + every { configuration } returns mockk(relaxUnitFun = true) } mockkObject(JDABuilderSession) From 73917d115dcddc5b3ec5aac9e3ab4fa4a1721813 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:55:04 +0200 Subject: [PATCH 48/68] JDA cache: Store cache key in JDAImpl instead of the full instance Fixes issues where an old instance would be used on shutdown --- .../restart/jda/cache/JDABuilderSession.kt | 9 +++++++++ .../jda/cache/transformer/JDAImplTransformer.kt | 12 +++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 251865c..3c960c0 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -98,10 +98,18 @@ internal class JDABuilderSession private constructor( private val activeSession: ThreadLocal = ThreadLocal.withInitial { error("No JDABuilderSession exists for this thread") } + private val sessions: MutableMap = hashMapOf() + @JvmStatic @DynamicCall fun currentSession(): JDABuilderSession = activeSession.get() + @JvmStatic + @DynamicCall + fun getSession(key: String): JDABuilderSession { + return sessions[key] ?: error("No JDABuilderSession exists for key '$key'") + } + @JvmStatic @DynamicCall fun getCacheKey(context: BContext): String? = context.config.restartConfig.cacheKey @@ -114,6 +122,7 @@ internal class JDABuilderSession private constructor( block: Runnable ) { val session = JDABuilderSession(key) + sessions[key] = session activeSession.set(session) try { block.run() diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 0d56b24..bbd7885 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -16,7 +16,7 @@ private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") private val CD_JDAImpl = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") private val CD_JDABuilderSession = classDesc() -private const val builderSessionFieldName = "builderSession" +private const val cacheKeyFieldName = "cacheKey" // TODO add transform on JDAImpl#login(String, ShardInfo, Compression, boolean, int, GatewayEncoding) internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { @@ -25,12 +25,13 @@ internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/j val classFile = ClassFile.of() val classModel = classFile.parse(classData) return classFile.build(classModel.thisClass().asSymbol()) { classBuilder -> - classBuilder.withField(builderSessionFieldName, CD_JDABuilderSession, ACC_PRIVATE or ACC_FINAL) + classBuilder.withField(cacheKeyFieldName, CD_String, ACC_PRIVATE or ACC_FINAL) classBuilder.withMethod("getBuilderSession", MethodTypeDesc.of(CD_JDABuilderSession), ACC_PUBLIC) { methodBuilder -> methodBuilder.withCode { codeBuilder -> codeBuilder.aload(codeBuilder.receiverSlot()) - codeBuilder.getfield(CD_JDAImpl, "builderSession", CD_JDABuilderSession) + codeBuilder.getfield(CD_JDAImpl, cacheKeyFieldName, CD_String) + codeBuilder.invokestatic(CD_JDABuilderSession, "getSession", MethodTypeDesc.of(CD_JDABuilderSession, CD_String)) codeBuilder.areturn() } } @@ -60,10 +61,11 @@ private class CaptureSessionKeyTransform : ClassTransform { methodBuilder.withCode { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() - // this.builderSession = JDABuilderSession.currentSession() + // this.cacheKey = JDABuilderSession.currentSession().getKey() codeBuilder.aload(thisSlot) codeBuilder.invokestatic(CD_JDABuilderSession, "currentSession", MethodTypeDesc.of(CD_JDABuilderSession)) - codeBuilder.putfield(CD_JDAImpl, builderSessionFieldName, CD_JDABuilderSession) + codeBuilder.invokevirtual(CD_JDABuilderSession, "getKey", MethodTypeDesc.of(CD_String)) + codeBuilder.putfield(CD_JDAImpl, cacheKeyFieldName, CD_String) // Add existing instructions codeModel.forEach { codeBuilder.with(it) } From c51024bf6ff8a5272370d92526027272d5877fad Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:56:40 +0200 Subject: [PATCH 49/68] JDA cache: Rewrite shutdown() calls in JDAImpl#shutdownNow() to doShutdown() It previously called JDABuilderSession#onShutdown more than once --- .../jda/cache/transformer/JDAImplTransformer.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index bbd7885..66b0079 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -4,6 +4,7 @@ import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* +import java.lang.classfile.instruction.InvokeInstruction import java.lang.constant.* import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void @@ -191,7 +192,16 @@ private class ShutdownNowTransform : ClassTransform { methodBuilder.withCode { codeBuilder -> // Move the shutdownNow() code to doShutdownNow() - codeModel.forEach { codeBuilder.with(it) } + codeModel.forEach { codeElement -> + // Replace shutdown() with doShutdown() so we don't call [[JDABuilderSession#onShutdown]] more than once + if (codeElement is InvokeInstruction && codeElement.name().equalsString("shutdown")) { + require(codeElement.type().equalsString("()V")) + codeBuilder.invokevirtual(codeElement.owner().asSymbol(), "doShutdown", MethodTypeDesc.of(CD_void)) + return@forEach + } + + codeBuilder.with(codeElement) + } } } From 8a6b7cd74e0c7d439094375e8103ceb227a11543 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 19:57:29 +0200 Subject: [PATCH 50/68] JDA cache: Rename transformer classes of constructors To avoid duplicate names in the same package --- .../restart/jda/cache/transformer/JDABuilderTransformer.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 5bedb43..a06e1aa 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -30,13 +30,13 @@ internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tio return classFile.transformClass( classFile.parse(classData), PublicInstanceMethodTransform() - .andThen(ConstructorTransform()) + .andThen(JDABuilderConstructorTransform()) .andThen(BuildTransform()) ) } } -private class ConstructorTransform : ClassTransform { +private class JDABuilderConstructorTransform : ClassTransform { override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) From 2d45357067828ea37ebda092795b2e83b683d440 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:06:27 +0200 Subject: [PATCH 51/68] JDA cache: Defer BContextImpl#scheduleShutdownSignal Call the full code when shutting down JDA, or call the passed closure when reusing JDA The closure allows to shut down the pools after the events were fired --- .../botcommands/restart/jda/cache/Agent.kt | 2 + .../restart/jda/cache/JDABuilderSession.kt | 46 +++++- .../botcommands/restart/jda/cache/JDACache.kt | 1 + .../transformer/BContextImplTransformer.kt | 141 ++++++++++++++++++ .../jda/cache/BContextImplTransformerTest.kt | 15 ++ 5 files changed, 197 insertions(+), 8 deletions(-) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt create mode 100644 restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt index c3852b0..2d11297 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache +import dev.freya02.botcommands.restart.jda.cache.transformer.BContextImplTransformer import dev.freya02.botcommands.restart.jda.cache.transformer.JDABuilderTransformer import dev.freya02.botcommands.restart.jda.cache.transformer.JDAImplTransformer import dev.freya02.botcommands.restart.jda.cache.transformer.JDAServiceTransformer @@ -12,6 +13,7 @@ object Agent { println("Agent args: $agentArgs") inst.addTransformer(JDABuilderTransformer) inst.addTransformer(JDAServiceTransformer) + inst.addTransformer(BContextImplTransformer) inst.addTransformer(JDAImplTransformer) } } \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 3c960c0..b75c374 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -24,7 +24,7 @@ private val logger = KotlinLogging.logger { } // An hybrid way would require rewriting that proxy, // so our module can hook into JDA and set the gateway URL to the proxy's internal class JDABuilderSession private constructor( - private val key: String, + @get:DynamicCall val key: String, ) { @get:DynamicCall @@ -32,18 +32,36 @@ internal class JDABuilderSession private constructor( var wasBuilt: Boolean = false private set + private lateinit var scheduleShutdownSignal: ScheduleShutdownSignalWrapper + // May also be shutdownNow @DynamicCall fun onShutdown(instance: JDA, shutdownFunction: Runnable) { + if (::scheduleShutdownSignal.isInitialized.not()) { + logger.error { "Expected BContextImpl#scheduleShutdownSignal to be called before shutdown, doing a full shut down" } + return shutdownFunction.run() + } + if (isJvmShuttingDown()) { - shutdownFunction.run() - return + scheduleShutdownSignal.runFully() + return shutdownFunction.run() } val eventManager = instance.eventManager as? BufferingEventManager eventManager?.detach() // If the event manager isn't what we expect, it will be logged when attempting to reuse - JDACache[key] = JDACache.Data(configuration, instance, shutdownFunction) + JDACache[key] = JDACache.Data(configuration, instance, shutdownFunction, scheduleShutdownSignal) + } + + /** + * Stores the actual code of BContextImpl#scheduleShutdownSignal and the callback it was passed + * + * [scheduleShutdownSignalFunction] will be called with [afterShutdownSignal] if JDA does get shut down, + * but if JDA is reused, only [afterShutdownSignal] is used. + */ + @DynamicCall + fun onScheduleShutdownSignal(scheduleShutdownSignalFunction: Runnable, afterShutdownSignal: () -> Unit) { + this.scheduleShutdownSignal = ScheduleShutdownSignalWrapper(scheduleShutdownSignalFunction, afterShutdownSignal) } @DynamicCall @@ -54,10 +72,12 @@ internal class JDABuilderSession private constructor( } private fun buildOrReuse(buildFunction: Supplier): JDA { + val cachedData = JDACache.remove(key) + fun createNewInstance(): JDA { val jda = buildFunction.get() - val oldInstanceData = JDACache.remove(key) - oldInstanceData?.doShutdown?.run() + cachedData?.scheduleShutdownSignal?.runFully() + cachedData?.doShutdown?.run() return jda } @@ -66,7 +86,6 @@ internal class JDABuilderSession private constructor( return createNewInstance() } - val cachedData = JDACache[key] if (cachedData == null) { logger.debug { "Creating a new JDA instance (key '$key')" } return createNewInstance() @@ -78,10 +97,11 @@ internal class JDABuilderSession private constructor( val eventManager = jda.eventManager as? BufferingEventManager ?: run { logger.warn { "Expected a BufferingEventManager but got a ${jda.eventManager.javaClass.name}, creating a new instance" } - cachedData.doShutdown.run() return createNewInstance() } + cachedData.scheduleShutdownSignal.runAfterShutdownSignal() + eventManager.setDelegate(configuration.eventManager) eventManager.handle(StatusChangeEvent(jda, JDA.Status.LOADING_SUBSYSTEMS, JDA.Status.CONNECTED)) jda.guildCache.forEachUnordered { eventManager.handle(GuildReadyEvent(jda, -1, it)) } @@ -93,6 +113,16 @@ internal class JDABuilderSession private constructor( } } + internal class ScheduleShutdownSignalWrapper internal constructor( + private val scheduleShutdownSignalFunction: Runnable, + private val afterShutdownSignal: () -> Unit + ) { + + internal fun runFully(): Unit = scheduleShutdownSignalFunction.run() + + internal fun runAfterShutdownSignal(): Unit = afterShutdownSignal() + } + companion object { // I would store them in a Map, but JDABuilder has no idea what the key is private val activeSession: ThreadLocal = diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt index 7ec4ac1..a8d58dc 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt @@ -20,5 +20,6 @@ internal object JDACache { val configuration: JDABuilderConfiguration, val jda: JDA, val doShutdown: Runnable, + val scheduleShutdownSignal: JDABuilderSession.ScheduleShutdownSignalWrapper, ) } \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt new file mode 100644 index 0000000..087d393 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt @@ -0,0 +1,141 @@ +package dev.freya02.botcommands.restart.jda.cache.transformer + +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession +import io.github.oshai.kotlinlogging.KotlinLogging +import java.lang.classfile.* +import java.lang.classfile.ClassFile.* +import java.lang.constant.* +import java.lang.constant.ConstantDescs.CD_String +import java.lang.constant.ConstantDescs.CD_void +import java.lang.invoke.* +import java.lang.reflect.AccessFlag + +private val logger = KotlinLogging.logger { } + +// Avoid importing BC and JDA classes +private val CD_BContext = ClassDesc.of("io.github.freya022.botcommands.api.core.BContext") +private val CD_BContextImpl = ClassDesc.of("io.github.freya022.botcommands.internal.core.BContextImpl") +private val CD_Function0 = ClassDesc.of("kotlin.jvm.functions.Function0") + +internal object BContextImplTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/internal/core/BContextImpl") { + + override fun transform(classData: ByteArray): ByteArray { + val classFile = ClassFile.of() + val classModel = classFile.parse(classData) + + return classFile.transformClass(classModel, + ScheduleShutdownSignalTransform(classModel) + ) + } +} + +private class ScheduleShutdownSignalTransform(private val classModel: ClassModel) : ClassTransform { + + override fun atStart(classBuilder: ClassBuilder) { + val targetMethodModel = classModel.methods().firstOrNull(::isTargetMethod) + ?: error("Could not find BContextImpl#${TARGET_METHOD_NAME}${TARGET_METHOD_SIGNATURE}") + + logger.trace { "Transferring BContextImpl#${TARGET_METHOD_NAME}${TARGET_METHOD_SIGNATURE} into $LAMBDA_NAME" } + + classBuilder.withMethodBody( + LAMBDA_NAME, + MethodTypeDesc.ofDescriptor(TARGET_METHOD_SIGNATURE), + ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL + ) { codeBuilder -> + val codeModel = targetMethodModel.code().get() + codeModel.forEach { codeBuilder.with(it) } + } + } + + override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!isTargetMethod(methodModel)) return classBuilder.retain(classElement) + + logger.trace { "Transforming BContextImpl#${TARGET_METHOD_NAME}${TARGET_METHOD_SIGNATURE}" } + + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withFlags(*(methodModel.flags().flags() - AccessFlag.PRIVATE + AccessFlag.PUBLIC).toTypedArray()) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + val afterShutdownSignalSlot = codeBuilder.parameterSlot(0) + val doScheduleShutdownSignalSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val sessionKeySlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val builderSessionSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // Runnable doScheduleShutdownSignal = () -> this.doScheduleShutdownSignal(afterShutdownSignal) + codeBuilder.aload(thisSlot) + codeBuilder.aload(afterShutdownSignalSlot) + codeBuilder.invokedynamic(DynamicCallSiteDesc.of( + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + classDesc(), + "metafactory", + MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) + ), + // The following parameters are from [[LambdaMetafactory#metafactory]] + // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", + // the method name in Runnable is "run" + "run", + // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", + // the return type is the implemented interface, + // while the parameters are the captured variables + MethodTypeDesc.of(classDesc(), CD_BContextImpl, CD_Function0), + // Bootstrap arguments (see `javap -c -v ` from a working .java sample) + // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", + // which is the signature of the implemented method, in this case, void Runnable.run() + MethodTypeDesc.of(CD_void), + // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", + // this is the method to be called when invoking the lambda, + // with the captured variables and parameters + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + CD_BContextImpl, + LAMBDA_NAME, + MethodTypeDesc.of(CD_void, CD_Function0) + ), + // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", + // this is "the signature and return type to be enforced dynamically at invocation type" + // This is usually the same as "interfaceMethodType" + MethodTypeDesc.of(CD_void), + )) + codeBuilder.astore(doScheduleShutdownSignalSlot) + + // String sessionKey = JDABuilderSession.getCacheKey(this) + codeBuilder.aload(thisSlot) + codeBuilder.invokestatic(classDesc(), "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) + codeBuilder.astore(sessionKeySlot) + + // JDABuilderSession builderSession = JDABuilderSession.getSession(sessionKey) + codeBuilder.aload(sessionKeySlot) + codeBuilder.invokestatic(classDesc(), "getSession", MethodTypeDesc.of(classDesc(), CD_String)) + codeBuilder.astore(builderSessionSlot) + + // builderSession.onScheduleShutdownSignal(doScheduleShutdownSignal, afterShutdownSignal) + codeBuilder.aload(builderSessionSlot) + codeBuilder.aload(doScheduleShutdownSignalSlot) + codeBuilder.aload(afterShutdownSignalSlot) + codeBuilder.invokevirtual(classDesc(), "onScheduleShutdownSignal", MethodTypeDesc.of(CD_void, classDesc(), CD_Function0)) + + // Required + codeBuilder.return_() + } + } + } + + private fun isTargetMethod(methodModel: MethodModel): Boolean { + if (!methodModel.methodName().equalsString(TARGET_METHOD_NAME)) return false + if (!methodModel.methodType().equalsString(TARGET_METHOD_SIGNATURE)) return false + return true + } + + private companion object { + const val TARGET_METHOD_NAME = "scheduleShutdownSignal" + const val TARGET_METHOD_SIGNATURE = "(Lkotlin/jvm/functions/Function0;)V" + + const val LAMBDA_NAME = "doScheduleShutdownSignal" + } +} diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt new file mode 100644 index 0000000..123667e --- /dev/null +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt @@ -0,0 +1,15 @@ +package dev.freya02.botcommands.restart.jda.cache + +import org.junit.jupiter.api.assertDoesNotThrow +import kotlin.test.Test + +class BContextImplTransformerTest { + + @Test + fun `BContextImpl is instrumented`() { + assertDoesNotThrow { + Class.forName("io.github.freya022.botcommands.internal.core.BContextImpl") + .getDeclaredMethod("doScheduleShutdownSignal", Function0::class.java) + } + } +} \ No newline at end of file From da1e9afdb11dbb303fb48e8d62b2fbc6d586b2c9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:06:42 +0200 Subject: [PATCH 52/68] JDA cache: Clean up JDACache --- .../dev/freya02/botcommands/restart/jda/cache/JDACache.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt index a8d58dc..7bc004a 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDACache.kt @@ -6,10 +6,6 @@ internal object JDACache { private val cache: MutableMap = hashMapOf() - internal operator fun contains(key: String): Boolean = key in cache - - internal operator fun get(key: String): Data? = cache[key] - internal operator fun set(key: String, data: Data) { cache[key] = data } From 8962737f26f280e0097d0d57c8acc489f9b986c1 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 20:25:04 +0200 Subject: [PATCH 53/68] JDA cache: Remove note --- .../restart/jda/cache/transformer/JDAImplTransformer.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 66b0079..604d69d 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -19,7 +19,6 @@ private val CD_JDABuilderSession = classDesc() private const val cacheKeyFieldName = "cacheKey" -// TODO add transform on JDAImpl#login(String, ShardInfo, Compression, boolean, int, GatewayEncoding) internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { override fun transform(classData: ByteArray): ByteArray { From 6835e5cbdc7cddf0b96bcc1738ce42437604b899 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:01:34 +0200 Subject: [PATCH 54/68] JDA cache: When JDA shuts down, check JVM status first, don't run scheduleShutdownSignal As we don't have it when it is triggered by JDA's shutdown hook --- .../botcommands/restart/jda/cache/JDABuilderSession.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index b75c374..b3dd98b 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -37,13 +37,13 @@ internal class JDABuilderSession private constructor( // May also be shutdownNow @DynamicCall fun onShutdown(instance: JDA, shutdownFunction: Runnable) { - if (::scheduleShutdownSignal.isInitialized.not()) { - logger.error { "Expected BContextImpl#scheduleShutdownSignal to be called before shutdown, doing a full shut down" } + if (isJvmShuttingDown()) { + // "scheduleShutdownSignal" isn't there yet if this shutdown is triggered by JDA's shutdown hook return shutdownFunction.run() } - if (isJvmShuttingDown()) { - scheduleShutdownSignal.runFully() + if (::scheduleShutdownSignal.isInitialized.not()) { + logger.error { "Expected BContextImpl#scheduleShutdownSignal to be called before shutdown, doing a full shut down" } return shutdownFunction.run() } From 6c20eeabf437d597ae8cc0b4edbd2edc637889ed Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:09:02 +0200 Subject: [PATCH 55/68] JDA cache: Shutdown if the configuration has unsupported values --- .../botcommands/restart/jda/cache/JDABuilderSession.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index b3dd98b..6405760 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -47,6 +47,13 @@ internal class JDABuilderSession private constructor( return shutdownFunction.run() } + // Don't save if this configuration has unsupported values + if (configuration.hasUnsupportedValues) { + scheduleShutdownSignal.runFully() + shutdownFunction.run() + return logger.debug { "Discarding JDA instance as the configuration had unsupported values (key '$key')" } + } + val eventManager = instance.eventManager as? BufferingEventManager eventManager?.detach() // If the event manager isn't what we expect, it will be logged when attempting to reuse From 268b29e78c507756fc0def2755661d5990cdb976 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:10:29 +0200 Subject: [PATCH 56/68] JDA cache: Consider configurations not equal if one of them has unsupported values Not actually useful at this moment, but still --- .../botcommands/restart/jda/cache/JDABuilderConfiguration.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index 6269cb3..1c30552 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -101,6 +101,9 @@ class JDABuilderConfiguration internal constructor() { } internal infix fun isSameAs(other: JDABuilderConfiguration): Boolean { + if (hasUnsupportedValues) return false + if (other.hasUnsupportedValues) return false + return builderValues == other.builderValues } From b870e79a70b18c4c4aafb9cf570191f9b3feee11 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Fri, 6 Jun 2025 21:10:45 +0200 Subject: [PATCH 57/68] JDA cache: Support JDABuilder#setEnableShutdownHook --- .../restart/jda/cache/JDABuilderConfiguration.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt index 1c30552..9910c7f 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderConfiguration.kt @@ -100,6 +100,11 @@ class JDABuilderConfiguration internal constructor() { builderValues[ValueType.ACTIVITY] = activity } + @DynamicCall + fun setEnableShutdownHook(enable: Boolean) { + builderValues[ValueType.ENABLE_SHUTDOWN_HOOK] = enable + } + internal infix fun isSameAs(other: JDABuilderConfiguration): Boolean { if (hasUnsupportedValues) return false if (other.hasUnsupportedValues) return false @@ -119,6 +124,7 @@ class JDABuilderConfiguration internal constructor() { MEMBER_CACHE_POLICY, CHUNKING_FILTER, LARGE_THRESHOLD, - ACTIVITY + ACTIVITY, + ENABLE_SHUTDOWN_HOOK, } } \ No newline at end of file From feb3ec0b74b6ca7693a2cb5e87f40dfb4c34849e Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:28:24 +0200 Subject: [PATCH 58/68] Add test libs to convention plugin --- .../main/kotlin/BotCommands-Restarter-conventions.gradle.kts | 3 ++- restarter-jda-cache/build.gradle.kts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts b/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts index 93d88bb..ffa6e82 100644 --- a/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/BotCommands-Restarter-conventions.gradle.kts @@ -24,7 +24,8 @@ repositories { } dependencies { - + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter-params:5.10.0") } kotlin { diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts index 48a26e8..7426527 100644 --- a/restarter-jda-cache/build.gradle.kts +++ b/restarter-jda-cache/build.gradle.kts @@ -13,7 +13,6 @@ dependencies { implementation(libs.kotlin.logging) implementation(libs.botcommands) - testImplementation(kotlin("test")) testImplementation(libs.mockk) testImplementation(libs.bytebuddy) testImplementation(libs.logback.classic) From 53f5730a8f28108d80ebc856f1d50be22e6a1735 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:34:15 +0200 Subject: [PATCH 59/68] JDA cache: Add utility method to create DynamicCallSiteDesc --- .../transformer/BContextImplTransformer.kt | 44 ++---- .../transformer/JDABuilderTransformer.kt | 45 ++---- .../cache/transformer/JDAImplTransformer.kt | 84 +++-------- .../transformer/JDAServiceTransformer.kt | 46 ++---- .../restart/jda/cache/transformer/utils.kt | 73 +++++++++- .../restart/jda/cache/CodeBuilderTest.kt | 136 ++++++++++++++++++ 6 files changed, 258 insertions(+), 170 deletions(-) create mode 100644 restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt index 087d393..3b58405 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt @@ -4,10 +4,10 @@ import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* -import java.lang.constant.* +import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void -import java.lang.invoke.* +import java.lang.constant.MethodTypeDesc import java.lang.reflect.AccessFlag private val logger = KotlinLogging.logger { } @@ -69,38 +69,14 @@ private class ScheduleShutdownSignalTransform(private val classModel: ClassModel // Runnable doScheduleShutdownSignal = () -> this.doScheduleShutdownSignal(afterShutdownSignal) codeBuilder.aload(thisSlot) codeBuilder.aload(afterShutdownSignalSlot) - codeBuilder.invokedynamic(DynamicCallSiteDesc.of( - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.STATIC, - classDesc(), - "metafactory", - MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) - ), - // The following parameters are from [[LambdaMetafactory#metafactory]] - // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", - // the method name in Runnable is "run" - "run", - // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", - // the return type is the implemented interface, - // while the parameters are the captured variables - MethodTypeDesc.of(classDesc(), CD_BContextImpl, CD_Function0), - // Bootstrap arguments (see `javap -c -v ` from a working .java sample) - // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", - // which is the signature of the implemented method, in this case, void Runnable.run() - MethodTypeDesc.of(CD_void), - // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", - // this is the method to be called when invoking the lambda, - // with the captured variables and parameters - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.VIRTUAL, - CD_BContextImpl, - LAMBDA_NAME, - MethodTypeDesc.of(CD_void, CD_Function0) - ), - // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", - // this is "the signature and return type to be enforced dynamically at invocation type" - // This is usually the same as "interfaceMethodType" - MethodTypeDesc.of(CD_void), + codeBuilder.invokedynamic(createLambda( + interfaceMethod = Runnable::run, + targetType = CD_BContextImpl, + targetMethod = "doScheduleShutdownSignal", + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_Function0), + isStatic = false )) codeBuilder.astore(doScheduleShutdownSignalSlot) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index a06e1aa..12c392d 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -6,9 +6,9 @@ import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* -import java.lang.constant.* +import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.* -import java.lang.invoke.* +import java.lang.constant.MethodTypeDesc import java.lang.reflect.AccessFlag import java.util.function.Supplier @@ -154,38 +154,15 @@ private class BuildTransform : ClassTransform { // Supplier doBuild = this::doBuild codeBuilder.aload(thisSlot) - codeBuilder.invokedynamic(DynamicCallSiteDesc.of( - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.STATIC, - classDesc(), - "metafactory", - MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) - ), - // The following parameters are from [[LambdaMetafactory#metafactory]] - // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", - // the method name in Supplier is "get" - "get", - // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", - // the return type is the implemented interface, - // while the parameters are the captured variables (incl. receiver) - MethodTypeDesc.of(classDesc>(), CD_JDABuilder), - // Bootstrap arguments (see `javap -c -v ` from a working .java sample) - // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", - // which is the signature of the implemented method, in this case, Object get() - MethodTypeDesc.of(CD_Object), - // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", - // this is the method to be called when invoking the lambda, - // with the captured variables and parameters - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.VIRTUAL, - CD_JDABuilder, - newBuildMethodName, - MethodTypeDesc.of(CD_JDA) - ), - // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", - // this is "the signature and return type to be enforced dynamically at invocation type" - // This is usually the same as "interfaceMethodType" - MethodTypeDesc.of(CD_Object), + + codeBuilder.invokedynamic(createLambda( + interfaceMethod = Supplier<*>::get, + targetType = CD_JDABuilder, + targetMethod = newBuildMethodName, + targetMethodReturnType = CD_JDA, + targetMethodArguments = listOf(), + capturedTypes = emptyList(), + isStatic = false )) codeBuilder.astore(doBuildSlot) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 604d69d..bf414df 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -5,10 +5,10 @@ import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* import java.lang.classfile.instruction.InvokeInstruction -import java.lang.constant.* +import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void -import java.lang.invoke.* +import java.lang.constant.MethodTypeDesc private val logger = KotlinLogging.logger { } @@ -114,38 +114,14 @@ private class ShutdownTransform : ClassTransform { // Runnable doShutdown = this::doShutdown codeBuilder.aload(thisSlot) - codeBuilder.invokedynamic(DynamicCallSiteDesc.of( - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.STATIC, - classDesc(), - "metafactory", - MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) - ), - // The following parameters are from [[LambdaMetafactory#metafactory]] - // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", - // the method name in Supplier is "get" - "run", - // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", - // the return type is the implemented interface, - // while the parameters are the captured variables (incl. receiver) - MethodTypeDesc.of(classDesc(), CD_JDAImpl), - // Bootstrap arguments (see `javap -c -v ` from a working .java sample) - // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", - // which is the signature of the implemented method, in this case, void run() - MethodTypeDesc.of(CD_void), - // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", - // this is the method to be called when invoking the lambda, - // with the captured variables and parameters - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.VIRTUAL, - CD_JDAImpl, - newShutdownMethodName, - MethodTypeDesc.of(CD_void) - ), - // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", - // this is "the signature and return type to be enforced dynamically at invocation type" - // This is usually the same as "interfaceMethodType" - MethodTypeDesc.of(CD_void), + codeBuilder.invokedynamic(createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAImpl, + targetMethod = newShutdownMethodName, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(), + isStatic = false )) codeBuilder.astore(doShutdownSlot) @@ -215,38 +191,14 @@ private class ShutdownNowTransform : ClassTransform { // Runnable doShutdownNow = this::doShutdownNow codeBuilder.aload(thisSlot) - codeBuilder.invokedynamic(DynamicCallSiteDesc.of( - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.STATIC, - classDesc(), - "metafactory", - MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) - ), - // The following parameters are from [[LambdaMetafactory#metafactory]] - // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", - // the method name in Supplier is "get" - "run", - // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", - // the return type is the implemented interface, - // while the parameters are the captured variables (incl. receiver) - MethodTypeDesc.of(classDesc(), CD_JDAImpl), - // Bootstrap arguments (see `javap -c -v ` from a working .java sample) - // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", - // which is the signature of the implemented method, in this case, void run() - MethodTypeDesc.of(CD_void), - // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", - // this is the method to be called when invoking the lambda, - // with the captured variables and parameters - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.VIRTUAL, - CD_JDAImpl, - newShutdownMethodName, - MethodTypeDesc.of(CD_void) - ), - // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", - // this is "the signature and return type to be enforced dynamically at invocation type" - // This is usually the same as "interfaceMethodType" - MethodTypeDesc.of(CD_void), + codeBuilder.invokedynamic(createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAImpl, + targetMethod = newShutdownMethodName, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(), + isStatic = false )) codeBuilder.astore(doShutdownNowSlot) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index 65a836a..452314e 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -7,10 +7,10 @@ import java.lang.classfile.ClassFile.* import java.lang.classfile.CodeModel import java.lang.classfile.MethodModel import java.lang.classfile.TypeKind -import java.lang.constant.* +import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void -import java.lang.invoke.* +import java.lang.constant.MethodTypeDesc private val logger = KotlinLogging.logger { } @@ -81,42 +81,18 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ codeBuilder.aload(sessionKeySlot) codeBuilder.ifnull(nullKeyLabel) - // Runnable sessionRunnable = this::[lambdaName] + // Runnable sessionRunnable = () -> [lambdaName](event, eventManager) codeBuilder.aload(thisSlot) codeBuilder.aload(readyEventSlot) codeBuilder.aload(eventManagerSlot) - codeBuilder.invokedynamic(DynamicCallSiteDesc.of( - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.STATIC, - classDesc(), - "metafactory", - MethodTypeDesc.of(classDesc(), classDesc(), CD_String, classDesc(), classDesc(), classDesc(), classDesc()) - ), - // The following parameters are from [[LambdaMetafactory#metafactory]] - // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", - // the method name in Runnable is "run" - "run", - // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", - // the return type is the implemented interface, - // while the parameters are the captured variables - MethodTypeDesc.of(classDesc(), CD_JDAService, CD_BReadyEvent, CD_IEventManager), - // Bootstrap arguments (see `javap -c -v ` from a working .java sample) - // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", - // which is the signature of the implemented method, in this case, void Runnable.run() - MethodTypeDesc.of(CD_void), - // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", - // this is the method to be called when invoking the lambda, - // with the captured variables and parameters - MethodHandleDesc.ofMethod( - DirectMethodHandleDesc.Kind.VIRTUAL, - CD_JDAService, - lambdaName, - MethodTypeDesc.of(CD_void, CD_BReadyEvent, CD_IEventManager) - ), - // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", - // this is "the signature and return type to be enforced dynamically at invocation type" - // This is usually the same as "interfaceMethodType" - MethodTypeDesc.of(CD_void), + codeBuilder.invokedynamic(createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAService, + targetMethod = lambdaName, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), + isStatic = false )) codeBuilder.astore(sessionRunnableSlot) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt index 3222ece..454e742 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt @@ -2,10 +2,81 @@ package dev.freya02.botcommands.restart.jda.cache.transformer import java.lang.classfile.ClassFileBuilder import java.lang.classfile.ClassFileElement -import java.lang.constant.ClassDesc +import java.lang.constant.* +import java.lang.constant.ConstantDescs.CD_String +import java.lang.invoke.* +import kotlin.reflect.KFunction +import kotlin.reflect.jvm.javaMethod internal inline fun classDesc(): ClassDesc = ClassDesc.of(T::class.java.name) internal fun ClassFileBuilder.retain(element: E) { with(element) +} + +internal val lambdaMetafactoryDesc = MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.STATIC, + classDesc(), + "metafactory", + MethodTypeDesc.of( + classDesc(), + classDesc(), + CD_String, + classDesc(), + classDesc(), + classDesc(), + classDesc() + ) +) + +internal fun createLambda( + interfaceMethod: KFunction<*>, + targetType: ClassDesc, + targetMethod: String, + targetMethodReturnType: ClassDesc, + targetMethodArguments: List, + capturedTypes: List, + isStatic: Boolean, +): DynamicCallSiteDesc { + val effectiveCapturedTypes = when { + isStatic -> capturedTypes + else -> listOf(targetType) + capturedTypes + } + + fun Class<*>.toClassDesc(): ClassDesc = + describeConstable().orElseThrow { IllegalArgumentException("$name cannot be transformed to a ClassDesc") } + + val interfaceJavaMethod = interfaceMethod.javaMethod!! + val targetInterface = interfaceJavaMethod.declaringClass.toClassDesc() + val methodReturnType = interfaceJavaMethod.returnType.toClassDesc() + val methodArguments = interfaceJavaMethod.parameterTypes.map { it.toClassDesc() } + + return DynamicCallSiteDesc.of( + lambdaMetafactoryDesc, + // The following parameters are from [[LambdaMetafactory#metafactory]] + // This is the 2nd argument of LambdaMetafactory#metafactory, "interfaceMethodName", + // the method name in Runnable is "run" + interfaceMethod.name, + // This is the 3rd argument of LambdaMetafactory#metafactory, "factoryType", + // the return type is the implemented interface, + // while the parameters are the captured variables + MethodTypeDesc.of(targetInterface, effectiveCapturedTypes), + // Bootstrap arguments (see `javap -c -v ` from a working .java sample) + // This is the 4th argument of LambdaMetafactory#metafactory, "interfaceMethodType", + // which is the signature of the implemented method, in this case, void Runnable.run() + MethodTypeDesc.of(methodReturnType, methodArguments), + // This is the 5th argument of LambdaMetafactory#metafactory, "implementation", + // this is the method to be called when invoking the lambda, + // with the captured variables and parameters + MethodHandleDesc.ofMethod( + if (isStatic) DirectMethodHandleDesc.Kind.STATIC else DirectMethodHandleDesc.Kind.VIRTUAL, + targetType, + targetMethod, + MethodTypeDesc.of(targetMethodReturnType, capturedTypes + targetMethodArguments) + ), + // This is the 6th argument of LambdaMetafactory#metafactory, "dynamicMethodType", + // this is "the signature and return type to be enforced dynamically at invocation type" + // This is usually the same as "interfaceMethodType" + MethodTypeDesc.of(methodReturnType, methodArguments), + ) } \ No newline at end of file diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt new file mode 100644 index 0000000..80be50c --- /dev/null +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt @@ -0,0 +1,136 @@ +package dev.freya02.botcommands.restart.jda.cache + +import dev.freya02.botcommands.restart.jda.cache.transformer.classDesc +import dev.freya02.botcommands.restart.jda.cache.transformer.createLambda +import dev.freya02.botcommands.restart.jda.cache.transformer.lambdaMetafactoryDesc +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource +import java.lang.constant.* +import java.lang.constant.ConstantDescs.CD_Object +import java.lang.constant.ConstantDescs.CD_void +import java.util.function.Supplier +import kotlin.test.assertEquals + +private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") +private val CD_JDAImpl = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") +private val CD_JDABuilder = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") +private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventManager") + +private val CD_JDAService = ClassDesc.of("io.github.freya022.botcommands.api.core.JDAService") +private val CD_BReadyEvent = ClassDesc.of("io.github.freya022.botcommands.api.core.events.BReadyEvent") +private val CD_BContextImpl = ClassDesc.of("io.github.freya022.botcommands.internal.core.BContextImpl") + +private val CD_Function0 = ClassDesc.of("kotlin.jvm.functions.Function0") + +object CodeBuilderTest { + + @MethodSource("Test createLambda") + @ParameterizedTest + fun `Test createLambda`(expected: DynamicCallSiteDesc, actual: DynamicCallSiteDesc) { + assertEquals(expected, actual) + } + + @JvmStatic + fun `Test createLambda`(): List = listOf( + Arguments.of( + DynamicCallSiteDesc.of( + lambdaMetafactoryDesc, + "get", + MethodTypeDesc.of(classDesc>(), CD_JDABuilder), + MethodTypeDesc.of(CD_Object), + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + CD_JDABuilder, + "doBuild", + MethodTypeDesc.of(CD_JDA) + ), + MethodTypeDesc.of(CD_Object), + ), + createLambda( + interfaceMethod = Supplier<*>::get, + targetType = CD_JDABuilder, + targetMethod = "doBuild", + targetMethodReturnType = CD_JDA, + targetMethodArguments = listOf(), + capturedTypes = listOf(), + isStatic = false + ) + ), + + Arguments.of( + DynamicCallSiteDesc.of( + lambdaMetafactoryDesc, + "run", + MethodTypeDesc.of(classDesc(), CD_JDAService, CD_BReadyEvent, CD_IEventManager), + MethodTypeDesc.of(CD_void), + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + CD_JDAService, + $$"lambda$onReadyEvent$BotCommands$withBuilderSession", + MethodTypeDesc.of(CD_void, CD_BReadyEvent, CD_IEventManager) + ), + MethodTypeDesc.of(CD_void), + ), + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAService, + targetMethod = $$"lambda$onReadyEvent$BotCommands$withBuilderSession", + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), + isStatic = false + ) + ), + + Arguments.of( + DynamicCallSiteDesc.of( + lambdaMetafactoryDesc, + "run", + MethodTypeDesc.of(classDesc(), CD_JDAImpl), + MethodTypeDesc.of(CD_void), + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + CD_JDAImpl, + "doShutdown", + MethodTypeDesc.of(CD_void) + ), + MethodTypeDesc.of(CD_void), + ), + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAImpl, + targetMethod = "doShutdown", + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(), + isStatic = false + ) + ), + + Arguments.of( + DynamicCallSiteDesc.of( + lambdaMetafactoryDesc, + "run", + MethodTypeDesc.of(classDesc(), CD_BContextImpl, CD_Function0), + MethodTypeDesc.of(CD_void), + MethodHandleDesc.ofMethod( + DirectMethodHandleDesc.Kind.VIRTUAL, + CD_BContextImpl, + "doScheduleShutdownSignal", + MethodTypeDesc.of(CD_void, CD_Function0) + ), + MethodTypeDesc.of(CD_void), + ), + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_BContextImpl, + targetMethod = "doScheduleShutdownSignal", + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_Function0), + isStatic = false + ) + ), + ) +} \ No newline at end of file From 596ec4ad9ddf0b66f60194cc96e7bb64579f64e2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:02:55 +0200 Subject: [PATCH 60/68] JDA cache: Move all class descriptors to a file --- .../transformer/BContextImplTransformer.kt | 13 ++------ .../jda/cache/transformer/ClassDescriptors.kt | 31 +++++++++++++++++++ .../{utils.kt => CodeBuilderUtils.kt} | 0 .../transformer/JDABuilderTransformer.kt | 29 ++++++----------- .../cache/transformer/JDAImplTransformer.kt | 10 ++---- .../transformer/JDAServiceTransformer.kt | 14 ++------- 6 files changed, 48 insertions(+), 49 deletions(-) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/{utils.kt => CodeBuilderUtils.kt} (100%) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt index 3b58405..184bb6d 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt @@ -1,10 +1,8 @@ package dev.freya02.botcommands.restart.jda.cache.transformer -import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* -import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void import java.lang.constant.MethodTypeDesc @@ -12,11 +10,6 @@ import java.lang.reflect.AccessFlag private val logger = KotlinLogging.logger { } -// Avoid importing BC and JDA classes -private val CD_BContext = ClassDesc.of("io.github.freya022.botcommands.api.core.BContext") -private val CD_BContextImpl = ClassDesc.of("io.github.freya022.botcommands.internal.core.BContextImpl") -private val CD_Function0 = ClassDesc.of("kotlin.jvm.functions.Function0") - internal object BContextImplTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/internal/core/BContextImpl") { override fun transform(classData: ByteArray): ByteArray { @@ -82,19 +75,19 @@ private class ScheduleShutdownSignalTransform(private val classModel: ClassModel // String sessionKey = JDABuilderSession.getCacheKey(this) codeBuilder.aload(thisSlot) - codeBuilder.invokestatic(classDesc(), "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) + codeBuilder.invokestatic(CD_JDABuilderSession, "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) codeBuilder.astore(sessionKeySlot) // JDABuilderSession builderSession = JDABuilderSession.getSession(sessionKey) codeBuilder.aload(sessionKeySlot) - codeBuilder.invokestatic(classDesc(), "getSession", MethodTypeDesc.of(classDesc(), CD_String)) + codeBuilder.invokestatic(CD_JDABuilderSession, "getSession", MethodTypeDesc.of(CD_JDABuilderSession, CD_String)) codeBuilder.astore(builderSessionSlot) // builderSession.onScheduleShutdownSignal(doScheduleShutdownSignal, afterShutdownSignal) codeBuilder.aload(builderSessionSlot) codeBuilder.aload(doScheduleShutdownSignalSlot) codeBuilder.aload(afterShutdownSignalSlot) - codeBuilder.invokevirtual(classDesc(), "onScheduleShutdownSignal", MethodTypeDesc.of(CD_void, classDesc(), CD_Function0)) + codeBuilder.invokevirtual(CD_JDABuilderSession, "onScheduleShutdownSignal", MethodTypeDesc.of(CD_void, CD_Runnable, CD_Function0)) // Required codeBuilder.return_() diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt new file mode 100644 index 0000000..2e41080 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt @@ -0,0 +1,31 @@ +package dev.freya02.botcommands.restart.jda.cache.transformer + +import dev.freya02.botcommands.restart.jda.cache.BufferingEventManager +import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession +import org.intellij.lang.annotations.Language +import java.lang.constant.ClassDesc + +internal val CD_Function0 = classDescOf("kotlin.jvm.functions.Function0") + +internal val CD_IllegalStateException = classDescOf("java.lang.IllegalStateException") +internal val CD_Runnable = classDescOf("java.lang.Runnable") +internal val CD_Supplier = classDescOf("java.util.function.Supplier") + +internal val CD_BContext = classDescOf("io.github.freya022.botcommands.api.core.BContext") +internal val CD_BContextImpl = classDescOf("io.github.freya022.botcommands.internal.core.BContextImpl") +internal val CD_JDAService = classDescOf("io.github.freya022.botcommands.api.core.JDAService") +internal val CD_BReadyEvent = classDescOf("io.github.freya022.botcommands.api.core.events.BReadyEvent") + +internal val CD_JDA = classDescOf("net.dv8tion.jda.api.JDA") +internal val CD_JDAImpl = classDescOf("net.dv8tion.jda.internal.JDAImpl") +internal val CD_JDABuilder = classDescOf("net.dv8tion.jda.api.JDABuilder") +internal val CD_IEventManager = classDescOf("net.dv8tion.jda.api.hooks.IEventManager") + +internal val CD_BufferingEventManager = classDesc() +internal val CD_JDABuilderSession = classDesc() +internal val CD_JDABuilderConfiguration = classDesc() + +private fun classDescOf(@Language("java", prefix = "import ", suffix = ";") name: String): ClassDesc { + return ClassDesc.of(name) +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt similarity index 100% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 12c392d..381f05e 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -1,8 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer -import dev.freya02.botcommands.restart.jda.cache.BufferingEventManager import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration -import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* @@ -14,15 +12,6 @@ import java.util.function.Supplier private val logger = KotlinLogging.logger { } -// Avoid importing BC and JDA classes -private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") -private val CD_JDABuilder = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") -private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventManager") - -private val CD_BufferingEventManager = classDesc() - -private val CD_IllegalStateException = ClassDesc.of("java.lang.IllegalStateException") - internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tion/jda/api/JDABuilder") { override fun transform(classData: ByteArray): ByteArray { @@ -60,15 +49,15 @@ private class JDABuilderConstructorTransform : ClassTransform { val intentsSlot = codeBuilder.parameterSlot(1) // JDABuilderConfiguration configuration = JDABuilderSession.currentSession().getConfiguration(); - codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) - codeBuilder.invokevirtual(classDesc(), "getConfiguration", MethodTypeDesc.of(classDesc())) + codeBuilder.invokestatic(CD_JDABuilderSession, "currentSession", MethodTypeDesc.of(CD_JDABuilderSession)) + codeBuilder.invokevirtual(CD_JDABuilderSession, "getConfiguration", MethodTypeDesc.of(CD_JDABuilderConfiguration)) codeBuilder.astore(builderConfigurationSlot) // configuration.onInit(token, intents); codeBuilder.aload(builderConfigurationSlot) codeBuilder.aload(tokenSlot) codeBuilder.iload(intentsSlot) - codeBuilder.invokevirtual(classDesc(), "onInit", MethodTypeDesc.of(CD_void, CD_String, CD_int)) + codeBuilder.invokevirtual(CD_JDABuilderConfiguration, "onInit", MethodTypeDesc.of(CD_void, CD_String, CD_int)) // Add existing instructions codeModel.forEach { codeBuilder.with(it) } @@ -167,13 +156,13 @@ private class BuildTransform : ClassTransform { codeBuilder.astore(doBuildSlot) // JDABuilderSession session = JDABuilderSession.currentSession(); - codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) + codeBuilder.invokestatic(CD_JDABuilderSession, "currentSession", MethodTypeDesc.of(CD_JDABuilderSession)) codeBuilder.astore(builderSessionSlot) // var jda = session.onBuild(this::doBuild); codeBuilder.aload(builderSessionSlot) codeBuilder.aload(doBuildSlot) - codeBuilder.invokevirtual(classDesc(), "onBuild", MethodTypeDesc.of(CD_JDA, classDesc>())) + codeBuilder.invokevirtual(CD_JDABuilderSession, "onBuild", MethodTypeDesc.of(CD_JDA, CD_Supplier)) // Again, prefer using a variable for clarity codeBuilder.astore(jdaSlot) @@ -207,8 +196,8 @@ private class PublicInstanceMethodTransform : ClassTransform { val builderConfigurationSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) // JDABuilderConfiguration configuration = JDABuilderSession.currentSession().getConfiguration(); - codeBuilder.invokestatic(classDesc(), "currentSession", MethodTypeDesc.of(classDesc())) - codeBuilder.invokevirtual(classDesc(), "getConfiguration", MethodTypeDesc.of(classDesc())) + codeBuilder.invokestatic(CD_JDABuilderSession, "currentSession", MethodTypeDesc.of(CD_JDABuilderSession)) + codeBuilder.invokevirtual(CD_JDABuilderSession, "getConfiguration", MethodTypeDesc.of(CD_JDABuilderConfiguration)) codeBuilder.astore(builderConfigurationSlot) if (hasBuilderSessionMethod) { @@ -225,7 +214,7 @@ private class PublicInstanceMethodTransform : ClassTransform { val slot = codeBuilder.parameterSlot(index) codeBuilder.loadLocal(typeKind, slot) } - codeBuilder.invokevirtual(classDesc(), methodName, methodType) + codeBuilder.invokevirtual(CD_JDABuilderConfiguration, methodName, methodType) } else { logger.trace { "Skipping $methodModel as it does not have an equivalent method handler" } @@ -234,7 +223,7 @@ private class PublicInstanceMethodTransform : ClassTransform { // configuration.markUnsupportedValue() codeBuilder.aload(builderConfigurationSlot) codeBuilder.ldc(signature as java.lang.String) - codeBuilder.invokevirtual(classDesc(), "markUnsupportedValue", MethodTypeDesc.of(CD_void, CD_String)) + codeBuilder.invokevirtual(CD_JDABuilderConfiguration, "markUnsupportedValue", MethodTypeDesc.of(CD_void, CD_String)) } // Add existing instructions diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index bf414df..98678a7 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -1,6 +1,5 @@ package dev.freya02.botcommands.restart.jda.cache.transformer -import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* @@ -12,11 +11,6 @@ import java.lang.constant.MethodTypeDesc private val logger = KotlinLogging.logger { } -// Avoid importing BC and JDA classes -private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") -private val CD_JDAImpl = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") -private val CD_JDABuilderSession = classDesc() - private const val cacheKeyFieldName = "cacheKey" internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { @@ -134,7 +128,7 @@ private class ShutdownTransform : ClassTransform { codeBuilder.aload(builderSessionSlot) codeBuilder.aload(thisSlot) codeBuilder.aload(doShutdownSlot) - codeBuilder.invokevirtual(CD_JDABuilderSession, "onShutdown", MethodTypeDesc.of(CD_void, CD_JDA, classDesc())) + codeBuilder.invokevirtual(CD_JDABuilderSession, "onShutdown", MethodTypeDesc.of(CD_void, CD_JDA, CD_Runnable)) codeBuilder.return_() } @@ -211,7 +205,7 @@ private class ShutdownNowTransform : ClassTransform { codeBuilder.aload(builderSessionSlot) codeBuilder.aload(thisSlot) codeBuilder.aload(doShutdownNowSlot) - codeBuilder.invokevirtual(CD_JDABuilderSession, "onShutdown", MethodTypeDesc.of(CD_void, CD_JDA, classDesc())) + codeBuilder.invokevirtual(CD_JDABuilderSession, "onShutdown", MethodTypeDesc.of(CD_void, CD_JDA, CD_Runnable)) codeBuilder.return_() } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index 452314e..535095b 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -1,25 +1,17 @@ package dev.freya02.botcommands.restart.jda.cache.transformer -import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.ClassFile import java.lang.classfile.ClassFile.* import java.lang.classfile.CodeModel import java.lang.classfile.MethodModel import java.lang.classfile.TypeKind -import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void import java.lang.constant.MethodTypeDesc private val logger = KotlinLogging.logger { } -// Avoid importing BC and JDA classes -private val CD_BContext = ClassDesc.of("io.github.freya022.botcommands.api.core.BContext") -private val CD_JDAService = ClassDesc.of("io.github.freya022.botcommands.api.core.JDAService") -private val CD_BReadyEvent = ClassDesc.of("io.github.freya022.botcommands.api.core.events.BReadyEvent") -private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventManager") - internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/api/core/JDAService") { private const val TARGET_METHOD_NAME = $$"onReadyEvent$BotCommands" @@ -70,7 +62,7 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ // var key = JDABuilderSession.getCacheKey(context) codeBuilder.aload(contextSlot) - codeBuilder.invokestatic(classDesc(), "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) + codeBuilder.invokestatic(CD_JDABuilderSession, "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) codeBuilder.astore(sessionKeySlot) // THE KEY IS NULLABLE @@ -96,10 +88,10 @@ internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/ )) codeBuilder.astore(sessionRunnableSlot) - // JDABuilderSession.withBuilderSession(key, this::[lambdaName]) + // JDABuilderSession.withBuilderSession(key, sessionRunnable) codeBuilder.aload(sessionKeySlot) codeBuilder.aload(sessionRunnableSlot) - codeBuilder.invokestatic(classDesc(), "withBuilderSession", MethodTypeDesc.of(CD_void, CD_String, classDesc())) + codeBuilder.invokestatic(CD_JDABuilderSession, "withBuilderSession", MethodTypeDesc.of(CD_void, CD_String, CD_Runnable)) // Required codeBuilder.return_() From 741423c3fe23fc33d2659d9328bd82323a8d2bb2 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 12:03:19 +0200 Subject: [PATCH 61/68] JDA cache: Move CodeBuilderTest to package, use common class descriptors --- .../{ => transformer}/CodeBuilderTest.kt | 54 +++++++------------ 1 file changed, 19 insertions(+), 35 deletions(-) rename restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/CodeBuilderTest.kt (61%) diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderTest.kt similarity index 61% rename from restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt rename to restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderTest.kt index 80be50c..baaae9a 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/CodeBuilderTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderTest.kt @@ -1,28 +1,12 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer -import dev.freya02.botcommands.restart.jda.cache.transformer.classDesc -import dev.freya02.botcommands.restart.jda.cache.transformer.createLambda -import dev.freya02.botcommands.restart.jda.cache.transformer.lambdaMetafactoryDesc import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource import java.lang.constant.* -import java.lang.constant.ConstantDescs.CD_Object -import java.lang.constant.ConstantDescs.CD_void import java.util.function.Supplier import kotlin.test.assertEquals -private val CD_JDA = ClassDesc.of("net.dv8tion.jda.api.JDA") -private val CD_JDAImpl = ClassDesc.of("net.dv8tion.jda.internal.JDAImpl") -private val CD_JDABuilder = ClassDesc.of("net.dv8tion.jda.api.JDABuilder") -private val CD_IEventManager = ClassDesc.of("net.dv8tion.jda.api.hooks.IEventManager") - -private val CD_JDAService = ClassDesc.of("io.github.freya022.botcommands.api.core.JDAService") -private val CD_BReadyEvent = ClassDesc.of("io.github.freya022.botcommands.api.core.events.BReadyEvent") -private val CD_BContextImpl = ClassDesc.of("io.github.freya022.botcommands.internal.core.BContextImpl") - -private val CD_Function0 = ClassDesc.of("kotlin.jvm.functions.Function0") - object CodeBuilderTest { @MethodSource("Test createLambda") @@ -37,15 +21,15 @@ object CodeBuilderTest { DynamicCallSiteDesc.of( lambdaMetafactoryDesc, "get", - MethodTypeDesc.of(classDesc>(), CD_JDABuilder), - MethodTypeDesc.of(CD_Object), + MethodTypeDesc.of(CD_Supplier, CD_JDABuilder), + MethodTypeDesc.of(ConstantDescs.CD_Object), MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, CD_JDABuilder, "doBuild", MethodTypeDesc.of(CD_JDA) ), - MethodTypeDesc.of(CD_Object), + MethodTypeDesc.of(ConstantDescs.CD_Object), ), createLambda( interfaceMethod = Supplier<*>::get, @@ -62,21 +46,21 @@ object CodeBuilderTest { DynamicCallSiteDesc.of( lambdaMetafactoryDesc, "run", - MethodTypeDesc.of(classDesc(), CD_JDAService, CD_BReadyEvent, CD_IEventManager), - MethodTypeDesc.of(CD_void), + MethodTypeDesc.of(CD_Runnable, CD_JDAService, CD_BReadyEvent, CD_IEventManager), + MethodTypeDesc.of(ConstantDescs.CD_void), MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, CD_JDAService, $$"lambda$onReadyEvent$BotCommands$withBuilderSession", - MethodTypeDesc.of(CD_void, CD_BReadyEvent, CD_IEventManager) + MethodTypeDesc.of(ConstantDescs.CD_void, CD_BReadyEvent, CD_IEventManager) ), - MethodTypeDesc.of(CD_void), + MethodTypeDesc.of(ConstantDescs.CD_void), ), createLambda( interfaceMethod = Runnable::run, targetType = CD_JDAService, targetMethod = $$"lambda$onReadyEvent$BotCommands$withBuilderSession", - targetMethodReturnType = CD_void, + targetMethodReturnType = ConstantDescs.CD_void, targetMethodArguments = listOf(), capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), isStatic = false @@ -87,21 +71,21 @@ object CodeBuilderTest { DynamicCallSiteDesc.of( lambdaMetafactoryDesc, "run", - MethodTypeDesc.of(classDesc(), CD_JDAImpl), - MethodTypeDesc.of(CD_void), + MethodTypeDesc.of(CD_Runnable, CD_JDAImpl), + MethodTypeDesc.of(ConstantDescs.CD_void), MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, CD_JDAImpl, "doShutdown", - MethodTypeDesc.of(CD_void) + MethodTypeDesc.of(ConstantDescs.CD_void) ), - MethodTypeDesc.of(CD_void), + MethodTypeDesc.of(ConstantDescs.CD_void), ), createLambda( interfaceMethod = Runnable::run, targetType = CD_JDAImpl, targetMethod = "doShutdown", - targetMethodReturnType = CD_void, + targetMethodReturnType = ConstantDescs.CD_void, targetMethodArguments = listOf(), capturedTypes = listOf(), isStatic = false @@ -112,21 +96,21 @@ object CodeBuilderTest { DynamicCallSiteDesc.of( lambdaMetafactoryDesc, "run", - MethodTypeDesc.of(classDesc(), CD_BContextImpl, CD_Function0), - MethodTypeDesc.of(CD_void), + MethodTypeDesc.of(CD_Runnable, CD_BContextImpl, CD_Function0), + MethodTypeDesc.of(ConstantDescs.CD_void), MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.VIRTUAL, CD_BContextImpl, "doScheduleShutdownSignal", - MethodTypeDesc.of(CD_void, CD_Function0) + MethodTypeDesc.of(ConstantDescs.CD_void, CD_Function0) ), - MethodTypeDesc.of(CD_void), + MethodTypeDesc.of(ConstantDescs.CD_void), ), createLambda( interfaceMethod = Runnable::run, targetType = CD_BContextImpl, targetMethod = "doScheduleShutdownSignal", - targetMethodReturnType = CD_void, + targetMethodReturnType = ConstantDescs.CD_void, targetMethodArguments = listOf(), capturedTypes = listOf(CD_Function0), isStatic = false From e1de668e7fb3dbb436734919ba6576392a27d922 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:13:42 +0200 Subject: [PATCH 62/68] JDA cache: Remove print --- .../kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt index 2d11297..9f46ef8 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/Agent.kt @@ -10,7 +10,6 @@ object Agent { @JvmStatic fun premain(agentArgs: String?, inst: Instrumentation) { - println("Agent args: $agentArgs") inst.addTransformer(JDABuilderTransformer) inst.addTransformer(JDAServiceTransformer) inst.addTransformer(BContextImplTransformer) From 870489e0a510c3139aae595fa484894fdcfe8915 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:52:16 +0200 Subject: [PATCH 63/68] JDA cache: Refactor transformers - Make a ClassTransformer using context parameters - Add MethodModel#toFullyQualifiedString - Add MethodModel#matches(name, signature) - Add ClassModel#findMethod(name, signature) - Add Int#withVisibility(AccessFlag?) - Use constants for transformation/move targets - Make sure the targets are present before transforming - Log transformations with what they do - Rename classes to what they do --- restarter-jda-cache/build.gradle.kts | 1 + .../transformer/BContextImplTransformer.kt | 49 ++--- .../jda/cache/transformer/CodeBuilderUtils.kt | 5 + .../transformer/ContextualClassTransform.kt | 25 +++ .../transformer/JDABuilderTransformer.kt | 96 +++++---- .../cache/transformer/JDAImplTransformer.kt | 150 +++++++------- .../transformer/JDAServiceTransformer.kt | 183 +++++++++--------- .../jda/cache/transformer/TransformUtils.kt | 42 ++++ 8 files changed, 311 insertions(+), 240 deletions(-) create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ContextualClassTransform.kt create mode 100644 restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt diff --git a/restarter-jda-cache/build.gradle.kts b/restarter-jda-cache/build.gradle.kts index 7426527..23aea36 100644 --- a/restarter-jda-cache/build.gradle.kts +++ b/restarter-jda-cache/build.gradle.kts @@ -26,6 +26,7 @@ java { kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_24 + freeCompilerArgs.add("-Xcontext-parameters") } } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt index 184bb6d..5525061 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt @@ -2,7 +2,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* -import java.lang.classfile.ClassFile.* import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void import java.lang.constant.MethodTypeDesc @@ -16,40 +15,32 @@ internal object BContextImplTransformer : AbstractClassFileTransformer("io/githu val classFile = ClassFile.of() val classModel = classFile.parse(classData) - return classFile.transformClass(classModel, - ScheduleShutdownSignalTransform(classModel) + return classFile.transformClass( + classModel, + DeferScheduleShutdownSignalTransform(classModel) ) } } -private class ScheduleShutdownSignalTransform(private val classModel: ClassModel) : ClassTransform { +private class DeferScheduleShutdownSignalTransform(private val classModel: ClassModel) : ContextualClassTransform { - override fun atStart(classBuilder: ClassBuilder) { - val targetMethodModel = classModel.methods().firstOrNull(::isTargetMethod) - ?: error("Could not find BContextImpl#${TARGET_METHOD_NAME}${TARGET_METHOD_SIGNATURE}") - - logger.trace { "Transferring BContextImpl#${TARGET_METHOD_NAME}${TARGET_METHOD_SIGNATURE} into $LAMBDA_NAME" } - - classBuilder.withMethodBody( - LAMBDA_NAME, - MethodTypeDesc.ofDescriptor(TARGET_METHOD_SIGNATURE), - ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL - ) { codeBuilder -> - val codeModel = targetMethodModel.code().get() - codeModel.forEach { codeBuilder.with(it) } - } + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + val targetMethod = classModel.findMethod(TARGET_NAME, TARGET_SIGNATURE) + logger.trace { "Moving ${targetMethod.toFullyQualifiedString()} to '$NEW_NAME'" } + targetMethod.transferCodeTo(NEW_NAME) } - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) - if (!isTargetMethod(methodModel)) return classBuilder.retain(classElement) - - logger.trace { "Transforming BContextImpl#${TARGET_METHOD_NAME}${TARGET_METHOD_SIGNATURE}" } + if (!methodModel.matches(TARGET_NAME, TARGET_SIGNATURE)) return classBuilder.retain(classElement) + logger.trace { "Transforming BContextImpl#${TARGET_NAME}${TARGET_SIGNATURE} to defer shutdown signal scheduling" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) - methodBuilder.withFlags(*(methodModel.flags().flags() - AccessFlag.PRIVATE + AccessFlag.PUBLIC).toTypedArray()) + methodBuilder.withFlags(methodModel.flags().flagsMask().withVisibility(AccessFlag.PUBLIC)) methodBuilder.withCode { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() @@ -95,16 +86,10 @@ private class ScheduleShutdownSignalTransform(private val classModel: ClassModel } } - private fun isTargetMethod(methodModel: MethodModel): Boolean { - if (!methodModel.methodName().equalsString(TARGET_METHOD_NAME)) return false - if (!methodModel.methodType().equalsString(TARGET_METHOD_SIGNATURE)) return false - return true - } - private companion object { - const val TARGET_METHOD_NAME = "scheduleShutdownSignal" - const val TARGET_METHOD_SIGNATURE = "(Lkotlin/jvm/functions/Function0;)V" + const val TARGET_NAME = "scheduleShutdownSignal" + const val TARGET_SIGNATURE = "(Lkotlin/jvm/functions/Function0;)V" - const val LAMBDA_NAME = "doScheduleShutdownSignal" + const val NEW_NAME = "doScheduleShutdownSignal" } } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt index 454e742..725d44e 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt @@ -2,6 +2,7 @@ package dev.freya02.botcommands.restart.jda.cache.transformer import java.lang.classfile.ClassFileBuilder import java.lang.classfile.ClassFileElement +import java.lang.classfile.CodeBuilder import java.lang.constant.* import java.lang.constant.ConstantDescs.CD_String import java.lang.invoke.* @@ -14,6 +15,10 @@ internal fun ClassFileBuilder.retain(element: E) { with(element) } +internal fun CodeBuilder.ldc(string: String) { + ldc(string as java.lang.String) +} + internal val lambdaMetafactoryDesc = MethodHandleDesc.ofMethod( DirectMethodHandleDesc.Kind.STATIC, classDesc(), diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ContextualClassTransform.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ContextualClassTransform.kt new file mode 100644 index 0000000..e857339 --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ContextualClassTransform.kt @@ -0,0 +1,25 @@ +package dev.freya02.botcommands.restart.jda.cache.transformer + +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassElement +import java.lang.classfile.ClassTransform + +internal interface ContextualClassTransform : ClassTransform { + + override fun atStart(builder: ClassBuilder): Unit = context(builder) { atStartContextual() } + + context(classBuilder: ClassBuilder) + fun atStartContextual() { } + + + override fun atEnd(builder: ClassBuilder): Unit = context(builder) { atEndContextual() } + + context(classBuilder: ClassBuilder) + fun atEndContextual() { } + + + override fun accept(builder: ClassBuilder, element: ClassElement): Unit = context(builder) { acceptContextual(element) } + + context(classBuilder: ClassBuilder) + fun acceptContextual(classElement: ClassElement) { } +} \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 381f05e..2701422 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -16,30 +16,30 @@ internal object JDABuilderTransformer : AbstractClassFileTransformer("net/dv8tio override fun transform(classData: ByteArray): ByteArray { val classFile = ClassFile.of() + val classModel = classFile.parse(classData) + return classFile.transformClass( - classFile.parse(classData), - PublicInstanceMethodTransform() - .andThen(JDABuilderConstructorTransform()) - .andThen(BuildTransform()) + classModel, + CaptureSetterParametersTransform() + .andThen(CaptureConstructorParametersTransform(classModel)) + .andThen(DeferBuildAndSetBufferingEventManagerTransform(classModel)) ) } } -private class JDABuilderConstructorTransform : ClassTransform { +private class CaptureConstructorParametersTransform(private val classModel: ClassModel) : ContextualClassTransform { - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { - val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) - if (!methodModel.methodName().equalsString("")) return classBuilder.retain(classElement) - - val methodType = methodModel.methodTypeSymbol() - if (methodType.parameterList() != listOf(CD_String, CD_int)) { - // TODO not sure about the exception model yet, - // maybe we should just disable the JDA cache instead of being completely incompatible - throw IllegalArgumentException("Incompatible JDABuilder constructor: $methodType") - } + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + classModel.findMethod(TARGET_NAME, TARGET_SIGNATURE) + } - logger.trace { "Transforming JDABuilder's constructor" } + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.matches(TARGET_NAME, TARGET_SIGNATURE)) return classBuilder.retain(classElement) + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to capture parameters" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) @@ -64,30 +64,26 @@ private class JDABuilderConstructorTransform : ClassTransform { } } } -} - -private class BuildTransform : ClassTransform { - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { - val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) - if (!methodModel.methodName().equalsString("build")) return classBuilder.retain(classElement) + private companion object { + const val TARGET_NAME = "" + const val TARGET_SIGNATURE = "(Ljava/lang/String;I)V" + } +} - val methodType = methodModel.methodTypeSymbol() - if (methodType.parameterList() != emptyList()) { - // TODO not sure about the exception model yet, - // maybe we should just disable the JDA cache instead of being completely incompatible - throw IllegalArgumentException("Incompatible JDABuilder build method: $methodType") - } +private class DeferBuildAndSetBufferingEventManagerTransform(private val classModel: ClassModel) : ContextualClassTransform { - logger.trace { "Transforming JDABuilder's build() method" } + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + val targetMethod = classModel.findMethod(TARGET_NAME, TARGET_SIGNATURE) - val newBuildMethodName = "doBuild" + logger.trace { "Adding JDABuilder#${NEW_NAME}() to set an event manager and build" } classBuilder.withMethod( - newBuildMethodName, + NEW_NAME, MethodTypeDesc.of(CD_JDA), ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL ) { methodBuilder -> - val codeModel = methodModel.code().get() + val codeModel = targetMethod.code().get() methodBuilder.withCode { codeBuilder -> val thisSlot = codeBuilder.receiverSlot() @@ -125,12 +121,19 @@ private class BuildTransform : ClassTransform { codeBuilder.new_(CD_IllegalStateException) codeBuilder.dup() - codeBuilder.ldc("The event manager must be set using the one provided in JDAService#createJDA" as java.lang.String) + codeBuilder.ldc("The event manager must be set using the one provided in JDAService#createJDA") codeBuilder.invokespecial(CD_IllegalStateException, "", MethodTypeDesc.of(CD_void, CD_String)) codeBuilder.athrow() } } + } + + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.matches(TARGET_NAME, TARGET_SIGNATURE)) return classBuilder.retain(classElement) + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to defer calls" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) @@ -147,7 +150,7 @@ private class BuildTransform : ClassTransform { codeBuilder.invokedynamic(createLambda( interfaceMethod = Supplier<*>::get, targetType = CD_JDABuilder, - targetMethod = newBuildMethodName, + targetMethod = NEW_NAME, targetMethodReturnType = CD_JDA, targetMethodArguments = listOf(), capturedTypes = emptyList(), @@ -171,23 +174,30 @@ private class BuildTransform : ClassTransform { } } } + + private companion object { + const val TARGET_NAME = "build" + const val TARGET_SIGNATURE = "()Lnet/dv8tion/jda/api/JDA;" + + const val NEW_NAME = "doBuild" + } } -private class PublicInstanceMethodTransform : ClassTransform { +private class CaptureSetterParametersTransform : ContextualClassTransform { private val builderSessionMethods: Set = ClassFile.of() .parse(JDABuilderConfiguration::class.java.getResourceAsStream("JDABuilderConfiguration.class")!!.readAllBytes()) .methods() .mapTo(hashSetOf(), ::MethodDesc) - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) if (!methodModel.flags().has(AccessFlag.PUBLIC)) return classBuilder.retain(classElement) if (methodModel.flags().has(AccessFlag.STATIC)) return classBuilder.retain(classElement) if (methodModel.methodName().stringValue() == "build") return classBuilder.retain(classElement) - logger.trace { "Transforming ${methodModel.methodName().stringValue()}" } - + // Log is done later classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) @@ -200,10 +210,10 @@ private class PublicInstanceMethodTransform : ClassTransform { codeBuilder.invokevirtual(CD_JDABuilderSession, "getConfiguration", MethodTypeDesc.of(CD_JDABuilderConfiguration)) codeBuilder.astore(builderConfigurationSlot) + val methodName = methodModel.methodName().stringValue() if (hasBuilderSessionMethod) { - logger.trace { "Registering $methodModel as a cache-compatible method" } + logger.trace { "Registering ${methodModel.toFullyQualifiedString()} as a cache-compatible method" } - val methodName = methodModel.methodName().stringValue() // Set return type to "void" because our method won't return JDABuilder, and it doesn't matter anyway val methodType = methodModel.methodTypeSymbol().changeReturnType(CD_void) @@ -216,13 +226,13 @@ private class PublicInstanceMethodTransform : ClassTransform { } codeBuilder.invokevirtual(CD_JDABuilderConfiguration, methodName, methodType) } else { - logger.trace { "Skipping $methodModel as it does not have an equivalent method handler" } + logger.trace { "Skipping ${methodModel.toFullyQualifiedString()} as it does not have an equivalent method handler" } - val signature = methodModel.methodName().stringValue() + "(${methodModel.methodTypeSymbol().parameterList().joinToString { it.displayName() }})" + val signature = methodName + "(${methodModel.methodTypeSymbol().parameterList().joinToString { it.displayName() }})" // configuration.markUnsupportedValue() codeBuilder.aload(builderConfigurationSlot) - codeBuilder.ldc(signature as java.lang.String) + codeBuilder.ldc(signature) codeBuilder.invokevirtual(CD_JDABuilderConfiguration, "markUnsupportedValue", MethodTypeDesc.of(CD_void, CD_String)) } diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 98678a7..8b9ff85 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -4,51 +4,52 @@ import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* import java.lang.classfile.instruction.InvokeInstruction -import java.lang.constant.ClassDesc import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void import java.lang.constant.MethodTypeDesc private val logger = KotlinLogging.logger { } -private const val cacheKeyFieldName = "cacheKey" - internal object JDAImplTransformer : AbstractClassFileTransformer("net/dv8tion/jda/internal/JDAImpl") { override fun transform(classData: ByteArray): ByteArray { val classFile = ClassFile.of() val classModel = classFile.parse(classData) - return classFile.build(classModel.thisClass().asSymbol()) { classBuilder -> - classBuilder.withField(cacheKeyFieldName, CD_String, ACC_PRIVATE or ACC_FINAL) - - classBuilder.withMethod("getBuilderSession", MethodTypeDesc.of(CD_JDABuilderSession), ACC_PUBLIC) { methodBuilder -> - methodBuilder.withCode { codeBuilder -> - codeBuilder.aload(codeBuilder.receiverSlot()) - codeBuilder.getfield(CD_JDAImpl, cacheKeyFieldName, CD_String) - codeBuilder.invokestatic(CD_JDABuilderSession, "getSession", MethodTypeDesc.of(CD_JDABuilderSession, CD_String)) - codeBuilder.areturn() - } - } - - val transform = CaptureSessionKeyTransform() - .andThen(ShutdownTransform()) - .andThen(ShutdownNowTransform()) + return classFile.transformClass( + classModel, + CaptureSessionKeyTransform() + .andThen(DeferShutdownTransform(classModel)) + .andThen(DeferShutdownNowTransform(classModel)) .andThen(AwaitShutdownTransform()) - classBuilder.transform(classModel, transform) - } + ) } } -private class CaptureSessionKeyTransform : ClassTransform { +private class CaptureSessionKeyTransform : ContextualClassTransform { - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { - val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) - if (!methodModel.methodName().equalsString("")) return classBuilder.retain(classElement) + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + logger.trace { "Adding JDAImpl#${CACHE_KEY_NAME}" } + classBuilder.withField(CACHE_KEY_NAME, CD_String, ACC_PRIVATE or ACC_FINAL) - // No need to check the signature, we can assign the field in all constructors + logger.trace { "Adding JDAImpl#getBuilderSession()" } + classBuilder.withMethod("getBuilderSession", MethodTypeDesc.of(CD_JDABuilderSession), ACC_PUBLIC) { methodBuilder -> + methodBuilder.withCode { codeBuilder -> + codeBuilder.aload(codeBuilder.receiverSlot()) + codeBuilder.getfield(CD_JDAImpl, CACHE_KEY_NAME, CD_String) + codeBuilder.invokestatic(CD_JDABuilderSession, "getSession", MethodTypeDesc.of(CD_JDABuilderSession, CD_String)) + codeBuilder.areturn() + } + } + } - logger.trace { "Transforming (one of) JDAImpl's constructor" } + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + // No need to check the signature, we can assign the field in all constructors + if (!methodModel.methodName().equalsString("")) return classBuilder.retain(classElement) + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to store the session key" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> val codeModel = methodElement as? CodeModel ?: return@transformMethod methodBuilder.retain(methodElement) @@ -59,44 +60,35 @@ private class CaptureSessionKeyTransform : ClassTransform { codeBuilder.aload(thisSlot) codeBuilder.invokestatic(CD_JDABuilderSession, "currentSession", MethodTypeDesc.of(CD_JDABuilderSession)) codeBuilder.invokevirtual(CD_JDABuilderSession, "getKey", MethodTypeDesc.of(CD_String)) - codeBuilder.putfield(CD_JDAImpl, cacheKeyFieldName, CD_String) + codeBuilder.putfield(CD_JDAImpl, CACHE_KEY_NAME, CD_String) // Add existing instructions codeModel.forEach { codeBuilder.with(it) } } } } -} - -private class ShutdownTransform : ClassTransform { - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { - val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) - if (!methodModel.methodName().equalsString("shutdown")) return classBuilder.retain(classElement) + private companion object { + const val CACHE_KEY_NAME = "cacheKey" + } +} - val methodType = methodModel.methodTypeSymbol() - if (methodType.parameterList() != emptyList()) { - // TODO not sure about the exception model yet, - // maybe we should just disable the JDA cache instead of being completely incompatible - throw IllegalArgumentException("Incompatible JDAImpl shutdown method: $methodType") - } +private class DeferShutdownTransform(private val classModel: ClassModel) : ContextualClassTransform { - logger.trace { "Transforming JDABuilder's shutdown() method" } + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + val targetMethod = classModel.findMethod(TARGET_NAME, TARGET_SIGNATURE) - val newShutdownMethodName = "doShutdown" - classBuilder.withMethod( - newShutdownMethodName, - MethodTypeDesc.of(CD_void), - ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL - ) { methodBuilder -> - val codeModel = methodModel.code().get() + logger.trace { "Moving ${targetMethod.toFullyQualifiedString()} to '$NEW_NAME'" } + targetMethod.transferCodeTo(NEW_NAME) + } - methodBuilder.withCode { codeBuilder -> - // Move the shutdown() code to doShutdown() - codeModel.forEach { codeBuilder.with(it) } - } - } + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.matches(TARGET_NAME, TARGET_SIGNATURE)) return classBuilder.retain(classElement) + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to defer execution" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) @@ -111,7 +103,7 @@ private class ShutdownTransform : ClassTransform { codeBuilder.invokedynamic(createLambda( interfaceMethod = Runnable::run, targetType = CD_JDAImpl, - targetMethod = newShutdownMethodName, + targetMethod = NEW_NAME, targetMethodReturnType = CD_void, targetMethodArguments = listOf(), capturedTypes = listOf(), @@ -134,30 +126,28 @@ private class ShutdownTransform : ClassTransform { } } } -} -private class ShutdownNowTransform : ClassTransform { + private companion object { + const val TARGET_NAME = "shutdown" + const val TARGET_SIGNATURE = "()V" - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { - val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) - if (!methodModel.methodName().equalsString("shutdownNow")) return classBuilder.retain(classElement) + const val NEW_NAME = "doShutdown" + } +} - val methodType = methodModel.methodTypeSymbol() - if (methodType.parameterList() != emptyList()) { - // TODO not sure about the exception model yet, - // maybe we should just disable the JDA cache instead of being completely incompatible - throw IllegalArgumentException("Incompatible JDAImpl shutdownNow method: $methodType") - } +private class DeferShutdownNowTransform(private val classModel: ClassModel) : ContextualClassTransform { - logger.trace { "Transforming JDABuilder's shutdownNow() method" } + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + val targetMethod = classModel.findMethod(TARGET_NAME, TARGET_SIGNATURE) - val newShutdownMethodName = "doShutdownNow" + logger.trace { "Moving ${targetMethod.toFullyQualifiedString()} to $NEW_NAME, replacing shutdown() with doShutdown()" } classBuilder.withMethod( - newShutdownMethodName, + NEW_NAME, MethodTypeDesc.of(CD_void), ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL ) { methodBuilder -> - val codeModel = methodModel.code().get() + val codeModel = targetMethod.code().get() methodBuilder.withCode { codeBuilder -> // Move the shutdownNow() code to doShutdownNow() @@ -173,7 +163,14 @@ private class ShutdownNowTransform : ClassTransform { } } } + } + + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.matches(TARGET_NAME, TARGET_SIGNATURE)) return classBuilder.retain(classElement) + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to defer execution" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) @@ -188,7 +185,7 @@ private class ShutdownNowTransform : ClassTransform { codeBuilder.invokedynamic(createLambda( interfaceMethod = Runnable::run, targetType = CD_JDAImpl, - targetMethod = newShutdownMethodName, + targetMethod = NEW_NAME, targetMethodReturnType = CD_void, targetMethodArguments = listOf(), capturedTypes = listOf(), @@ -211,16 +208,23 @@ private class ShutdownNowTransform : ClassTransform { } } } + + private companion object { + const val TARGET_NAME = "shutdownNow" + const val TARGET_SIGNATURE = "()V" + + const val NEW_NAME = "doShutdownNow" + } } -private class AwaitShutdownTransform : ClassTransform { +private class AwaitShutdownTransform : ContextualClassTransform { - override fun accept(classBuilder: ClassBuilder, classElement: ClassElement) { + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) if (!methodModel.methodName().equalsString("awaitShutdown")) return classBuilder.retain(classElement) - logger.trace { "Transforming (one of) JDA's awaitShutdown method" } - + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to immediately return" } classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index 535095b..820155a 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -1,11 +1,7 @@ package dev.freya02.botcommands.restart.jda.cache.transformer import io.github.oshai.kotlinlogging.KotlinLogging -import java.lang.classfile.ClassFile -import java.lang.classfile.ClassFile.* -import java.lang.classfile.CodeModel -import java.lang.classfile.MethodModel -import java.lang.classfile.TypeKind +import java.lang.classfile.* import java.lang.constant.ConstantDescs.CD_String import java.lang.constant.ConstantDescs.CD_void import java.lang.constant.MethodTypeDesc @@ -14,99 +10,102 @@ private val logger = KotlinLogging.logger { } internal object JDAServiceTransformer : AbstractClassFileTransformer("io/github/freya022/botcommands/api/core/JDAService") { - private const val TARGET_METHOD_NAME = $$"onReadyEvent$BotCommands" - private const val TARGET_METHOD_SIGNATURE = "(Lio/github/freya022/botcommands/api/core/events/BReadyEvent;Lnet/dv8tion/jda/api/hooks/IEventManager;)V" - override fun transform(classData: ByteArray): ByteArray { val classFile = ClassFile.of() - var hasModifiedMethod = false - val newBytes = classFile.transformClass(classFile.parse(classData)) { classBuilder, classElement -> - val methodModel = classElement as? MethodModel ?: return@transformClass classBuilder.retain(classElement) - if (!methodModel.methodName().equalsString(TARGET_METHOD_NAME)) return@transformClass classBuilder.retain(classElement) - if (!methodModel.methodType().equalsString(TARGET_METHOD_SIGNATURE)) return@transformClass classBuilder.retain(classElement) - - hasModifiedMethod = true - - // Put the original code of onReadyEvent in the lambda, - // it will be fired by JDABuilderSession.withBuilderSession in onReadyEvent - val lambdaName = $$"lambda$onReadyEvent$BotCommands$withBuilderSession" - classBuilder.withMethodBody( - lambdaName, - MethodTypeDesc.ofDescriptor(TARGET_METHOD_SIGNATURE), - ACC_PRIVATE or ACC_SYNTHETIC or ACC_FINAL - ) { codeBuilder -> - - val codeModel = methodModel.code().get() - codeModel.forEach { codeBuilder.with(it) } - } + val classModel = classFile.parse(classData) + + return classFile.transformClass( + classModel, + WrapOnReadyEventWithJDABuilderSessionTransform(classModel) + ) + } +} + +private class WrapOnReadyEventWithJDABuilderSessionTransform(private val classModel: ClassModel) : ContextualClassTransform { - classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> - if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) - - methodBuilder.withCode { codeBuilder -> - val thisSlot = codeBuilder.receiverSlot() - - val readyEventSlot = codeBuilder.parameterSlot(0) - val eventManagerSlot = codeBuilder.parameterSlot(1) - - val contextSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - val sessionKeySlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - val sessionRunnableSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) - - // var context = event.getContext() - // We could inline this to avoid a successive store/load, - // but I think using variables is probably a better practice, let's leave the optimization to the VM - codeBuilder.aload(readyEventSlot) - codeBuilder.invokevirtual(CD_BReadyEvent, "getContext", MethodTypeDesc.of(CD_BContext)) - codeBuilder.astore(contextSlot) - - // var key = JDABuilderSession.getCacheKey(context) - codeBuilder.aload(contextSlot) - codeBuilder.invokestatic(CD_JDABuilderSession, "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) - codeBuilder.astore(sessionKeySlot) - - // THE KEY IS NULLABLE - // If it is, then don't make a session - val nullKeyLabel = codeBuilder.newLabel() - - // if (key == null) -> nullKeyLabel - codeBuilder.aload(sessionKeySlot) - codeBuilder.ifnull(nullKeyLabel) - - // Runnable sessionRunnable = () -> [lambdaName](event, eventManager) - codeBuilder.aload(thisSlot) - codeBuilder.aload(readyEventSlot) - codeBuilder.aload(eventManagerSlot) - codeBuilder.invokedynamic(createLambda( - interfaceMethod = Runnable::run, - targetType = CD_JDAService, - targetMethod = lambdaName, - targetMethodReturnType = CD_void, - targetMethodArguments = listOf(), - capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), - isStatic = false - )) - codeBuilder.astore(sessionRunnableSlot) - - // JDABuilderSession.withBuilderSession(key, sessionRunnable) - codeBuilder.aload(sessionKeySlot) - codeBuilder.aload(sessionRunnableSlot) - codeBuilder.invokestatic(CD_JDABuilderSession, "withBuilderSession", MethodTypeDesc.of(CD_void, CD_String, CD_Runnable)) - - // Required - codeBuilder.return_() - - // nullKeyLabel code - codeBuilder.labelBinding(nullKeyLabel) - codeBuilder.return_() - } + context(classBuilder: ClassBuilder) + override fun atStartContextual() { + // Put the original code of onReadyEvent in the lambda, + // it will be fired by JDABuilderSession.withBuilderSession in onReadyEvent + val targetMethod = classModel.findMethod(TARGET_NAME, TARGET_SIGNATURE) + + logger.trace { "Moving ${targetMethod.toFullyQualifiedString()} to '$NEW_NAME'" } + targetMethod.transferCodeTo(NEW_NAME) + } + + context(classBuilder: ClassBuilder) + override fun acceptContextual(classElement: ClassElement) { + val methodModel = classElement as? MethodModel ?: return classBuilder.retain(classElement) + if (!methodModel.matches(TARGET_NAME, TARGET_SIGNATURE)) return classBuilder.retain(classElement) + + logger.trace { "Transforming ${methodModel.toFullyQualifiedString()} to wrap the code in a build session" } + classBuilder.transformMethod(methodModel) { methodBuilder, methodElement -> + if (methodElement !is CodeModel) return@transformMethod methodBuilder.retain(methodElement) + + methodBuilder.withCode { codeBuilder -> + val thisSlot = codeBuilder.receiverSlot() + + val readyEventSlot = codeBuilder.parameterSlot(0) + val eventManagerSlot = codeBuilder.parameterSlot(1) + + val contextSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val sessionKeySlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + val sessionRunnableSlot = codeBuilder.allocateLocal(TypeKind.REFERENCE) + + // var context = event.getContext() + // We could inline this to avoid a successive store/load, + // but I think using variables is probably a better practice, let's leave the optimization to the VM + codeBuilder.aload(readyEventSlot) + codeBuilder.invokevirtual(CD_BReadyEvent, "getContext", MethodTypeDesc.of(CD_BContext)) + codeBuilder.astore(contextSlot) + + // var key = JDABuilderSession.getCacheKey(context) + codeBuilder.aload(contextSlot) + codeBuilder.invokestatic(CD_JDABuilderSession, "getCacheKey", MethodTypeDesc.of(CD_String, CD_BContext)) + codeBuilder.astore(sessionKeySlot) + + // THE KEY IS NULLABLE + // If it is, then don't make a session + val nullKeyLabel = codeBuilder.newLabel() + + // if (key == null) -> nullKeyLabel + codeBuilder.aload(sessionKeySlot) + codeBuilder.ifnull(nullKeyLabel) + + // Runnable sessionRunnable = () -> [lambdaName](event, eventManager) + codeBuilder.aload(thisSlot) + codeBuilder.aload(readyEventSlot) + codeBuilder.aload(eventManagerSlot) + codeBuilder.invokedynamic(createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAService, + targetMethod = NEW_NAME, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), + isStatic = false + )) + codeBuilder.astore(sessionRunnableSlot) + + // JDABuilderSession.withBuilderSession(key, sessionRunnable) + codeBuilder.aload(sessionKeySlot) + codeBuilder.aload(sessionRunnableSlot) + codeBuilder.invokestatic(CD_JDABuilderSession, "withBuilderSession", MethodTypeDesc.of(CD_void, CD_String, CD_Runnable)) + + // Required + codeBuilder.return_() + + // nullKeyLabel code + codeBuilder.labelBinding(nullKeyLabel) + codeBuilder.return_() } } + } - check(hasModifiedMethod) { - "Could not find JDAService#onReadyEvent(BReadyEvent, IEventManager)" - } + private companion object { + const val TARGET_NAME = $$"onReadyEvent$BotCommands" + const val TARGET_SIGNATURE = "(Lio/github/freya022/botcommands/api/core/events/BReadyEvent;Lnet/dv8tion/jda/api/hooks/IEventManager;)V" - return newBytes + const val NEW_NAME = $$"lambda$onReadyEvent$BotCommands$withBuilderSession" } } \ No newline at end of file diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt new file mode 100644 index 0000000..9cee6ec --- /dev/null +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt @@ -0,0 +1,42 @@ +package dev.freya02.botcommands.restart.jda.cache.transformer + +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassFile.ACC_SYNTHETIC +import java.lang.classfile.ClassModel +import java.lang.classfile.MethodModel +import java.lang.reflect.AccessFlag +import kotlin.jvm.optionals.getOrNull + +internal fun Int.withVisibility(visibility: AccessFlag?): Int { + var flags = this + flags = flags and (AccessFlag.PUBLIC.mask() or AccessFlag.PROTECTED.mask() or AccessFlag.PRIVATE.mask()).inv() + if (visibility != null) // null = package-private + flags = flags or visibility.mask() + return flags +} + +internal fun MethodModel.matches(name: String, signature: String): Boolean { + return methodName().equalsString(name) && methodType().equalsString(signature) +} + +internal fun ClassModel.findMethod(name: String, signature: String): MethodModel { + return this.methods().firstOrNull { it.matches(name, signature) } + ?: error("Could not find ${this.thisClass().name().stringValue()}#$name$signature") +} + +context(classBuilder: ClassBuilder) +internal fun MethodModel.transferCodeTo(targetMethodName: String, visibility: AccessFlag = AccessFlag.PRIVATE) { + classBuilder.withMethodBody( + classBuilder.constantPool().utf8Entry(targetMethodName), + methodType(), + visibility.mask() or ACC_SYNTHETIC // Synthetic so this doesn't require a mock + ) { codeBuilder -> + val codeModel = code().orElseThrow { IllegalArgumentException("Method ${this.toFullyQualifiedString()} does not have code") } + codeModel.forEach { codeBuilder.with(it) } + } +} + +internal fun MethodModel.toFullyQualifiedString(): String { + val className = parent().getOrNull()?.thisClass()?.asSymbol()?.displayName() ?: "" + return "$className#${methodName().stringValue()}${methodTypeSymbol().displayDescriptor()}" +} \ No newline at end of file From 214e8731cf9177a8a3f35010a9803f44fc1bd335 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:56:03 +0200 Subject: [PATCH 64/68] JDA cache: Move utils and tests to package --- .../botcommands/restart/jda/cache/JDABuilderSession.kt | 1 + .../restart/jda/cache/transformer/BContextImplTransformer.kt | 1 + .../restart/jda/cache/transformer/ClassDescriptors.kt | 1 + .../restart/jda/cache/transformer/JDABuilderTransformer.kt | 1 + .../restart/jda/cache/transformer/JDAImplTransformer.kt | 1 + .../restart/jda/cache/transformer/JDAServiceTransformer.kt | 1 + .../jda/cache/transformer/{ => utils}/CodeBuilderUtils.kt | 2 +- .../jda/cache/transformer/{ => utils}/TransformUtils.kt | 2 +- .../restart/jda/cache/{utils.kt => utils/JvmUtils.kt} | 2 +- .../cache/{ => transformer}/BContextImplTransformerTest.kt | 2 +- .../jda/cache/{ => transformer}/JDABuilderTransformerTest.kt | 5 ++++- .../jda/cache/{ => transformer}/JDAImplTransformerTest.kt | 3 ++- .../jda/cache/{ => transformer}/JDAServiceTransformerTest.kt | 3 ++- .../{CodeBuilderTest.kt => utils/CodeBuilderUtilsTest.kt} | 5 +++-- 14 files changed, 21 insertions(+), 9 deletions(-) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/{ => utils}/CodeBuilderUtils.kt (98%) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/{ => utils}/TransformUtils.kt (96%) rename restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/{utils.kt => utils/JvmUtils.kt} (78%) rename restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/BContextImplTransformerTest.kt (86%) rename restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/JDABuilderTransformerTest.kt (94%) rename restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/JDAImplTransformerTest.kt (83%) rename restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/{ => transformer}/JDAServiceTransformerTest.kt (96%) rename restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/{CodeBuilderTest.kt => utils/CodeBuilderUtilsTest.kt} (96%) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt index 6405760..d430112 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderSession.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache +import dev.freya02.botcommands.restart.jda.cache.utils.isJvmShuttingDown import io.github.freya022.botcommands.api.core.BContext import io.github.oshai.kotlinlogging.KotlinLogging import net.dv8tion.jda.api.JDA diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt index 5525061..ed8f8dd 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.transformer.utils.* import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.constant.ConstantDescs.CD_String diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt index 2e41080..4ddf7b1 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/ClassDescriptors.kt @@ -3,6 +3,7 @@ package dev.freya02.botcommands.restart.jda.cache.transformer import dev.freya02.botcommands.restart.jda.cache.BufferingEventManager import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession +import dev.freya02.botcommands.restart.jda.cache.transformer.utils.classDesc import org.intellij.lang.annotations.Language import java.lang.constant.ClassDesc diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 2701422..1bb8d29 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -1,6 +1,7 @@ package dev.freya02.botcommands.restart.jda.cache.transformer import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration +import dev.freya02.botcommands.restart.jda.cache.transformer.utils.* import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 8b9ff85..12f009c 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.transformer.utils.* import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.classfile.ClassFile.* diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index 820155a..cb3f09a 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -1,5 +1,6 @@ package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.transformer.utils.* import io.github.oshai.kotlinlogging.KotlinLogging import java.lang.classfile.* import java.lang.constant.ConstantDescs.CD_String diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/CodeBuilderUtils.kt similarity index 98% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/CodeBuilderUtils.kt index 725d44e..04e4f22 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderUtils.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/CodeBuilderUtils.kt @@ -1,4 +1,4 @@ -package dev.freya02.botcommands.restart.jda.cache.transformer +package dev.freya02.botcommands.restart.jda.cache.transformer.utils import java.lang.classfile.ClassFileBuilder import java.lang.classfile.ClassFileElement diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/TransformUtils.kt similarity index 96% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/TransformUtils.kt index 9cee6ec..4f59e6f 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/TransformUtils.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/TransformUtils.kt @@ -1,4 +1,4 @@ -package dev.freya02.botcommands.restart.jda.cache.transformer +package dev.freya02.botcommands.restart.jda.cache.transformer.utils import java.lang.classfile.ClassBuilder import java.lang.classfile.ClassFile.ACC_SYNTHETIC diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils/JvmUtils.kt similarity index 78% rename from restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt rename to restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils/JvmUtils.kt index 08640b6..bb94188 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/utils/JvmUtils.kt @@ -1,4 +1,4 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.utils internal fun isJvmShuttingDown() = try { Runtime.getRuntime().removeShutdownHook(NullShutdownHook) diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformerTest.kt similarity index 86% rename from restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt rename to restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformerTest.kt index 123667e..a03ae2d 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/BContextImplTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformerTest.kt @@ -1,4 +1,4 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.Test diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformerTest.kt similarity index 94% rename from restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt rename to restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformerTest.kt index 3329192..f7989ff 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDABuilderTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformerTest.kt @@ -1,5 +1,8 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.BufferingEventManager +import dev.freya02.botcommands.restart.jda.cache.JDABuilderConfiguration +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.mockk.* import net.dv8tion.jda.api.JDA import net.dv8tion.jda.api.JDABuilder diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformerTest.kt similarity index 83% rename from restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt rename to restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformerTest.kt index 55a6db6..da37b3f 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAImplTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformerTest.kt @@ -1,5 +1,6 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.mockk.* import net.dv8tion.jda.internal.JDAImpl import kotlin.test.Test diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformerTest.kt similarity index 96% rename from restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt rename to restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformerTest.kt index e0169aa..9c7badc 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/JDAServiceTransformerTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformerTest.kt @@ -1,5 +1,6 @@ -package dev.freya02.botcommands.restart.jda.cache +package dev.freya02.botcommands.restart.jda.cache.transformer +import dev.freya02.botcommands.restart.jda.cache.JDABuilderSession import io.github.freya022.botcommands.api.core.JDAService import io.github.freya022.botcommands.api.core.events.BReadyEvent import io.mockk.* diff --git a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderTest.kt b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/CodeBuilderUtilsTest.kt similarity index 96% rename from restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderTest.kt rename to restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/CodeBuilderUtilsTest.kt index baaae9a..5931e19 100644 --- a/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/CodeBuilderTest.kt +++ b/restarter-jda-cache/src/test/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/utils/CodeBuilderUtilsTest.kt @@ -1,5 +1,6 @@ -package dev.freya02.botcommands.restart.jda.cache.transformer +package dev.freya02.botcommands.restart.jda.cache.transformer.utils +import dev.freya02.botcommands.restart.jda.cache.transformer.* import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -7,7 +8,7 @@ import java.lang.constant.* import java.util.function.Supplier import kotlin.test.assertEquals -object CodeBuilderTest { +object CodeBuilderUtilsTest { @MethodSource("Test createLambda") @ParameterizedTest From c4e8c55f1bd372e0ee5f44b8088f738d59efbdc6 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 7 Jun 2025 19:56:53 +0200 Subject: [PATCH 65/68] JDA cache: Small reformat --- .../transformer/BContextImplTransformer.kt | 20 +++++----- .../transformer/JDABuilderTransformer.kt | 20 +++++----- .../cache/transformer/JDAImplTransformer.kt | 40 ++++++++++--------- .../transformer/JDAServiceTransformer.kt | 20 +++++----- 4 files changed, 55 insertions(+), 45 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt index ed8f8dd..ea3c9c8 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/BContextImplTransformer.kt @@ -54,15 +54,17 @@ private class DeferScheduleShutdownSignalTransform(private val classModel: Class // Runnable doScheduleShutdownSignal = () -> this.doScheduleShutdownSignal(afterShutdownSignal) codeBuilder.aload(thisSlot) codeBuilder.aload(afterShutdownSignalSlot) - codeBuilder.invokedynamic(createLambda( - interfaceMethod = Runnable::run, - targetType = CD_BContextImpl, - targetMethod = "doScheduleShutdownSignal", - targetMethodReturnType = CD_void, - targetMethodArguments = listOf(), - capturedTypes = listOf(CD_Function0), - isStatic = false - )) + codeBuilder.invokedynamic( + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_BContextImpl, + targetMethod = NEW_NAME, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_Function0), + isStatic = false + ) + ) codeBuilder.astore(doScheduleShutdownSignalSlot) // String sessionKey = JDABuilderSession.getCacheKey(this) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt index 1bb8d29..0232216 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDABuilderTransformer.kt @@ -148,15 +148,17 @@ private class DeferBuildAndSetBufferingEventManagerTransform(private val classMo // Supplier doBuild = this::doBuild codeBuilder.aload(thisSlot) - codeBuilder.invokedynamic(createLambda( - interfaceMethod = Supplier<*>::get, - targetType = CD_JDABuilder, - targetMethod = NEW_NAME, - targetMethodReturnType = CD_JDA, - targetMethodArguments = listOf(), - capturedTypes = emptyList(), - isStatic = false - )) + codeBuilder.invokedynamic( + createLambda( + interfaceMethod = Supplier<*>::get, + targetType = CD_JDABuilder, + targetMethod = NEW_NAME, + targetMethodReturnType = CD_JDA, + targetMethodArguments = listOf(), + capturedTypes = emptyList(), + isStatic = false + ) + ) codeBuilder.astore(doBuildSlot) // JDABuilderSession session = JDABuilderSession.currentSession(); diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt index 12f009c..b144235 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAImplTransformer.kt @@ -101,15 +101,17 @@ private class DeferShutdownTransform(private val classModel: ClassModel) : Conte // Runnable doShutdown = this::doShutdown codeBuilder.aload(thisSlot) - codeBuilder.invokedynamic(createLambda( - interfaceMethod = Runnable::run, - targetType = CD_JDAImpl, - targetMethod = NEW_NAME, - targetMethodReturnType = CD_void, - targetMethodArguments = listOf(), - capturedTypes = listOf(), - isStatic = false - )) + codeBuilder.invokedynamic( + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAImpl, + targetMethod = NEW_NAME, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(), + isStatic = false + ) + ) codeBuilder.astore(doShutdownSlot) // var builderSession = getBuilderSession() @@ -183,15 +185,17 @@ private class DeferShutdownNowTransform(private val classModel: ClassModel) : Co // Runnable doShutdownNow = this::doShutdownNow codeBuilder.aload(thisSlot) - codeBuilder.invokedynamic(createLambda( - interfaceMethod = Runnable::run, - targetType = CD_JDAImpl, - targetMethod = NEW_NAME, - targetMethodReturnType = CD_void, - targetMethodArguments = listOf(), - capturedTypes = listOf(), - isStatic = false - )) + codeBuilder.invokedynamic( + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAImpl, + targetMethod = NEW_NAME, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(), + isStatic = false + ) + ) codeBuilder.astore(doShutdownNowSlot) // var builderSession = getBuilderSession() diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt index cb3f09a..04e032b 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/transformer/JDAServiceTransformer.kt @@ -77,15 +77,17 @@ private class WrapOnReadyEventWithJDABuilderSessionTransform(private val classMo codeBuilder.aload(thisSlot) codeBuilder.aload(readyEventSlot) codeBuilder.aload(eventManagerSlot) - codeBuilder.invokedynamic(createLambda( - interfaceMethod = Runnable::run, - targetType = CD_JDAService, - targetMethod = NEW_NAME, - targetMethodReturnType = CD_void, - targetMethodArguments = listOf(), - capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), - isStatic = false - )) + codeBuilder.invokedynamic( + createLambda( + interfaceMethod = Runnable::run, + targetType = CD_JDAService, + targetMethod = NEW_NAME, + targetMethodReturnType = CD_void, + targetMethodArguments = listOf(), + capturedTypes = listOf(CD_BReadyEvent, CD_IEventManager), + isStatic = false + ) + ) codeBuilder.astore(sessionRunnableSlot) // JDABuilderSession.withBuilderSession(key, sessionRunnable) From c70b2d4402d1724f23ba41d72f235b65a4a016c0 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:06:01 +0200 Subject: [PATCH 66/68] Restarter: Check for InvocationTargetException when catching ImmediateRestartException --- .../botcommands/internal/restart/ImmediateRestartException.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt index d6a6612..19edb5b 100644 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/ImmediateRestartException.kt @@ -1,5 +1,7 @@ package dev.freya02.botcommands.internal.restart +import java.lang.reflect.InvocationTargetException + class ImmediateRestartException internal constructor() : RuntimeException("Dummy exception to stop the execution of the first main thread") { internal companion object { @@ -13,7 +15,7 @@ class ImmediateRestartException internal constructor() : RuntimeException("Dummy private class ExpectedReloadExceptionHandler(private val delegate: Thread.UncaughtExceptionHandler?) : Thread.UncaughtExceptionHandler { override fun uncaughtException(t: Thread, e: Throwable) { - if (e is ImmediateRestartException) { + if (e is ImmediateRestartException || (e is InvocationTargetException && e.targetException is ImmediateRestartException)) { return } From 85237c79cab4d4afffec6e152addf334a71498b9 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:11:24 +0200 Subject: [PATCH 67/68] Restarter: Override ClassLoader methods As they aren't handled by URLClassLoader and will thus call the AppClassLoader --- .../internal/restart/RestartClassLoader.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt index a6b5ff9..d803b78 100644 --- a/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt +++ b/restarter/src/main/kotlin/dev/freya02/botcommands/internal/restart/RestartClassLoader.kt @@ -2,8 +2,39 @@ package dev.freya02.botcommands.internal.restart import java.net.URL import java.net.URLClassLoader +import java.util.* +// STILL SUPER DUPER IMPORTANT TO OVERRIDE SOME STUFF AND DELEGATE internal class RestartClassLoader internal constructor( urls: List, parent: ClassLoader, -) : URLClassLoader(urls.toTypedArray(), parent) \ No newline at end of file +) : URLClassLoader(urls.toTypedArray(), parent) { + + override fun getResources(name: String): Enumeration { + return this.parent.getResources(name) + } + + override fun getResource(name: String): URL? { + return findResource(name) ?: super.getResource(name) + } + + override fun findResource(name: String): URL? { + return super.findResource(name) + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + return synchronized(getClassLoadingLock(name)) { + val loadedClass = findLoadedClass(name) ?: try { + findClass(name) + } catch (_: ClassNotFoundException) { + Class.forName(name, false, parent) + } + if (resolve) resolveClass(loadedClass) + loadedClass + } + } + + override fun findClass(name: String): Class<*> { + return super.findClass(name) + } +} \ No newline at end of file From cf5080fc0c7a1e749181dd810a44eb7738149a18 Mon Sep 17 00:00:00 2001 From: freya02 <41875020+freya022@users.noreply.github.com> Date: Sat, 14 Jun 2025 11:58:46 +0200 Subject: [PATCH 68/68] JDA cache: Don't use delegate event handler in critical section --- .../restart/jda/cache/BufferingEventManager.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt index 0275ee3..32c9581 100644 --- a/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt +++ b/restarter-jda-cache/src/main/kotlin/dev/freya02/botcommands/restart/jda/cache/BufferingEventManager.kt @@ -46,12 +46,16 @@ internal class BufferingEventManager @DynamicCall constructor( } override fun handle(event: GenericEvent) { - lock.withLock { + val delegate = lock.withLock { val delegate = delegate - if (delegate != null) return delegate.handle(event) - - eventBuffer += event + if (delegate == null) { + eventBuffer += event + return + } + delegate } + + delegate.handle(event) } override fun getRegisteredListeners(): List {