Enable HMCL to export jstack dump file 让 HMCL 能够导出游戏运行栈文件 (#2582)

* Enable HMCL to create game thread dump while game is running

* Fix checkstyle

* Hide accessToken

* Code cleanup

* Code cleanup

* Enhance I18N and declare the charset (UTF-8) of output file

* Inline variables

* Update the modifier of org.jackhuang.hmcl.game.GameDumpCreator#writeDumpHeadTo from public to private

* Refactor

* Add license for GameDumpCreator, remove support for Java 8

* Remove unnecessary Arrays.copyOf

* Fix checkstyle

* Use system charset to read the inputstream from JVM

* opt GameDumpCreator

* retry on failed attach to vm

* update GameDumpCreator

* Opt GameDumpCreator

* Fix

* Include BCIG

* Use BCIG to get PID.

* Fix.

* Fix again.

* Code cleanup. Fix bugs.

---------

Co-authored-by: Glavo <zjx001202@gmail.com>
This commit is contained in:
Burning_TNT
2024-01-08 20:35:46 +08:00
committed by GitHub
parent 4149876e04
commit 5d3660ffb8
13 changed files with 326 additions and 12 deletions

View File

@@ -0,0 +1,174 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;
import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.io.*;
import java.nio.CharBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
/**
* Generate a JVM dump on a process.
* WARNING: Initializing this class may cause NoClassDefFoundError.
*/
public final class GameDumpGenerator {
private GameDumpGenerator() {
}
private static final int TOOL_VERSION = 9;
private static final int DUMP_TIME = 3;
private static final int RETRY_TIME = 3;
public static void writeDumpTo(long pid, Path path) throws IOException, InterruptedException {
try (Writer writer = Files.newBufferedWriter(path)) {
// On a local machine, the lvmid and the pid are the same.
VirtualMachine vm = attachVM(String.valueOf(pid), writer);
try {
writeDumpHeadTo(vm, writer);
for (int i = 0; i < DUMP_TIME; i++) {
if (i > 0)
Thread.sleep(3000);
writer.write("====================\n");
writeDumpBodyTo(vm, writer);
}
} finally {
vm.detach();
}
}
}
private static void writeDumpHeadTo(VirtualMachine vm, Writer writer) throws IOException {
writer.write("===== Minecraft JStack Dump =====\n");
writeDumpHeadKeyValueTo("Tool Version", String.valueOf(TOOL_VERSION), writer, false);
writeDumpHeadKeyValueTo("VM PID", vm.id(), writer, false);
StringBuilder stringBuilder = new StringBuilder();
{
execute(vm, "VM.command_line", stringBuilder);
writeDumpHeadKeyValueTo(
"VM Command Line",
Logging.filterForbiddenToken(stringBuilder.toString()),
writer,
true
);
}
{
stringBuilder.setLength(0);
execute(vm, "VM.version", stringBuilder);
writeDumpHeadKeyValueTo("VM Version", stringBuilder.toString(), writer, true);
}
writer.write("\n\n");
}
public static void writeDumpHeadKeyValueTo(String key, String value, Writer writer, boolean multiline) throws IOException {
writer.write(key);
writer.write(':');
writer.write(' ');
if (multiline) {
writer.write('{');
writer.write('\n');
int lineStart = 0;
int lineEnd = value.indexOf("\n", lineStart);
while (true) {
if (lineEnd == -1) {
if (lineStart < value.length()) {
writer.write(" ");
writer.write(value, lineStart, value.length() - lineStart);
writer.write('\n');
}
break;
} else {
writer.write(" ");
writer.write(value, lineStart, lineEnd - lineStart);
writer.write('\n');
lineStart = lineEnd + 1;
lineEnd = value.indexOf("\n", lineStart);
}
}
writer.write('}');
} else {
writer.write(value);
}
writer.write('\n');
}
private static void writeDumpBodyTo(VirtualMachine vm, Writer writer) throws IOException {
execute(vm, "Thread.print", writer);
}
private static VirtualMachine attachVM(String lvmid, Writer writer) throws IOException, InterruptedException {
for (int i = 0; i < RETRY_TIME; i++) {
try {
return VirtualMachine.attach(lvmid);
} catch (Throwable e) {
LOG.log(Level.WARNING, "An exception encountered while attaching vm " + lvmid, e);
writer.write(StringUtils.getStackTrace(e));
writer.write('\n');
Thread.sleep(3000);
}
}
String message = "Cannot attach VM " + lvmid;
writer.write(message);
throw new IOException(message);
}
private static void execute(VirtualMachine vm, String command, Appendable target) throws IOException {
try (Reader reader = new InputStreamReader(executeJVMCommand(vm, command), OperatingSystem.NATIVE_CHARSET)) {
char[] data = new char[256];
CharBuffer cb = CharBuffer.wrap(data);
int len;
while ((len = reader.read(data)) > 0) { // Directly read the data into a CharBuffer would cause useless array copy actions.
target.append(cb, 0, len);
}
} catch (Throwable throwable) {
LOG.log(Level.WARNING, "An exception encountered while executing jcmd " + vm.id(), throwable);
target.append(StringUtils.getStackTrace(throwable));
target.append('\n');
}
}
private static InputStream executeJVMCommand(VirtualMachine vm, String command) throws IOException, AttachNotSupportedException {
if (vm instanceof sun.tools.attach.HotSpotVirtualMachine) {
return ((sun.tools.attach.HotSpotVirtualMachine) vm).executeJCmd(command);
} else {
throw new AttachNotSupportedException("Unsupported VM implementation " + vm.getClass().getName());
}
}
}

View File

@@ -17,10 +17,13 @@
*/
package org.jackhuang.hmcl.util.platform;
import net.burningtnt.bcigenerator.api.BytecodeImpl;
import net.burningtnt.bcigenerator.api.BytecodeImplError;
import org.jackhuang.hmcl.launch.StreamPump;
import org.jackhuang.hmcl.util.Lang;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -33,7 +36,6 @@ import java.util.function.Predicate;
* @see org.jackhuang.hmcl.launch.StreamPump
*/
public class ManagedProcess {
private final Process process;
private final List<String> commands;
private final String classpath;
@@ -81,6 +83,58 @@ public class ManagedProcess {
return process;
}
/**
* The PID of the raw system process
*
* @throws UnsupportedOperationException if current Java environment is not supported.
* @return PID
*/
public long getPID() throws UnsupportedOperationException {
if (JavaVersion.CURRENT_JAVA.getParsedVersion() >= 9) {
// Method Process.pid() is provided (Java 9 or later). Invoke it to get the pid.
return getPID0(process);
} else {
// Method Process.pid() is not provided. (Java 8).
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
// On Windows, we can invoke method Process.pid() to get the pid.
// However, this method is supplied since Java 9.
// So, there is no ways to get the pid.
throw new UnsupportedOperationException("Cannot get the pid of a Process on Java 8 on Windows.");
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX || OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
// On Linux or Mac, we can get field UnixProcess.pid field to get the pid.
// All the Java version is accepted.
// See https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/solaris/classes/java/lang/UNIXProcess.java.linux
try {
Field pidField = process.getClass().getDeclaredField("pid");
pidField.setAccessible(true);
return pidField.getInt(process);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new UnsupportedOperationException("Cannot get the pid of a Process on Java 8 on OSX / Linux.", e);
}
} else {
// Unknown Operating System, no fallback available.
throw new UnsupportedOperationException(String.format("Cannot get the pid of a Process on Java 8 on Unknown Operating System (%s).", System.getProperty("os.name")));
}
}
}
/**
* Get the PID of a process with BytecodeImplGenerator
*/
@BytecodeImpl({
"LABEL METHOD_HEAD",
"ALOAD 0",
"INVOKEVIRTUAL Ljava/lang/Process;pid()J",
"LABEL RELEASE_PARAMETER",
"LRETURN",
"LOCALVARIABLE process [Ljava/lang/Process; METHOD_HEAD RELEASE_PARAMETER 0",
"MAXS 2 1"
})
@SuppressWarnings("unused")
private static long getPID0(Process process) {
throw new BytecodeImplError();
}
/**
* The command line.
*

View File

@@ -41,8 +41,12 @@ public final class SystemUtils {
return managedProcess.getProcess().waitFor();
}
public static boolean supportJVMAttachment() {
return JavaVersion.CURRENT_JAVA.getParsedVersion() >= 9
&& Thread.currentThread().getContextClassLoader().getResource("com/sun/tools/attach/VirtualMachine.class") != null;
}
private static void onLogLine(String log) {
LOG.info(log);
}
}