Skip to content

Commit d794aa1

Browse files
committed
Implement client watchdog
1 parent d48ec15 commit d794aa1

File tree

5 files changed

+121
-1
lines changed

5 files changed

+121
-1
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
version = getSubprojectVersion(project)
2+
3+
moduleDependencies(project, [':fabric-lifecycle-events-v1'])
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package net.fabricmc.fabric.impl.client.crash.report.info;
18+
19+
import java.nio.file.Path;
20+
import java.util.Locale;
21+
22+
import com.mojang.logging.LogUtils;
23+
import org.slf4j.Logger;
24+
25+
import net.minecraft.Bootstrap;
26+
import net.minecraft.client.MinecraftClient;
27+
import net.minecraft.server.dedicated.DedicatedServerWatchdog;
28+
import net.minecraft.util.Util;
29+
import net.minecraft.util.crash.CrashReport;
30+
import net.minecraft.util.crash.ReportType;
31+
32+
import net.fabricmc.api.ClientModInitializer;
33+
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
34+
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
35+
import net.fabricmc.fabric.mixin.client.crash.report.info.MinecraftClientAccessor;
36+
import net.fabricmc.loader.api.FabricLoader;
37+
38+
public class ClientWatchdog implements ClientModInitializer {
39+
private static final Logger LOGGER = LogUtils.getLogger();
40+
private static final int DEFAULT_MAX_TIME_MS = 30000;
41+
private static final boolean ENABLED = FabricLoader.getInstance().isDevelopmentEnvironment() || Boolean.getBoolean("fabric.clientWatchdog.enabled");
42+
private static final int MAX_TIME_MS = Integer.getInteger("fabric.clientWatchdog.maxTimeMs", DEFAULT_MAX_TIME_MS);
43+
private volatile long tickStartTimeMs = -1;
44+
45+
@Override
46+
public void onInitializeClient() {
47+
if (!ENABLED) return;
48+
ClientTickEvents.START_CLIENT_TICK.register((client) -> tickStartTimeMs = Util.getMeasuringTimeMs());
49+
ClientLifecycleEvents.CLIENT_STARTED.register((client) -> {
50+
Thread thread = new Thread(() -> run(client));
51+
thread.setName("Fabric Client Watchdog");
52+
thread.setDaemon(true);
53+
thread.start();
54+
});
55+
}
56+
57+
public void run(MinecraftClient client) {
58+
while (client.isRunning()) {
59+
long tickStartTime = this.tickStartTimeMs;
60+
long currentTime = Util.getMeasuringTimeMs();
61+
long deltaMs = currentTime - tickStartTime;
62+
63+
if (tickStartTime >= 0 && deltaMs > MAX_TIME_MS) {
64+
LOGGER.error(
65+
LogUtils.FATAL_MARKER,
66+
"A single client tick took {} seconds (should be max {})",
67+
String.format(Locale.ROOT, "%.2f", (float) deltaMs / 1000),
68+
String.format(Locale.ROOT, "%.2f", (float) MAX_TIME_MS / 1000)
69+
);
70+
LOGGER.error(LogUtils.FATAL_MARKER, "Considering it to be crashed, client will forcibly shutdown.");
71+
CrashReport report = DedicatedServerWatchdog.createCrashReport("Fabric Client Watchdog", ((MinecraftClientAccessor) client).getThread().threadId());
72+
client.addDetailsToCrashReport(report);
73+
Bootstrap.println("Crash report:\n" + report.asString(ReportType.MINECRAFT_CRASH_REPORT));
74+
Path path = client.runDirectory.toPath().resolve("crash-reports").resolve("crash-" + Util.getFormattedCurrentTime() + "-client.txt");
75+
76+
if (report.writeToFile(path, ReportType.MINECRAFT_CRASH_REPORT)) {
77+
LOGGER.error("This crash report has been saved to: {}", path.toAbsolutePath());
78+
} else {
79+
LOGGER.error("We were unable to save this crash report to disk.");
80+
}
81+
82+
System.exit(1);
83+
}
84+
}
85+
}
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.fabricmc.fabric.mixin.client.crash.report.info;
2+
3+
import org.spongepowered.asm.mixin.Mixin;
4+
import org.spongepowered.asm.mixin.gen.Accessor;
5+
6+
import net.minecraft.client.MinecraftClient;
7+
8+
@Mixin(MinecraftClient.class)
9+
public interface MinecraftClientAccessor {
10+
@Accessor
11+
Thread getThread();
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"required": true,
3+
"package": "net.fabricmc.fabric.mixin.client.crash.report.info",
4+
"compatibilityLevel": "JAVA_21",
5+
"client": [
6+
"MinecraftClientAccessor"
7+
],
8+
"injectors": {
9+
"defaultRequire": 1
10+
}
11+
}

fabric-crash-report-info-v1/src/main/resources/fabric.mod.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,17 @@
1919
"fabricloader": ">=0.16.4"
2020
},
2121
"description": "Adds Fabric-related debug info to crash reports.",
22+
"entrypoints": {
23+
"client": [
24+
"net.fabricmc.fabric.impl.client.crash.report.info.ClientWatchdog"
25+
]
26+
},
2227
"mixins": [
23-
"fabric-crash-report-info-v1.mixins.json"
28+
"fabric-crash-report-info-v1.mixins.json",
29+
{
30+
"config": "fabric-crash-report-info-v1.client.mixins.json",
31+
"environment": "client"
32+
}
2433
],
2534
"custom": {
2635
"fabric-api:module-lifecycle": "stable"

0 commit comments

Comments
 (0)