diff --git a/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Extensions.kt b/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Extensions.kt index 0f6cf543c3..e446cd8845 100644 --- a/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Extensions.kt +++ b/build-extensions/src/main/kotlin/eu/cloudnetservice/cloudnet/gradle/util/Extensions.kt @@ -43,7 +43,14 @@ fun TaskProvider.applyJarMetadata( "Implementation-Vendor" to "CloudNetService", "Implementation-Title" to Versions.CLOUDNET_CODE_NAME, ) - preMain?.let { manifest.attributes("Premain-Class" to it) } + preMain?.let { + manifest.attributes( + "Premain-Class" to it, + "Can-Redefine-Classes" to true, + "Can-Retransform-Classes" to true, + "Can-Set-Native-Method-Prefix" to true, + ) + } val commit = git.commit() val branchName = git.branchName() diff --git a/driver/impl/src/main/java/eu/cloudnetservice/driver/impl/event/DefaultRegisteredEventListener.java b/driver/impl/src/main/java/eu/cloudnetservice/driver/impl/event/DefaultRegisteredEventListener.java index 67a3f4ab6b..5f12ca76ef 100644 --- a/driver/impl/src/main/java/eu/cloudnetservice/driver/impl/event/DefaultRegisteredEventListener.java +++ b/driver/impl/src/main/java/eu/cloudnetservice/driver/impl/event/DefaultRegisteredEventListener.java @@ -87,7 +87,7 @@ final class DefaultRegisteredEventListener implements RegisteredEventListener { */ @Override public void fireEvent(@NonNull Event event) { - LOGGER.debug( + LOGGER.trace( "Calling event {} on listener {}", event.getClass().getName(), this.instance().getClass().getName()); diff --git a/node/api/src/main/java/eu/cloudnetservice/node/event/service/CloudServiceJvmClassPathConstructEvent.java b/node/api/src/main/java/eu/cloudnetservice/node/event/service/CloudServiceJvmClassPathConstructEvent.java new file mode 100644 index 0000000000..5a5735310e --- /dev/null +++ b/node/api/src/main/java/eu/cloudnetservice/node/event/service/CloudServiceJvmClassPathConstructEvent.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * 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 + * + * http://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 eu.cloudnetservice.node.event.service; + +import eu.cloudnetservice.node.service.CloudService; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import lombok.NonNull; +import org.jetbrains.annotations.UnmodifiableView; + +/** + * An event which is called after the service class path got constructed, but before it is being used to start the + * service. Removing elements from the class path is not possible. + * + * @since 4.0 + */ +public final class CloudServiceJvmClassPathConstructEvent extends CloudServiceEvent { + + private final Collection classPath; + + /** + * Constructs a new cloud service jvm class path event. + * + * @param service the service for which the class path got constructed. + * @param classPath the constructed class path. + */ + public CloudServiceJvmClassPathConstructEvent(@NonNull CloudService service, @NonNull Collection classPath) { + super(service); + this.classPath = classPath; + } + + /** + * Gets the constructed class path for the service. This set is an unmodifiable view of the class path. + * + * @return the constructed class path. + */ + @UnmodifiableView + public @NonNull Collection classPath() { + return Collections.unmodifiableCollection(this.classPath); + } + + /** + * Adds a new entry to the class path of the service. + * + * @param path the path to add. + * @throws NullPointerException if the given path is null. + */ + public void addClassPathEntry(@NonNull Path path) { + this.classPath.add(path); + } + + /** + * Adds multiple new entries to the class path of the service. + * + * @param paths the paths to add. + * @throws NullPointerException if the given collection is null. + */ + public void addClassPathEntries(@NonNull Collection paths) { + this.classPath.addAll(paths); + } +} diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/JVMService.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/JVMService.java index d21cb5001f..299e12fb19 100644 --- a/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/JVMService.java +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/JVMService.java @@ -16,19 +16,19 @@ package eu.cloudnetservice.node.impl.service.defaults; -import com.google.common.base.Preconditions; -import com.google.common.primitives.Ints; import eu.cloudnetservice.driver.event.EventManager; import eu.cloudnetservice.driver.language.I18n; import eu.cloudnetservice.driver.service.ServiceConfiguration; import eu.cloudnetservice.driver.service.ServiceEnvironment; import eu.cloudnetservice.driver.service.ServiceEnvironmentType; import eu.cloudnetservice.node.config.Configuration; +import eu.cloudnetservice.node.event.service.CloudServiceJvmClassPathConstructEvent; import eu.cloudnetservice.node.event.service.CloudServicePostProcessStartEvent; import eu.cloudnetservice.node.event.service.CloudServicePreProcessStartEvent; import eu.cloudnetservice.node.impl.service.InternalCloudServiceManager; import eu.cloudnetservice.node.impl.service.defaults.log.ProcessServiceLogCache; import eu.cloudnetservice.node.impl.service.defaults.log.ProcessServiceLogReadScheduler; +import eu.cloudnetservice.node.impl.service.defaults.wrapper.WrapperFileProvider; import eu.cloudnetservice.node.impl.tick.DefaultTickLoop; import eu.cloudnetservice.node.impl.version.ServiceVersionProvider; import eu.cloudnetservice.node.service.ServiceConfigurationPreparer; @@ -36,22 +36,20 @@ import eu.cloudnetservice.utils.base.StringUtil; import eu.cloudnetservice.utils.base.io.FileUtil; import io.vavr.CheckedFunction1; -import io.vavr.Tuple2; import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.jar.Attributes; import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.regex.Pattern; import java.util.stream.Collectors; import lombok.NonNull; import org.jetbrains.annotations.Nullable; @@ -61,7 +59,6 @@ public class JVMService extends AbstractService { protected static final Logger LOGGER = LoggerFactory.getLogger(JVMService.class); - protected static final Pattern FILE_NUMBER_PATTERN = Pattern.compile("(\\d+).*"); protected static final Collection DEFAULT_JVM_SYSTEM_PROPERTIES = Arrays.asList( "--enable-preview", "-Dfile.encoding=UTF-8", @@ -70,7 +67,6 @@ public class JVMService extends AbstractService { "-Djline.terminal=jline.UnsupportedTerminal"); protected static final Path LIB_PATH = Path.of("launcher", "libs"); - protected static final Path WRAPPER_TEMP_FILE = FileUtil.TEMP_DIR.resolve("caches").resolve("wrapper.jar"); protected volatile Process process; @@ -139,17 +135,20 @@ protected void startProcess() { } // get the agent class of the application (if any) - var agentClass = applicationInformation._2().mainAttributes().getValue("Premain-Class"); - if (agentClass == null) { - // some old versions named the agent class 'Launcher-Agent-Class' - try that - agentClass = applicationInformation._2().mainAttributes().getValue("Launcher-Agent-Class"); - } + var agentClass = applicationInformation.mainAttributes().getValue("Launcher-Agent-Class"); // prepare the full wrapper class path - var classPath = String.format( - "%s%s", - this.computeWrapperClassPath(wrapperInformation._1()), - wrapperInformation._1().toAbsolutePath()); + List classPathBuilder = new ArrayList<>(); + this.computeWrapperClassPath(classPathBuilder, wrapperInformation.path()); + classPathBuilder.add(wrapperInformation.path()); + classPathBuilder.add(applicationInformation.path()); + this.eventManager.callEvent(new CloudServiceJvmClassPathConstructEvent(this, classPathBuilder)); + var classPath = classPathBuilder.stream() + .map(Path::toAbsolutePath) + .map(Path::normalize) + .map(Path::toString) + .distinct() + .collect(Collectors.joining(File.pathSeparator)); // prepare the service startup List arguments = new LinkedList<>(); @@ -168,33 +167,27 @@ protected void startProcess() { // override some default configuration options arguments.addAll(DEFAULT_JVM_SYSTEM_PROPERTIES); - arguments.add("-javaagent:" + wrapperInformation._1().toAbsolutePath()); + arguments.add("-javaagent:" + wrapperInformation.path().toAbsolutePath()); arguments.add("-Dcloudnet.wrapper.messages.language=" + super.i18n.selectedLanguage().toLanguageTag()); - - // fabric specific class path - arguments.add(String.format("-Dfabric.systemLibraries=%s", classPath)); + if (agentClass != null) { + arguments.add("-Dcloudnet.wrapper.launcher-agent-class=" + agentClass); + } // set the used host and port as system property arguments.add("-Dservice.bind.host=" + this.serviceConfiguration().hostAddress()); arguments.add("-Dservice.bind.port=" + this.serviceConfiguration().port()); - // add the class path and the main class of the wrapper + // add the class path and the main class of the application arguments.add("-cp"); arguments.add(classPath); - arguments.add(wrapperInformation._2().getValue("Main-Class")); // the main class we want to invoke first - - // add all internal process parameters (they will be removed by the wrapper before starting the application) - arguments.add(applicationInformation._2().mainAttributes().getValue("Main-Class")); - arguments.add(String.valueOf(agentClass)); // the agent class might be null - arguments.add(applicationInformation._1().toAbsolutePath().toString()); - arguments.add(Boolean.toString(applicationInformation._2().preloadJarContent())); + arguments.add(applicationInformation.mainAttributes().getValue(Attributes.Name.MAIN_CLASS)); // add all process parameters arguments.addAll(environmentType.defaultProcessArguments()); arguments.addAll(this.serviceConfiguration().processConfig().processParameters()); // try to start the process like that - this.doStartProcess(arguments, wrapperInformation._1(), applicationInformation._1()); + this.doStartProcess(arguments, wrapperInformation.path(), applicationInformation.path()); } @Override @@ -287,28 +280,17 @@ protected void doStartProcess( } } - protected @Nullable Tuple2 prepareWrapperFile() { - // check if the wrapper file is there - unpack it if not - if (Files.notExists(WRAPPER_TEMP_FILE)) { - FileUtil.createDirectory(WRAPPER_TEMP_FILE.getParent()); - try (var stream = JVMService.class.getClassLoader().getResourceAsStream("wrapper.jar")) { - // ensure that the wrapper file is there - if (stream == null) { - throw new IllegalStateException("Build-in \"wrapper.jar\" missing, unable to start jvm based services"); - } - // copy the wrapper file to the output directory - Files.copy(stream, WRAPPER_TEMP_FILE, StandardCopyOption.REPLACE_EXISTING); - } catch (IOException exception) { - LOGGER.error("Unable to copy \"wrapper.jar\" to {}", WRAPPER_TEMP_FILE, exception); - } - } - // read the main class - return this.completeJarAttributeInformation( - WRAPPER_TEMP_FILE, + + protected @Nullable JarFileData prepareWrapperFile() { + var wrapperTempPath = WrapperFileProvider.unpackWrapperFile(); + var mainAttributes = this.extractFromJarFile( + wrapperTempPath, file -> file.getManifest().getMainAttributes()); + Objects.requireNonNull(mainAttributes, "Wrapper jar does not contain a manifest"); + return new JarFileData(wrapperTempPath, mainAttributes); } - protected @Nullable Tuple2 prepareApplicationFile( + protected @Nullable JarFileData prepareApplicationFile( @NonNull ServiceEnvironmentType environmentType ) { // collect all names of environment names @@ -326,45 +308,29 @@ protected void doStartProcess( return Files.walk(this.serviceDirectory, 1) .filter(path -> { var filename = path.getFileName().toString(); - // check if the file is a jar file - it must end with '.jar' for that if (!filename.endsWith(".jar")) { return false; } - // search if any environment is in the name of the file + for (var environment : environments) { if (filename.contains(environment)) { return true; } } - // not an application file for the environment + return false; - }).min((left, right) -> { - // get the first number from the left path - var leftMatcher = FILE_NUMBER_PATTERN.matcher(left.getFileName().toString()); - // no match -> neutral - if (!leftMatcher.matches()) { - return 0; - } + }) + .map(path -> { + var manifest = this.extractFromJarFile(path, JarFile::getManifest); + Objects.requireNonNull(manifest, "Application jar does not contain a manifest"); - // get the first number from the right patch - var rightMatcher = FILE_NUMBER_PATTERN.matcher(right.getFileName().toString()); - // no match -> neutral - if (!rightMatcher.matches()) { - return 0; - } + var mainClass = manifest.getMainAttributes().getValue("Main-Class"); + Objects.requireNonNull(mainClass, "Application jar manifest does not contain a Main-Class"); - // extract the numbers - var leftNumber = Ints.tryParse(leftMatcher.group(1)); - var rightNumber = Ints.tryParse(rightMatcher.group(1)); - // compare both of the numbers - return leftNumber == null || rightNumber == null ? 0 : Integer.compare(leftNumber, rightNumber); + return new JarFileData(path, manifest.getMainAttributes()); }) - .map(path -> this.completeJarAttributeInformation( - path, - file -> new ApplicationStartupInformation( - file.getEntry("META-INF/versions.list") != null, - this.validateManifest(file.getManifest()).getMainAttributes()) - )).orElse(null); + .findFirst() + .orElse(null); } catch (IOException exception) { LOGGER.error( "Unable to find application file information in {} for environment {}", @@ -375,23 +341,21 @@ protected void doStartProcess( } } - protected @Nullable Tuple2 completeJarAttributeInformation( + protected @Nullable T extractFromJarFile( @NonNull Path jarFilePath, @NonNull CheckedFunction1 mapper ) { // open the file and lookup the main class try (var jarFile = new JarFile(jarFilePath.toFile())) { - return new Tuple2<>(jarFilePath, mapper.apply(jarFile)); + return mapper.apply(jarFile); } catch (Throwable exception) { LOGGER.error("Unable to open wrapper file at {} for reading: ", jarFilePath, exception); return null; } } - protected @NonNull String computeWrapperClassPath(@NonNull Path wrapperPath) { - var builder = new StringBuilder(); + protected void computeWrapperClassPath(@NonNull Collection classPath, @NonNull Path wrapperPath) { FileUtil.openZipFile(wrapperPath, fs -> { - // get the wrapper cnl file and check if it is available var wrapperCnl = fs.getPath("wrapper.cnl"); if (Files.exists(wrapperCnl)) { Files.lines(wrapperCnl) @@ -409,24 +373,12 @@ protected void doStartProcess( parts[5], parts.length == 8 ? "-" + parts[7] : ""); return LIB_PATH.resolve(path); - }).forEach(path -> builder.append(path.toAbsolutePath()).append(File.pathSeparatorChar)); + }).forEach(classPath::add); } }); - // contains all paths we need now - return builder.toString(); - } - - protected @NonNull Manifest validateManifest(@Nullable Manifest manifest) { - // make sure that we have a manifest at all - Preconditions.checkNotNull(manifest, "Application jar does not contain a manifest."); - // make sure that the manifest at least contains a main class - Preconditions.checkNotNull( - manifest.getMainAttributes().getValue("Main-Class"), - "Application jar manifest does not contain a Main-Class."); - return manifest; } - protected record ApplicationStartupInformation(boolean preloadJarContent, @NonNull Attributes mainAttributes) { + protected record JarFileData(@NonNull Path path, @NonNull Attributes mainAttributes) { } } diff --git a/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/wrapper/WrapperFileProvider.java b/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/wrapper/WrapperFileProvider.java new file mode 100644 index 0000000000..82eff1070e --- /dev/null +++ b/node/impl/src/main/java/eu/cloudnetservice/node/impl/service/defaults/wrapper/WrapperFileProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * 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 + * + * http://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 eu.cloudnetservice.node.impl.service.defaults.wrapper; + +import eu.cloudnetservice.utils.base.io.FileUtil; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import lombok.NonNull; + +/** + * Helper to unpack the wrapper file that is shaded into the current node jar. + * + * @since 4.0 + */ +public final class WrapperFileProvider { + + private static final Lock WRAPPER_COPY_LOCK = new ReentrantLock(); + private static final Path WRAPPER_FILE_PATH = FileUtil.TEMP_DIR.resolve("caches").resolve("wrapper.jar"); + + static { + // ensure that the file gets deleted initially, so that the first + // access of the wrapper always unpacks the latest version from the jar + FileUtil.delete(WRAPPER_FILE_PATH); + } + + private WrapperFileProvider() { + throw new UnsupportedOperationException(); + } + + /** + * Unpacks the wrapper jar if it doesn't exist on the file system already. If another thread is unpacking the file + * already, the call blocks until the other thread completes the operation. The method returns the path to the + * unpacked wrapper file. + * + * @return the path to the unpacked wrapper file. + * @throws NullPointerException if the wrapper.jar file is missing in the current node jar. + * @throws IllegalStateException if the wrapper.jar cannot be unpacked to the file system. + */ + public static @NonNull Path unpackWrapperFile() { + if (Files.notExists(WRAPPER_FILE_PATH)) { + WRAPPER_COPY_LOCK.lock(); + try { + if (Files.notExists(WRAPPER_FILE_PATH)) { + try (var stream = WrapperFileProvider.class.getClassLoader().getResourceAsStream("wrapper.jar")) { + Objects.requireNonNull(stream, "Shaded \"wrapper.jar\" file missing, custom build?"); + Files.createDirectories(WRAPPER_FILE_PATH.getParent()); + Files.copy(stream, WRAPPER_FILE_PATH, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException exception) { + throw new IllegalStateException("Unable to unpack shaded wrapper file", exception); + } + } + } finally { + WRAPPER_COPY_LOCK.unlock(); + } + } + + return WRAPPER_FILE_PATH; + } +} diff --git a/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/ApplicationPostStartEvent.java b/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/ApplicationPostStartEvent.java deleted file mode 100644 index 66b5d5e378..0000000000 --- a/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/ApplicationPostStartEvent.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * 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 - * - * http://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 eu.cloudnetservice.wrapper.event; - -import eu.cloudnetservice.driver.event.Event; -import lombok.NonNull; - -/** - * An event called when the underlying application of the wrapper was started. This event cannot be used by platform - * plugins or extensions, as this event is fired before they get enabled. - * - * @since 4.0 - */ -public final class ApplicationPostStartEvent extends Event { - - private final Class applicationMainClass; - private final Thread applicationThread; - private final ClassLoader classLoader; - - /** - * Constructs a new ApplicationPostStartEvent instance. - * - * @param applicationMainClass the main class instance which will be invoked to start the application. - * @param applicationThread the thread in which the application was started. - * @param classLoader the class loader which loaded the application main class. - * @throws NullPointerException if the given app main, app thread or class loader is null. - */ - public ApplicationPostStartEvent( - @NonNull Class applicationMainClass, - @NonNull Thread applicationThread, - @NonNull ClassLoader classLoader - ) { - this.applicationMainClass = applicationMainClass; - this.applicationThread = applicationThread; - this.classLoader = classLoader; - } - - /** - * Get the main class which was invoked when starting the application. - * - * @return the invoked main class of the application. - */ - public @NonNull Class applicationMainClass() { - return this.applicationMainClass; - } - - /** - * Get the thread in which the application is running. - * - * @return the thread of the application. - */ - public @NonNull Thread applicationThread() { - return this.applicationThread; - } - - /** - * Get the class loader which was used to load the main class of the application (and all other classes of the - * application in the runtime unless the application decides to switch to another loader). - * - * @return the class loader of the application main class. - */ - public @NonNull ClassLoader classLoader() { - return this.classLoader; - } -} diff --git a/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/ApplicationPreStartEvent.java b/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/ApplicationPreStartEvent.java deleted file mode 100644 index 2e83a5459f..0000000000 --- a/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/ApplicationPreStartEvent.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * 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 - * - * http://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 eu.cloudnetservice.wrapper.event; - -import eu.cloudnetservice.driver.event.Event; -import java.util.Collection; -import lombok.NonNull; - -/** - * An event called when the underlying application of the wrapper is about to be started. This event cannot be used by - * platform plugins or extensions, as this event is fired before they get enabled. - * - * @since 4.0 - */ -public final class ApplicationPreStartEvent extends Event { - - private final Class applicationMainClass; - private final Collection arguments; - private final ClassLoader classLoader; - - /** - * Constructs a new ApplicationPreStartEvent instance. - * - * @param applicationMainClass the main class instance which will be invoked to start the application. - * @param arguments the process arguments which will be passed to the application. - * @param classLoader the class loader which loaded the application main class. - * @throws NullPointerException if the given application main, arguments or class loader is null. - */ - public ApplicationPreStartEvent( - @NonNull Class applicationMainClass, - @NonNull Collection arguments, - @NonNull ClassLoader classLoader - ) { - this.applicationMainClass = applicationMainClass; - this.arguments = arguments; - this.classLoader = classLoader; - } - - /** - * Get the main class which was invoked when starting the application. - * - * @return the invoked main class of the application. - */ - public @NonNull Class applicationMainClass() { - return this.applicationMainClass; - } - - /** - * Get the arguments which will be passed to the application when starting. The returned collection is modifiable and - * can be used to change the arguments before starting. - * - * @return the arguments which will be passed to the application as process arguments. - */ - public @NonNull Collection arguments() { - return this.arguments; - } - - /** - * Get the class loader which was used to load the main class of the application (and all other classes of the - * application in the runtime unless the application decides to switch to another loader). - * - * @return the class loader of the application main class. - */ - public @NonNull ClassLoader classLoader() { - return this.classLoader; - } -} diff --git a/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/WrapperBootstrapCompleteEvent.java b/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/WrapperBootstrapCompleteEvent.java new file mode 100644 index 0000000000..35b8924852 --- /dev/null +++ b/wrapper-jvm/api/src/main/java/eu/cloudnetservice/wrapper/event/WrapperBootstrapCompleteEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * 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 + * + * http://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 eu.cloudnetservice.wrapper.event; + +import eu.cloudnetservice.driver.event.Event; + +/** + * An event called as last act of the cloudnet wrapper before the application is started. This event cannot be used by + * platform plugins or extensions, as this event is fired before they get enabled. + * + * @since 4.0 + */ +public final class WrapperBootstrapCompleteEvent extends Event { + +} diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/ApplicationThread.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/ApplicationThread.java deleted file mode 100644 index ba299e3a92..0000000000 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/ApplicationThread.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * 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 - * - * http://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 eu.cloudnetservice.wrapper.impl; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Collection; -import java.util.Objects; -import lombok.NonNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Thread that executes the main method of the wrapped application and stays alive until the application exits, either - * normally or by throwing an exception. - * - * @since 4.0 - */ -final class ApplicationThread extends Thread { - - private static final Logger LOGGER = LoggerFactory.getLogger(ApplicationThread.class); - - private static final int LOGGED_ERROR_EXIST_STATUS = -1; - private static final int UNLOGGED_ERROR_EXIST_STATUS = -2; - - private final Method mainMethod; - private final String[] mainArgs; - - /** - * Constructs and setups the application thread for execution. - * - * @param mainMethod the main method to use for running the wrapped application. - * @param mainArgs the arguments to pass to the wrapped main method. - * @throws NullPointerException if the given main method or main args collection is null. - */ - public ApplicationThread(@NonNull Method mainMethod, @NonNull Collection mainArgs) { - this.mainMethod = mainMethod; - this.mainArgs = mainArgs.toArray(String[]::new); - - // explicitly disable daemon mode for the thread to ensure it keeps the wrapper - // alive until the wrapped application terminates in any way - super.setDaemon(false); - super.setName("Application-Thread"); - super.setPriority(Thread.NORM_PRIORITY + 1); - } - - /** - * {@inheritDoc} - */ - @Override - public void run() { - try { - this.mainMethod.invoke(null, new Object[]{this.mainArgs}); - LOGGER.info("Invocation of application main method {} completed successfully", this.mainMethod); - return; - } catch (InvocationTargetException exception) { - // application threw error during execution - var cause = exception.getCause(); - var exceptionToHandle = Objects.requireNonNullElse(cause, exception); - LOGGER.error("Caught application exception while running {}", this.mainMethod, exceptionToHandle); - } catch (IllegalArgumentException | NullPointerException exception) { - // illegal invocation of the given main method due to argument type mismatch - LOGGER.error("[BUG] Unable to invoke main method {} of application: {}", this.mainMethod, exception.getMessage()); - } catch (IllegalAccessException exception) { - // illegal access to the main method, possibly private or in a module - LOGGER.error( - "The main method {} of the application cannot be called because the access modifiers of method are too strict: {}", - this.mainMethod, exception.getMessage()); - } catch (Exception exception) { - LOGGER.error("Caught unexpected exception while running {}", this.mainMethod, exception); - } catch (Throwable _) { - // assume the worst case situation if no other catch clause handled the exception yet - // immediately exit the vm without even trying to log something (logging might fail as well) - Runtime.getRuntime().halt(UNLOGGED_ERROR_EXIST_STATUS); - } - - // fall-through case for handled exceptions - System.exit(LOGGED_ERROR_EXIST_STATUS); - } -} diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Main.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Main.java deleted file mode 100644 index 80ba919551..0000000000 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Main.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2019-present CloudNetService team & contributors - * - * 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 - * - * http://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 eu.cloudnetservice.wrapper.impl; - -import com.google.common.collect.Lists; -import eu.cloudnetservice.driver.inject.InjectionLayer; -import eu.cloudnetservice.driver.registry.ServiceRegistry; -import eu.cloudnetservice.wrapper.impl.transform.ClassTransformerRegistry; -import io.leangen.geantyref.TypeFactory; -import java.time.Instant; -import java.util.List; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import lombok.NonNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class Main { - - private Main() { - throw new UnsupportedOperationException(); - } - - public static void main(@NonNull String... args) throws Throwable { - var startInstant = Instant.now(); - - // initialize injector & install all autoconfigure bindings - var bootInjectLayer = InjectionLayer.boot(); - bootInjectLayer.installAutoConfigureBindings(Main.class.getClassLoader(), "driver"); - bootInjectLayer.installAutoConfigureBindings(Main.class.getClassLoader(), "wrapper"); - - ServiceRegistry.registry().discoverServices(Wrapper.class); - - var rootLogger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - - // initial bindings which we cannot (or it makes no sense to) construct - var builder = bootInjectLayer.injector().createBindingBuilder(); - bootInjectLayer.install(builder.bind(org.slf4j.Logger.class).qualifiedWithName("root").toInstance(rootLogger)); - bootInjectLayer.install(builder.bind(Instant.class).qualifiedWithName("startInstant").toInstance(startInstant)); - - var threadFactory = Thread.ofPlatform() - .daemon(true) - .priority(Thread.NORM_PRIORITY) - .inheritInheritableThreadLocals(true) - .name("CloudNet-TaskScheduler-Thread-", 0L) - .factory(); - bootInjectLayer.install(builder - .bind(ScheduledExecutorService.class) - .qualifiedWithName("taskScheduler") - .toInstance(Executors.newScheduledThreadPool(2, threadFactory))); - - // bind the transformer registry here - we *could* provided it by constructing, but we don't - // want to expose the Instrumentation instance - bootInjectLayer.install(builder.bind(ClassTransformerRegistry.class).toInstance(Premain.transformerRegistry)); - - // console arguments - var type = TypeFactory.parameterizedClass(List.class, String.class); - bootInjectLayer.install(builder.bind(type).qualifiedWithName("consoleArgs").toInstance(Lists.newArrayList(args))); - - // boot the wrapper - bootInjectLayer.instance(Wrapper.class); - } -} diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Premain.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Premain.java index 1cbad4ac52..813d6ca8b1 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Premain.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Premain.java @@ -16,27 +16,21 @@ package eu.cloudnetservice.wrapper.impl; +import eu.cloudnetservice.driver.inject.InjectionLayer; import eu.cloudnetservice.wrapper.impl.transform.ClassTransformerRegistry; import eu.cloudnetservice.wrapper.impl.transform.DefaultClassTransformerRegistry; import eu.cloudnetservice.wrapper.impl.transform.unsafe.UnsafeTransformer; -import java.io.IOException; import java.lang.instrument.Instrumentation; -import java.lang.reflect.Method; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; +import java.time.Instant; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import lombok.NonNull; import org.jetbrains.annotations.Nullable; final class Premain { - static Instrumentation instrumentation; - static ClassTransformerRegistry transformerRegistry; - - public static void premain(@Nullable String agentArgs, @NonNull Instrumentation inst) { - Premain.instrumentation = inst; - Premain.transformerRegistry = new DefaultClassTransformerRegistry(inst); + public static void premain(@Nullable String agentArgs, @NonNull Instrumentation inst) throws Exception { + var transformerRegistry = new DefaultClassTransformerRegistry(inst); // init and registers the unsafe transformer very early in the process. this is done here // as we usually don't allow transformers to be registered so early as they're intended to @@ -44,79 +38,77 @@ public static void premain(@Nullable String agentArgs, @NonNull Instrumentation var unsafeTransformerDisabled = Boolean.getBoolean("cloudnet.wrapper.unsafe-transform-disabled"); if (!unsafeTransformerDisabled) { UnsafeTransformer.init(inst); - Premain.transformerRegistry.registerTransformer(new UnsafeTransformer()); + transformerRegistry.registerTransformer(new UnsafeTransformer()); } + + invokePremain(inst); + bootstrapWrapper(transformerRegistry); } - public static void preloadClasses(@NonNull Path file, @NonNull ClassLoader loader) { - try (var stream = new JarInputStream(Files.newInputStream(file))) { - JarEntry entry; - while ((entry = stream.getNextJarEntry()) != null) { - // only resolve class files - if (!entry.isDirectory() && entry.getName().endsWith(".class")) { - // canonicalize the class name - var className = entry.getName().replace('/', '.').replace(".class", ""); - // load the class - try { - Class.forName(className, false, loader); - } catch (ClassNotFoundException ignored) { - // ignore - } - } - } - } catch (IOException exception) { - throw new IllegalStateException("Unable to preload classes in app file", exception); - } + private static void bootstrapWrapper(@NonNull ClassTransformerRegistry transformerRegistry) { + var startInstant = Instant.now(); + + // initialize injector & install all autoconfigure bindings + var bootInjectLayer = InjectionLayer.boot(); + bootInjectLayer.installAutoConfigureBindings(Wrapper.class.getClassLoader(), "driver"); + bootInjectLayer.installAutoConfigureBindings(Wrapper.class.getClassLoader(), "wrapper"); + + // initial bindings which we cannot (or it makes no sense to) construct + var builder = bootInjectLayer.injector().createBindingBuilder(); + bootInjectLayer.install(builder.bind(Instant.class).qualifiedWithName("startInstant").toInstance(startInstant)); + + var threadFactory = Thread.ofPlatform() + .daemon(true) + .priority(Thread.NORM_PRIORITY) + .inheritInheritableThreadLocals(true) + .name("CloudNet-TaskScheduler-Thread-", 0L) + .factory(); + bootInjectLayer.install(builder + .bind(ScheduledExecutorService.class) + .qualifiedWithName("taskScheduler") + .toInstance(Executors.newScheduledThreadPool(2, threadFactory))); + + // bind the transformer registry here - we *could* provided it by constructing, but we don't + // want to expose the Instrumentation instance + bootInjectLayer.install(builder.bind(ClassTransformerRegistry.class).toInstance(transformerRegistry)); + + // boot the wrapper + bootInjectLayer.instance(Wrapper.class); } - public static void invokePremain(@NonNull String premainClass, @NonNull ClassLoader loader) throws Exception { - if (!premainClass.equals("null")) { - try { - var agentClass = Class.forName(premainClass, true, loader); - // find any possible premain method as defined in: - // ~ https://docs.oracle.com/en/java/javase/11/docs/api/java.instrument/java/lang/instrument/package-summary.html - // agentmain(String, Instrumentation) - var method = methodOrNull(agentClass, "agentmain", String.class, Instrumentation.class); - if (method != null) { - invokeAgentMainMethod(method, "", Premain.instrumentation); - return; - } - // agentmain(String) - method = methodOrNull(agentClass, "agentmain", String.class); - if (method != null) { - invokeAgentMainMethod(method, ""); - return; - } - // premain(String, Instrumentation) - method = methodOrNull(agentClass, "premain", String.class, Instrumentation.class); - if (method != null) { - invokeAgentMainMethod(method, "", Premain.instrumentation); - return; - } - // premain(String) - method = methodOrNull(agentClass, "premain", String.class); - if (method != null) { - invokeAgentMainMethod(method, ""); - return; - } - // the given agent class has no agent main methods - this should never happen - throw new IllegalArgumentException("Agent Class " + premainClass + " has no agent main methods"); - } catch (ClassNotFoundException ignored) { - // the agent main class is not available - this should not happen, but we don't care + public static void invokePremain(@NonNull Instrumentation instrumentation) throws Exception { + try { + var agentClassName = System.getProperty("cloudnet.wrapper.launcher-agent-class"); + if (agentClassName == null || agentClassName.isBlank()) { + return; } + + // find any possible premain method as defined in: + // ~ https://docs.oracle.com/en/java/javase/25/docs/api/java.instrument/java/lang/instrument/package-summary.html + var agentClass = Class.forName(agentClassName, true, Premain.class.getClassLoader()); + + // agentmain(String, Instrumentation) + if (invokeAgentMain(agentClass, instrumentation)) { + return; + } + // agentmain(String) + invokeAgentMain(agentClass, null); + } catch (ClassNotFoundException ignored) { + // the agent main class is not available - this should not happen, but we don't care } } - private static void invokeAgentMainMethod(@NonNull Method method, Object... args) throws Exception { - method.setAccessible(true); - method.invoke(null, args); - } + private static boolean invokeAgentMain(@NonNull Class source, @Nullable Instrumentation inst) throws Exception { + var args = inst != null ? new Class[]{String.class, Instrumentation.class} : new Class[]{String.class}; + var invokeArgs = inst != null ? new Object[]{"", inst} : new Object[]{""}; - private static @Nullable Method methodOrNull(@NonNull Class source, @NonNull String name, Class... args) { try { - return source.getDeclaredMethod(name, args); + var method = source.getDeclaredMethod("agentmain", args); + method.setAccessible(true); + method.invoke(null, invokeArgs); + return true; } catch (NoSuchMethodException exception) { - return null; + return false; } } } diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Wrapper.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Wrapper.java index 8e2b74bcb5..edf0161043 100644 --- a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Wrapper.java +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/Wrapper.java @@ -28,11 +28,11 @@ import eu.cloudnetservice.driver.network.NetworkClient; import eu.cloudnetservice.driver.network.chunk.event.EventChunkHandlerFactory; import eu.cloudnetservice.driver.registry.Service; +import eu.cloudnetservice.driver.registry.ServiceRegistry; import eu.cloudnetservice.utils.base.io.FileUtil; import eu.cloudnetservice.utils.base.resource.ResourceResolver; import eu.cloudnetservice.wrapper.configuration.WrapperConfiguration; -import eu.cloudnetservice.wrapper.event.ApplicationPostStartEvent; -import eu.cloudnetservice.wrapper.event.ApplicationPreStartEvent; +import eu.cloudnetservice.wrapper.event.WrapperBootstrapCompleteEvent; import eu.cloudnetservice.wrapper.holder.ServiceInfoHolder; import eu.cloudnetservice.wrapper.impl.network.chunk.TemplateStorageCallbackListener; import eu.cloudnetservice.wrapper.impl.network.listener.AuthorizationPacketListener; @@ -43,24 +43,16 @@ import eu.cloudnetservice.wrapper.impl.transform.ClassTransformer; import eu.cloudnetservice.wrapper.impl.transform.ClassTransformerRegistry; import jakarta.inject.Inject; -import jakarta.inject.Named; import jakarta.inject.Provider; -import java.io.File; import java.io.IOException; -import java.net.URL; -import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collection; -import java.util.LinkedList; -import java.util.List; import java.util.Locale; import java.util.Objects; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.LockSupport; -import java.util.jar.JarFile; import lombok.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,17 +67,23 @@ public final class Wrapper { private static final Logger LOGGER = LoggerFactory.getLogger(Wrapper.class); - public static void loadTranslations(@NonNull @Service I18n i18n) { + @Inject + @Order(50) + private void initServiceRegistry() { + ServiceRegistry.registry().discoverServices(Wrapper.class); + } + + @Inject + @Order(100) + private void initI18n(@NonNull @Service I18n i18n) { var resourcePath = Path.of(ResourceResolver.resolveCodeSourceOfClass(Wrapper.class)); FileUtil.openZipFile(resourcePath, fs -> { - // get the language directory var langDir = fs.getPath("lang/"); if (Files.notExists(langDir) || !Files.isDirectory(langDir)) { throw new IllegalStateException("lang/ must be an existing directory inside the jar to load"); } - // visit each file and register it as a language source + FileUtil.walkFileTree(langDir, ($, sub) -> { - // try to load and register the language file try (var stream = Files.newInputStream(sub)) { var lang = sub.getFileName().toString().replace(".properties", ""); i18n.registerProvider(Locale.forLanguageTag(lang), PropertiesTranslationProvider.fromProperties(stream)); @@ -94,12 +92,6 @@ public static void loadTranslations(@NonNull @Service I18n i18n) { } }, false, "*.properties"); }); - } - - @Inject - @Order(100) - private void initI18n(@NonNull @Service I18n i18n) { - loadTranslations(i18n); i18n.selectLanguage(Locale.forLanguageTag(System.getProperty("cloudnet.wrapper.messages.language", "en_US"))); } @@ -167,7 +159,6 @@ private void connectToNode( @Order(200) private void installShutdownHook(@NonNull Provider shutdownHandlerProvider) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { - // get the shutdown handler and execute the shutdown process var shutdownHandler = shutdownHandlerProvider.get(); shutdownHandler.shutdown(); })); @@ -201,68 +192,15 @@ private void registerDefaultListeners(@NonNull EventManager eventManager) { } @Inject - @Order(Integer.MAX_VALUE) - private void startApplication( - @NonNull EventManager eventManager, - @NonNull @Named("consoleArgs") List consoleArgs - ) throws Exception { - // get all the information provided through the command line - var mainClass = consoleArgs.remove(0); - var premainClass = consoleArgs.remove(0); - var appFile = Path.of(consoleArgs.remove(0)); - var preLoadAppJar = Boolean.parseBoolean(consoleArgs.remove(0)); - - // preload all jars in the application if requested - var loader = ClassLoader.getSystemClassLoader(); - if (preLoadAppJar) { - // create a custom class loader for loading the application resources - loader = new URLClassLoader( - new URL[]{appFile.toUri().toURL()}, - ClassLoader.getSystemClassLoader()); - // force our loader to load all classes in the jar - Premain.preloadClasses(appFile, loader); - } - - // append the application file to the system class path - Premain.instrumentation.appendToSystemClassLoaderSearch(new JarFile(appFile.toFile())); - - // invoke the premain method if given - Premain.invokePremain(premainClass, loader); - - // get the main method - var main = Class.forName(mainClass, true, loader); - var mainMethod = main.getMethod("main", String[].class); - - // inform the user about the pre-start - Collection arguments = new LinkedList<>(consoleArgs); - eventManager.callEvent(new ApplicationPreStartEvent(main, arguments, loader)); - - // initially the class path is not allowed to contain the path to the app file - // as the wrapper need to load it in a custom class loader after the system - // class loader is set up. - // however, some people for some reason rely on the app file being on the class - // path (for example to search resources). therefore we re-append the app file - // after jvm init so that the app file does not show up in the system class path - // but will show up if someone access "java.class.path" (or some other source - // in java, everything uses this property, e.g. RuntimeMXBean) - System.setProperty("java.class.path", this.appendAppFileToClassPath(appFile)); - - // start the application - LOGGER.info("Starting wrapped application using {} (pre-main class: {})", mainMethod, premainClass); - var applicationThread = new ApplicationThread(mainMethod, arguments); - applicationThread.setContextClassLoader(loader); - applicationThread.start(); - - // inform the user about the post-start - eventManager.callEvent(new ApplicationPostStartEvent(main, applicationThread, loader)); + @Order(350) + private void provideClassPathToFabric() { + var classPath = System.getProperty("java.class.path"); + System.setProperty("fabric.systemLibraries", classPath); } - private @NonNull String appendAppFileToClassPath(@NonNull Path appFile) { - var currentClassPath = System.getProperty("java.class.path"); - if (currentClassPath == null || currentClassPath.isBlank()) { - return appFile.getFileName().toString(); - } else { - return currentClassPath + File.pathSeparator + appFile.getFileName(); - } + @Inject + @Order(Integer.MAX_VALUE) + private void wrapperBootstrapComplete(@NonNull EventManager eventManager) { + eventManager.callEvent(new WrapperBootstrapCompleteEvent()); } } diff --git a/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/bukkit/PaperclipClassLoaderTransformer.java b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/bukkit/PaperclipClassLoaderTransformer.java new file mode 100644 index 0000000000..930f1ad010 --- /dev/null +++ b/wrapper-jvm/impl/src/main/java/eu/cloudnetservice/wrapper/impl/transform/bukkit/PaperclipClassLoaderTransformer.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019-present CloudNetService team & contributors + * + * 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 + * + * http://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 eu.cloudnetservice.wrapper.impl.transform.bukkit; + +import eu.cloudnetservice.wrapper.impl.transform.ClassTransformer; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.instruction.InvokeInstruction; +import lombok.NonNull; +import org.jetbrains.annotations.ApiStatus; + +/** + * A transformer implementation that removes the paperclip call to get the parent class loader of the paperclip class + * which results in the bootstrap class loader which does not have the files from the class path. + * + * @since 4.0 + */ +@ApiStatus.Internal +public class PaperclipClassLoaderTransformer implements ClassTransformer { + + private static final String MAIN_METHOD_NAME = "main"; + private static final String CLASS_LOADER_GET_PARENT_METHOD = "getParent"; + private static final String CLASS_LOADER_INTERNAL_NAME = "java/lang/ClassLoader"; + private static final String PAPERCLIP_MAIN_CLASS = "io/papermc/paperclip/Paperclip"; + + /** + * Constructs a new instance of this transformer, usually done via SPI. + */ + public PaperclipClassLoaderTransformer() { + // used by SPI + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull TransformWillingness classTransformWillingness(@NonNull String internalClassName) { + return internalClassName.equals(PAPERCLIP_MAIN_CLASS) + ? TransformWillingness.ACCEPT_ONCE + : TransformWillingness.REJECT; + } + + /** + * {@inheritDoc} + */ + @Override + public @NonNull ClassTransform provideClassTransform(@NonNull ClassModel original) { + return ClassTransform.transformingMethodBodies( + methodModel -> methodModel.methodName().equalsString(MAIN_METHOD_NAME), + (builder, element) -> { + if (element instanceof InvokeInstruction invoke) { + if (invoke.method().name().equalsString(CLASS_LOADER_GET_PARENT_METHOD) + && invoke.owner().asInternalName().equals(CLASS_LOADER_INTERNAL_NAME)) { + return; + } + } + + builder.with(element); + } + ); + } +} diff --git a/wrapper-jvm/impl/src/main/resources/META-INF/services/eu.cloudnetservice.wrapper.impl.transform.ClassTransformer b/wrapper-jvm/impl/src/main/resources/META-INF/services/eu.cloudnetservice.wrapper.impl.transform.ClassTransformer index becc6b34fa..9ed4b4432d 100644 --- a/wrapper-jvm/impl/src/main/resources/META-INF/services/eu.cloudnetservice.wrapper.impl.transform.ClassTransformer +++ b/wrapper-jvm/impl/src/main/resources/META-INF/services/eu.cloudnetservice.wrapper.impl.transform.ClassTransformer @@ -19,6 +19,7 @@ eu.cloudnetservice.wrapper.impl.transform.bukkit.BukkitJavaVersionCheckTransform eu.cloudnetservice.wrapper.impl.transform.bukkit.PaperConfigTransformer eu.cloudnetservice.wrapper.impl.transform.bukkit.FAWEConfigTransformer eu.cloudnetservice.wrapper.impl.transform.bukkit.FAWEReflectionUtilsTransformer +eu.cloudnetservice.wrapper.impl.transform.bukkit.PaperclipClassLoaderTransformer eu.cloudnetservice.wrapper.impl.transform.bukkit.WorldEditJava8DetectorTransformer eu.cloudnetservice.wrapper.impl.transform.bukkit.FAWEWorldEditDownloadURLTransformer eu.cloudnetservice.wrapper.impl.transform.netty.OldEpollDisableTransformer