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:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user