Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

launch: More robust plugin classpath and manifest handling #3257

Merged
merged 2 commits into from
Jan 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);

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