Skip to content

Commit

Permalink
launch: Detect builtin plugins when not first on classpath
Browse files Browse the repository at this point in the history
This corrects builtin plugin detection in situations where the
SpongeVanilla jar is not the first in the classpath, and properly allows
gathering enumerations of resources.

ModLauncher has been bumped, and cached manifests are now passed to the
ModLauncher manifest finder.
  • Loading branch information
zml2008 committed Jan 16, 2021
1 parent df9c5a0 commit ec1ed7a
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 25 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mappingsVersion=1.16.4
recommendedVersion=0-SNAPSHOT
asmVersion=7.2
mixinVersion=0.8.2
modlauncherVersion=7.0.1
modlauncherVersion=8.0.9
pluginSpiVersion=0.1.4-SNAPSHOT
guavaVersion=21.0
junitVersion=5.7.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,29 @@
import org.spongepowered.plugin.jvm.locator.ResourceType;
import org.spongepowered.vanilla.applaunch.Main;

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.jar.Manifest;

/**
* The common Sponge {@link ILaunchHandlerService launch handler} for development
Expand Down Expand Up @@ -86,7 +98,8 @@ public abstract class AbstractVanillaLaunchHandler implements ILaunchHandlerServ

@Override
public void configureTransformationClassLoader(final ITransformingClassLoaderBuilder builder) {
builder.setClassBytesLocator(this.getResourceLocator());
builder.setResourceEnumeratorLocator(this.getResourceLocator());
builder.setManifestLocator(this.getManifestLocator());
}

@Override
Expand All @@ -113,30 +126,105 @@ public Callable<Void> launchService(final String[] arguments, final ITransformin
};
}

protected Function<String, Optional<URL>> getResourceLocator() {
protected Function<String, Enumeration<URL>> getResourceLocator() {
return s -> {
for (final Map.Entry<PluginLanguageService<PluginResource>, List<PluginCandidate<PluginResource>>> serviceCandidates :
Main.getInstance().getPluginEngine().getCandidates().entrySet()) {
for (final PluginCandidate<PluginResource> candidate : serviceCandidates.getValue()) {
final PluginResource resource = candidate.getResource();

if (resource instanceof JVMPluginResource) {
if (((JVMPluginResource) resource).getType() != ResourceType.JAR) {
continue;
// Save unnecessary searches of plugin classes for things that are definitely not plugins
// In this case: MC and fastutil
if (s.startsWith("net/minecraft") || s.startsWith("it/unimi")) {
return Collections.emptyEnumeration();
}

return new Enumeration<URL>() {
final Iterator<Map.Entry<PluginLanguageService<PluginResource>, List<PluginCandidate<PluginResource>>>> serviceCandidates =
Main.getInstance().getPluginEngine().getCandidates().entrySet().iterator();
Iterator<PluginCandidate<PluginResource>> candidates;
URL next = this.computeNext();

@Override
public boolean hasMoreElements() {
return this.next != null;
}

@Override
public URL nextElement() {
final URL next = this.next;
if (next == null) {
throw new NoSuchElementException();
}
this.next = this.computeNext();
return next;
}

private URL computeNext() {
while (true) {
if (this.candidates != null && !this.candidates.hasNext()) {
this.candidates = null;
}
if (this.candidates == null) {
if (!this.serviceCandidates.hasNext()) {
return null;
}
this.candidates = this.serviceCandidates.next().getValue().iterator();
}

if (this.candidates.hasNext()) {
final PluginResource resource = this.candidates.next().getResource();
if (resource instanceof JVMPluginResource) {
if (((JVMPluginResource) resource).getType() != ResourceType.JAR) {
continue;
}
}

final Path resolved = resource.getFileSystem().getPath(s);
if (Files.exists(resolved)) {
try {
return resolved.toUri().toURL();
} catch (final MalformedURLException ex) {
throw new RuntimeException(ex);
}
}
}
}
}
};
};
}

final Path resolved = resource.getFileSystem().getPath(s);
if (Files.exists(resolved)) {
try {
return Optional.of(resolved.toUri().toURL());
} catch (final MalformedURLException ex) {
throw new RuntimeException(ex);
private final ConcurrentMap<URL, Optional<Manifest>> manifestCache = new ConcurrentHashMap<>();
private static final Optional<Manifest> UNKNOWN_MANIFEST = Optional.of(new Manifest());

private Function<URLConnection, Optional<Manifest>> getManifestLocator() {
return connection -> {
if (connection instanceof JarURLConnection) {
final URL jarFileUrl = ((JarURLConnection) connection).getJarFileURL();
final Optional<Manifest> manifest = this.manifestCache.computeIfAbsent(jarFileUrl, key -> {
for (final List<PluginResource> resources : Main.getInstance().getPluginEngine().getResources().values()) {
for (final PluginResource resource : resources) {
if (resource instanceof JVMPluginResource) {
final JVMPluginResource jvmResource = (JVMPluginResource) resource;
try {
if (jvmResource.getType() == ResourceType.JAR && resource.getPath().toAbsolutePath().normalize().equals(Paths.get(key.toURI()).toAbsolutePath().normalize())) {
return jvmResource.getManifest();
}
} catch (final URISyntaxException ex) {
this.logger.error("Failed to load manifest from jar {}: ", key, ex);
}
}
}
}
return AbstractVanillaLaunchHandler.UNKNOWN_MANIFEST;
});

try {
if (manifest == AbstractVanillaLaunchHandler.UNKNOWN_MANIFEST) {
return Optional.ofNullable(((JarURLConnection) connection).getManifest());
} else {
return manifest;
}
} catch (final IOException ex) {
this.logger.error("Failed to load manifest from jar {}: ", jarFileUrl, ex);
}
}

return Optional.empty();
};
}
Expand Down
2 changes: 1 addition & 1 deletion vanilla/src/applaunch/resources/log4j2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</RollingRandomAccessFile>
</Appenders>
<Loggers>
<Root level="all">
<Root level="DEBUG">
<AppenderRef ref="SysOut" level="INFO"/>
<AppenderRef ref="File" level="INFO"/>
<AppenderRef ref="DebugFile" level="DEBUG"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,13 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public final class InstallerMain {

private static final Pattern CLASSPATH_SPLITTER = Pattern.compile(";", Pattern.LITERAL);

static {
System.setProperty("log4j.configurationFile", "log4j2_launcher.xml");
}
Expand Down Expand Up @@ -88,9 +91,11 @@ public void run() throws Exception {
}
final String depsClasspath = this.installer.getLibraryManager().getAll().values().stream().map(LibraryManager.Library::getFile).
map(Path::toAbsolutePath).map(Path::normalize).map(Path::toString).collect(Collectors.joining(File.pathSeparator));
final String classpath = Paths.get(System.getProperty("java.class.path")).toAbsolutePath() + File.pathSeparator +
depsClasspath + File.pathSeparator +
minecraftJar.toAbsolutePath().normalize().toString();
final String launchClasspath = CLASSPATH_SPLITTER.splitAsStream(System.getProperty("java.class.path"))
.map(it -> Paths.get(it).toAbsolutePath().toString())
.collect(Collectors.joining(File.pathSeparator));
final String classpath = launchClasspath + File.pathSeparator + depsClasspath +
File.pathSeparator + minecraftJar.toAbsolutePath().normalize().toString();
final List<String> gameArgs = Arrays.asList(this.installer.getLauncherConfig().args.split(" "));

this.installer.getLogger().debug("Setting classpath to: " + classpath);
Expand Down
3 changes: 3 additions & 0 deletions vanilla/src/installer/resources/log4j2_launcher.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
<AppenderRef ref="SysOut" level="INFO"/>
<AppenderRef ref="File" level="INFO"/>
<AppenderRef ref="DebugFile" level="TRACE"/>
<filters>
<MarkerFilter marker="CLASSDUMP" onMatch="DENY" onMismatch="NEUTRAL"/>
</filters>
</Root>
</Loggers>
</Configuration>
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,14 @@
import org.spongepowered.vanilla.launch.plugin.VanillaPluginManager;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLStreamHandler;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.Enumeration;

public abstract class VanillaLaunch extends Launch {

Expand Down Expand Up @@ -84,12 +90,44 @@ protected final void createPlatformPlugins(final PluginEngine pluginEngine) {
.orElseThrow(() -> new RuntimeException("The game directory has not been added to the environment!"));

try {
final Collection<PluginMetadata> read = PluginMetadataHelper.builder().build().read(VanillaLaunch.class.getResourceAsStream(
"/META-INF/" + JVMPluginResourceLocatorService.DEFAULT_METADATA_FILENAME));
// This is a bit nasty, but allows Sponge to detect builtin platform plugins when it's not the first entry on the classpath.
final URL classUrl = VanillaLaunch.class.getResource("/" + VanillaLaunch.class.getName().replace('.', '/') + ".class");

Collection<PluginMetadata> read = null;
if (classUrl.getProtocol().equals("file")) { // In development environment, we aren't even discovered by ModLauncher for some reason
read = PluginMetadataHelper.builder().build().read(VanillaLaunch.class.getResourceAsStream(
"/META-INF/" + JVMPluginResourceLocatorService.DEFAULT_METADATA_FILENAME));
} else if (classUrl.getProtocol().equals("jar")) { // In production
// Extract the path of the underlying jar file, and parse it as a path to normalize it
final String[] classUrlSplit = classUrl.getPath().split("!");
final Path expectedFile = Paths.get(new URI(classUrlSplit[0]));

// Then go through every possible resource
final Enumeration<URL> manifests =
VanillaLaunch.class.getClassLoader().getResources("/META-INF/" + JVMPluginResourceLocatorService.DEFAULT_METADATA_FILENAME);
while (manifests.hasMoreElements()) {
final URL next = manifests.nextElement();
if (!next.getProtocol().equals("jar")) {
continue;
}

// And stop when the normalized jar in that resource matches the URL of the jar that loaded VanillaLaunch?
final String[] pathSplit = next.getPath().split("!");
if (pathSplit.length == 2) {
if (Paths.get(new URI(pathSplit[0])).equals(expectedFile)) {
read = PluginMetadataHelper.builder().build().read(next.openStream());
break;
}
}
}
}
if (read == null) {
throw new RuntimeException("Could not determine location for implementation metadata!");
}
for (final PluginMetadata metadata : read) {
this.getPluginManager().addDummyPlugin(new DummyPluginContainer(metadata, gameDirectory, this.getLogger(), this));
}
} catch (final IOException e) {
} catch (final IOException | URISyntaxException e) {
throw new RuntimeException("Could not load metadata information for the implementation! This should be impossible!");
}
}
Expand Down

0 comments on commit ec1ed7a

Please sign in to comment.