Skip to content

Commit

Permalink
Stop Gradle daemon after unrecoverable zip errors (#1113)
Browse files Browse the repository at this point in the history
  • Loading branch information
modmuss50 committed May 2, 2024
1 parent 2752dc3 commit be1e207
Show file tree
Hide file tree
Showing 8 changed files with 455 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ public void run() {
extension.setDependencyManager(dependencyManager);
dependencyManager.handleDependencies(getProject(), serviceManager);
} catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
ExceptionUtil.processException(e, getProject());
disownLock();
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to setup Minecraft", e);
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ public void run() throws IOException {
try (var timer = new Timer("Decompiled sources")) {
runWithoutCache();
} catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
ExceptionUtil.processException(e, getProject());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to decompile", e);
}

Expand All @@ -222,7 +222,7 @@ public void run() throws IOException {
runWithCache(fs.getRoot());
}
} catch (Exception e) {
ExceptionUtil.printFileLocks(e, getProject());
ExceptionUtil.processException(e, getProject());
throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to decompile", e);
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/net/fabricmc/loom/util/ExceptionUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import net.fabricmc.loom.nativeplatform.LoomNativePlatform;
import net.fabricmc.loom.nativeplatform.LoomNativePlatformException;
import net.fabricmc.loom.util.gradle.daemon.DaemonUtils;

public final class ExceptionUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ExceptionUtil.class);
Expand All @@ -59,17 +60,24 @@ public static <E, C extends Throwable> E createDescriptiveWrapper(BiFunction<Str
return constructor.apply(descriptiveMessage, cause);
}

public static void printFileLocks(Throwable e, Project project) {
public static void processException(Throwable e, Project project) {
Throwable cause = e;
boolean unrecoverable = false;

while (cause != null) {
if (cause instanceof FileSystemException fse) {
if (cause instanceof FileSystemUtil.UnrecoverableZipException) {
unrecoverable = true;
} else if (cause instanceof FileSystemException fse) {
printFileLocks(fse.getFile(), project);
break;
}

cause = cause.getCause();
}

if (unrecoverable) {
DaemonUtils.tryStopGradleDaemon(project);
}
}

private static void printFileLocks(String filename, Project project) {
Expand Down
55 changes: 49 additions & 6 deletions src/main/java/net/fabricmc/loom/util/FileSystemUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
Expand All @@ -38,7 +41,7 @@
import net.fabricmc.tinyremapper.FileSystemReference;

public final class FileSystemUtil {
public record Delegate(FileSystemReference reference) implements AutoCloseable, Supplier<FileSystem> {
public record Delegate(FileSystemReference reference, URI uri) implements AutoCloseable, Supplier<FileSystem> {
public Path getPath(String path, String... more) {
return get().getPath(path, more);
}
Expand Down Expand Up @@ -69,7 +72,31 @@ public String readString(String path) throws IOException {

@Override
public void close() throws IOException {
reference.close();
try {
reference.close();
} catch (IOException e) {
// An IOException can only ever be thrown by the underlying FileSystem.close() call in tiny remapper
// This means that this reference was the last open
try {
// We would then almost always expect this to throw a FileSystemNotFoundException
FileSystem fileSystem = FileSystems.getFileSystem(uri);

if (fileSystem.isOpen()) {
// Or the unlikely chance that another thread opened a new reference
throw e;
}

// However if we end up here, the closed FileSystem was not removed from ZipFileSystemProvider.filesystems
// This leaves us in a broken state, preventing this JVM from ever being able to open a zip at this path.
// See: https://bugs.openjdk.org/browse/JDK-8291712
throw new UnrecoverableZipException(e.getMessage(), e);
} catch (FileSystemNotFoundException ignored) {
// This the "happy" case, where the zip FS failed to close but was
}

// Throw the normal exception, we can recover from this
throw e;
}
}

@Override
Expand All @@ -87,18 +114,34 @@ private FileSystemUtil() {
}

public static Delegate getJarFileSystem(File file, boolean create) throws IOException {
return new Delegate(FileSystemReference.openJar(file.toPath(), create));
return new Delegate(FileSystemReference.openJar(file.toPath(), create), toJarUri(file.toPath()));
}

public static Delegate getJarFileSystem(Path path, boolean create) throws IOException {
return new Delegate(FileSystemReference.openJar(path, create));
return new Delegate(FileSystemReference.openJar(path, create), toJarUri(path));
}

public static Delegate getJarFileSystem(Path path) throws IOException {
return new Delegate(FileSystemReference.openJar(path));
return new Delegate(FileSystemReference.openJar(path), toJarUri(path));
}

public static Delegate getJarFileSystem(URI uri, boolean create) throws IOException {
return new Delegate(FileSystemReference.open(uri, create));
return new Delegate(FileSystemReference.open(uri, create), uri);
}

private static URI toJarUri(Path path) {
URI uri = path.toUri();

try {
return new URI("jar:" + uri.getScheme(), uri.getHost(), uri.getPath(), uri.getFragment());
} catch (URISyntaxException e) {
throw new RuntimeException("can't convert path "+path+" to uri", e);
}
}

public static class UnrecoverableZipException extends RuntimeException {
public UnrecoverableZipException(String message, Throwable cause) {
super(message, cause);
}
}
}
124 changes: 124 additions & 0 deletions src/main/java/net/fabricmc/loom/util/gradle/daemon/DaemonUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package net.fabricmc.loom.util.gradle.daemon;

import java.nio.file.Path;
import java.util.List;
import java.util.UUID;

import org.gradle.api.Project;
import org.gradle.cache.FileLockManager;
import org.gradle.internal.file.Chmod;
import org.gradle.internal.remote.internal.RemoteConnection;
import org.gradle.internal.remote.internal.inet.TcpOutgoingConnector;
import org.gradle.internal.serialize.Serializers;
import org.gradle.internal.service.ServiceRegistry;
import org.gradle.invocation.DefaultGradle;
import org.gradle.launcher.daemon.client.DaemonClientConnection;
import org.gradle.launcher.daemon.client.StopDispatcher;
import org.gradle.launcher.daemon.protocol.DaemonMessageSerializer;
import org.gradle.launcher.daemon.protocol.Message;
import org.gradle.launcher.daemon.protocol.StopWhenIdle;
import org.gradle.launcher.daemon.registry.DaemonInfo;
import org.gradle.launcher.daemon.registry.DaemonRegistry;
import org.gradle.launcher.daemon.registry.PersistentDaemonRegistry;
import org.gradle.util.GradleVersion;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* This uses a vast amount of Gradle internal APIs, however this is only used when the JVM is in an unrecoverable state.
* The alternative is to kill the JVM, using System.exit, which is not ideal and leaves scary messages in the log.
*/
public final class DaemonUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(DaemonUtils.class);

private DaemonUtils() {
}

/**
* Request the Gradle daemon to stop when it becomes idle.
*/
public static void tryStopGradleDaemon(Project project) {
try {
stopWhenIdle(project);
} catch (Throwable t) {
LOGGER.error("Failed to request the Gradle demon to stop", t);
}
}

@VisibleForTesting
public static boolean stopWhenIdle(Project project) {
DaemonInfo daemonInfo = findCurrentDaemon(project);

if (daemonInfo == null) {
return false;
}

RemoteConnection<Message> connection = null;

try {
// Gradle communicates with the daemon using a TCP connection, and a custom binary protocol.
// We connect to the daemon using the daemon's address, and then send a StopWhenIdle message.
connection = new TcpOutgoingConnector().connect(daemonInfo.getAddress()).create(Serializers.stateful(DaemonMessageSerializer.create(null)));
DaemonClientConnection daemonClientConnection = new DaemonClientConnection(connection, daemonInfo, null);
new StopDispatcher().dispatch(daemonClientConnection, new StopWhenIdle(UUID.randomUUID(), daemonInfo.getToken()));
} finally {
if (connection != null) {
connection.stop();
}
}

LOGGER.warn("Requested Gradle daemon to stop on exit.");
return true;
}

@Nullable
private static DaemonInfo findCurrentDaemon(Project project) {
// Gradle maintains a list of running daemons in a registry.bin file.
final Path registryBin = project.getGradle().getGradleUserHomeDir().toPath().resolve("daemon").resolve(GradleVersion.current().getVersion()).resolve("registry.bin");
project.getLogger().lifecycle("Looking for daemon in: " + registryBin);

// We can use a PersistentDaemonRegistry to read this
final ServiceRegistry services = ((DefaultGradle) project.getGradle()).getServices();
final DaemonRegistry registry = new PersistentDaemonRegistry(registryBin.toFile(), services.get(FileLockManager.class), services.get(Chmod.class));

final long pid = ProcessHandle.current().pid();
final List<DaemonInfo> runningDaemons = registry.getAll();

LOGGER.info("Found {} running Gradle daemons in registry: {}", runningDaemons.size(), registryBin);

for (DaemonInfo daemonInfo : runningDaemons) {
if (daemonInfo.getPid() == pid) {
return daemonInfo;
}
}

LOGGER.warn("Could not find current process in daemon registry: {}", registryBin);
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2024 FabricMC
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/

package net.fabricmc.loom.test.integration

import spock.lang.Specification
import spock.lang.Unroll

import net.fabricmc.loom.test.util.GradleProjectTestTrait

import static net.fabricmc.loom.test.LoomTestConstants.STANDARD_TEST_VERSIONS
import static org.gradle.testkit.runner.TaskOutcome.SUCCESS

class DaemonShutdownTest extends Specification implements GradleProjectTestTrait {
@Unroll
def "custom decompiler (gradle #version)"() {
setup:
def gradle = gradleProject(project: "minimalBase", version: version)
gradle.buildSrc("stopDaemon")
gradle.buildGradle << '''
dependencies {
minecraft "com.mojang:minecraft:1.20.4"
mappings "net.fabricmc:yarn:1.20.4+build.3:v2"
}
'''
when:
def result = gradle.run(task: "help")
then:
result.task(":help").outcome == SUCCESS
where:
version << STANDARD_TEST_VERSIONS
}
}
Loading

0 comments on commit be1e207

Please sign in to comment.