Skip to content

Commit

Permalink
launch: More robust plugin classpath and manifest handling (#3257)
Browse files Browse the repository at this point in the history
  • Loading branch information
zml2008 authored Jan 17, 2021
1 parent df9c5a0 commit 7976197
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 28 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 @@ -30,24 +30,33 @@
import cpw.mods.modlauncher.api.ITransformingClassLoaderBuilder;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.spongepowered.plugin.PluginCandidate;
import org.spongepowered.plugin.PluginLanguageService;
import org.spongepowered.plugin.PluginResource;
import org.spongepowered.plugin.jvm.locator.JVMPluginResource;
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 +95,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 +123,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<List<PluginResource>> serviceResources =
Main.getInstance().getPluginEngine().getResources().values().iterator();
Iterator<PluginResource> resources;
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.resources != null && !this.resources.hasNext()) {
this.resources = null;
}
if (this.resources == null) {
if (!this.serviceResources.hasNext()) {
return null;
}
this.resources = this.serviceResources.next().iterator();
}

if (this.resources.hasNext()) {
final PluginResource resource = this.resources.next();
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);

This comment has been minimized.

Copy link
@pschichtel

pschichtel Jan 29, 2021

Contributor

just randomly noticed this: Shouldn't that be Pattern.compile(File.pathSeparator, Pattern.LITERAL); ? (Linux user speaking here)

This comment has been minimized.

Copy link
@zml2008

zml2008 Jan 30, 2021

Author Member

err, yes it should be -- thanks for pointing that out


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 7976197

Please sign in to comment.