diff --git a/fabric-crash-report-info-v1/build.gradle b/fabric-crash-report-info-v1/build.gradle index 85b5e378d4..078667c61c 100644 --- a/fabric-crash-report-info-v1/build.gradle +++ b/fabric-crash-report-info-v1/build.gradle @@ -1 +1,3 @@ version = getSubprojectVersion(project) + +moduleDependencies(project, [':fabric-lifecycle-events-v1']) diff --git a/fabric-crash-report-info-v1/src/client/java/net/fabricmc/fabric/impl/client/crash/report/info/ClientWatchdog.java b/fabric-crash-report-info-v1/src/client/java/net/fabricmc/fabric/impl/client/crash/report/info/ClientWatchdog.java new file mode 100644 index 0000000000..3ed4219afa --- /dev/null +++ b/fabric-crash-report-info-v1/src/client/java/net/fabricmc/fabric/impl/client/crash/report/info/ClientWatchdog.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * 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 net.fabricmc.fabric.impl.client.crash.report.info; + +import java.nio.file.Path; +import java.util.Locale; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import net.minecraft.Bootstrap; +import net.minecraft.client.MinecraftClient; +import net.minecraft.server.dedicated.DedicatedServerWatchdog; +import net.minecraft.util.Util; +import net.minecraft.util.crash.CrashReport; +import net.minecraft.util.crash.ReportType; + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.mixin.client.crash.report.info.MinecraftClientAccessor; +import net.fabricmc.loader.api.FabricLoader; + +public class ClientWatchdog implements ClientModInitializer { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int DEFAULT_MAX_TIME_MS = 30000; + private static final boolean ENABLED = FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("fabric.clientWatchdog.enabled"); + private static final int MAX_TIME_MS = Integer.getInteger("fabric.clientWatchdog.maxTimeMs", DEFAULT_MAX_TIME_MS); + private volatile long tickStartTimeMs = -1; + + @Override + public void onInitializeClient() { + if (!ENABLED) return; + ClientTickEvents.START_CLIENT_TICK.register((client) -> tickStartTimeMs = Util.getMeasuringTimeMs()); + ClientLifecycleEvents.CLIENT_STARTED.register((client) -> { + Thread thread = new Thread(() -> run(client)); + thread.setName("Fabric Client Watchdog"); + thread.setDaemon(true); + thread.start(); + }); + } + + public void run(MinecraftClient client) { + while (client.isRunning()) { + long tickStartTime = this.tickStartTimeMs; + long currentTime = Util.getMeasuringTimeMs(); + long deltaMs = currentTime - tickStartTime; + + if (tickStartTime >= 0 && deltaMs > MAX_TIME_MS) { + LOGGER.error( + LogUtils.FATAL_MARKER, + "A single client tick took {} seconds (should be max {})", + String.format(Locale.ROOT, "%.2f", (float) deltaMs / 1000), + String.format(Locale.ROOT, "%.2f", (float) MAX_TIME_MS / 1000) + ); + LOGGER.error(LogUtils.FATAL_MARKER, "Considering it to be crashed, client will forcibly shutdown."); + CrashReport report = DedicatedServerWatchdog.createCrashReport("Fabric Client Watchdog", ((MinecraftClientAccessor) client).getThread().threadId()); + client.addDetailsToCrashReport(report); + Bootstrap.println("Crash report:\n" + report.asString(ReportType.MINECRAFT_CRASH_REPORT)); + Path path = client.runDirectory.toPath().resolve("crash-reports").resolve("crash-" + Util.getFormattedCurrentTime() + "-client.txt"); + + if (report.writeToFile(path, ReportType.MINECRAFT_CRASH_REPORT)) { + LOGGER.error("This crash report has been saved to: {}", path.toAbsolutePath()); + } else { + LOGGER.error("We were unable to save this crash report to disk."); + } + + System.exit(1); + } + } + } +} diff --git a/fabric-crash-report-info-v1/src/client/java/net/fabricmc/fabric/mixin/client/crash/report/info/MinecraftClientAccessor.java b/fabric-crash-report-info-v1/src/client/java/net/fabricmc/fabric/mixin/client/crash/report/info/MinecraftClientAccessor.java new file mode 100644 index 0000000000..7ebdbeb852 --- /dev/null +++ b/fabric-crash-report-info-v1/src/client/java/net/fabricmc/fabric/mixin/client/crash/report/info/MinecraftClientAccessor.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * 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 net.fabricmc.fabric.mixin.client.crash.report.info; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.client.MinecraftClient; + +@Mixin(MinecraftClient.class) +public interface MinecraftClientAccessor { + @Accessor + Thread getThread(); +} diff --git a/fabric-crash-report-info-v1/src/client/resources/fabric-crash-report-info-v1.client.mixins.json b/fabric-crash-report-info-v1/src/client/resources/fabric-crash-report-info-v1.client.mixins.json new file mode 100644 index 0000000000..e9c04db4b7 --- /dev/null +++ b/fabric-crash-report-info-v1/src/client/resources/fabric-crash-report-info-v1.client.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "net.fabricmc.fabric.mixin.client.crash.report.info", + "compatibilityLevel": "JAVA_21", + "client": [ + "MinecraftClientAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-crash-report-info-v1/src/main/resources/fabric.mod.json b/fabric-crash-report-info-v1/src/main/resources/fabric.mod.json index 57abf77152..ec75211347 100644 --- a/fabric-crash-report-info-v1/src/main/resources/fabric.mod.json +++ b/fabric-crash-report-info-v1/src/main/resources/fabric.mod.json @@ -19,8 +19,17 @@ "fabricloader": ">=0.16.4" }, "description": "Adds Fabric-related debug info to crash reports.", + "entrypoints": { + "client": [ + "net.fabricmc.fabric.impl.client.crash.report.info.ClientWatchdog" + ] + }, "mixins": [ - "fabric-crash-report-info-v1.mixins.json" + "fabric-crash-report-info-v1.mixins.json", + { + "config": "fabric-crash-report-info-v1.client.mixins.json", + "environment": "client" + } ], "custom": { "fabric-api:module-lifecycle": "stable"