mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-08-04 03:46:57 -04:00
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:
parent
4149876e04
commit
5d3660ffb8
@ -208,7 +208,8 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
|||||||
"javafx.graphics/com.sun.prism",
|
"javafx.graphics/com.sun.prism",
|
||||||
"javafx.controls/com.sun.javafx.scene.control",
|
"javafx.controls/com.sun.javafx.scene.control",
|
||||||
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
"javafx.controls/com.sun.javafx.scene.control.behavior",
|
||||||
"javafx.controls/javafx.scene.control.skin"
|
"javafx.controls/javafx.scene.control.skin",
|
||||||
|
"jdk.attach/sun.tools.attach"
|
||||||
).joinToString(" ")
|
).joinToString(" ")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -747,7 +747,7 @@ public final class LauncherHelper {
|
|||||||
|
|
||||||
if (showLogs)
|
if (showLogs)
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
logWindow = new LogWindow();
|
logWindow = new LogWindow(process);
|
||||||
logWindow.showNormal();
|
logWindow.showNormal();
|
||||||
logWindowLatch.countDown();
|
logWindowLatch.countDown();
|
||||||
});
|
});
|
||||||
|
@ -267,7 +267,7 @@ public class GameCrashWindow extends Stage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showLogWindow() {
|
private void showLogWindow() {
|
||||||
LogWindow logWindow = new LogWindow();
|
LogWindow logWindow = new LogWindow(managedProcess);
|
||||||
|
|
||||||
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
|
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
|
||||||
if (managedProcess.getClasspath() != null)
|
if (managedProcess.getClasspath() != null)
|
||||||
|
@ -36,13 +36,16 @@ import javafx.scene.control.*;
|
|||||||
import javafx.scene.input.KeyCode;
|
import javafx.scene.input.KeyCode;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.*;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.jackhuang.hmcl.game.GameDumpGenerator;
|
||||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||||
import org.jackhuang.hmcl.setting.Theme;
|
import org.jackhuang.hmcl.setting.Theme;
|
||||||
import org.jackhuang.hmcl.util.Holder;
|
import org.jackhuang.hmcl.util.Holder;
|
||||||
import org.jackhuang.hmcl.util.CircularArrayList;
|
import org.jackhuang.hmcl.util.CircularArrayList;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||||
|
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||||
|
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@ -64,7 +67,6 @@ import static org.jackhuang.hmcl.util.StringUtils.parseEscapeSequence;
|
|||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
|
||||||
* @author huangyuhui
|
* @author huangyuhui
|
||||||
*/
|
*/
|
||||||
public final class LogWindow extends Stage {
|
public final class LogWindow extends Stage {
|
||||||
@ -90,13 +92,17 @@ public final class LogWindow extends Stage {
|
|||||||
|
|
||||||
private boolean stopCheckLogCount = false;
|
private boolean stopCheckLogCount = false;
|
||||||
|
|
||||||
public LogWindow() {
|
private final ManagedProcess gameProcess;
|
||||||
|
|
||||||
|
public LogWindow(ManagedProcess gameProcess) {
|
||||||
setScene(new Scene(impl, 800, 480));
|
setScene(new Scene(impl, 800, 480));
|
||||||
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
getScene().getStylesheets().addAll(Theme.getTheme().getStylesheets(config().getLauncherFontFamily()));
|
||||||
setTitle(i18n("logwindow.title"));
|
setTitle(i18n("logwindow.title"));
|
||||||
getIcons().add(newBuiltinImage("/assets/img/icon.png"));
|
getIcons().add(newBuiltinImage("/assets/img/icon.png"));
|
||||||
|
|
||||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||||
|
|
||||||
|
this.gameProcess = gameProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void logLine(String filteredLine, Log4jLevel level) {
|
public void logLine(String filteredLine, Log4jLevel level) {
|
||||||
@ -219,6 +225,35 @@ public final class LogWindow extends Stage {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onExportDump(JFXButton button) {
|
||||||
|
thread(() -> {
|
||||||
|
if (button.getText().equals(i18n("logwindow.export_dump.dependency_ok.button"))) {
|
||||||
|
if (SystemUtils.supportJVMAttachment()) {
|
||||||
|
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.doing_button")));
|
||||||
|
|
||||||
|
Path dumpFile = Paths.get("minecraft-exported-jstack-dump-" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH-mm-ss")) + ".log").toAbsolutePath();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (gameProcess.isRunning()) {
|
||||||
|
GameDumpGenerator.writeDumpTo(gameProcess.getPID(), dumpFile);
|
||||||
|
FXUtils.showFileInExplorer(dumpFile);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOG.log(Level.WARNING, "Failed to create minecraft jstack dump", e);
|
||||||
|
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump.dependency_ok.button"));
|
||||||
|
alert.setTitle(i18n("message.error"));
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.runLater(() -> button.setText(i18n("logwindow.export_dump.dependency_ok.button")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void onExportGameCrashInfo() {
|
private void onExportGameCrashInfo() {
|
||||||
if (exportGameCrashInfoCallback == null) return;
|
if (exportGameCrashInfoCallback == null) return;
|
||||||
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
exportGameCrashInfoCallback.accept(logs.stream().map(x -> x.log).collect(Collectors.joining(OperatingSystem.LINE_SEPARATOR)));
|
||||||
@ -303,7 +338,7 @@ public final class LogWindow extends Stage {
|
|||||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||||
{
|
{
|
||||||
getStyleClass().add("log-window-list-cell");
|
getStyleClass().add("log-window-list-cell");
|
||||||
Region clippedContainer = (Region)listView.lookup(".clipped-container");
|
Region clippedContainer = (Region) listView.lookup(".clipped-container");
|
||||||
if (clippedContainer != null) {
|
if (clippedContainer != null) {
|
||||||
maxWidthProperty().bind(clippedContainer.widthProperty());
|
maxWidthProperty().bind(clippedContainer.widthProperty());
|
||||||
prefWidthProperty().bind(clippedContainer.widthProperty());
|
prefWidthProperty().bind(clippedContainer.widthProperty());
|
||||||
@ -398,15 +433,25 @@ public final class LogWindow extends Stage {
|
|||||||
autoScrollCheckBox.setSelected(true);
|
autoScrollCheckBox.setSelected(true);
|
||||||
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
control.autoScroll.bind(autoScrollCheckBox.selectedProperty());
|
||||||
|
|
||||||
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
|
||||||
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
|
|
||||||
|
|
||||||
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
JFXButton exportLogsButton = new JFXButton(i18n("button.export"));
|
||||||
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
|
exportLogsButton.setOnMouseClicked(e -> getSkinnable().onExportLogs());
|
||||||
|
|
||||||
|
JFXButton terminateButton = new JFXButton(i18n("logwindow.terminate_game"));
|
||||||
|
terminateButton.setOnMouseClicked(e -> getSkinnable().onTerminateGame());
|
||||||
|
|
||||||
|
JFXButton exportDumpButton = new JFXButton();
|
||||||
|
if (SystemUtils.supportJVMAttachment()) {
|
||||||
|
exportDumpButton.setText(i18n("logwindow.export_dump.dependency_ok.button"));
|
||||||
|
exportDumpButton.setOnAction(e -> getSkinnable().onExportDump(exportDumpButton));
|
||||||
|
} else {
|
||||||
|
exportDumpButton.setText(i18n("logwindow.export_dump.no_dependency.button"));
|
||||||
|
exportDumpButton.setTooltip(new Tooltip(i18n("logwindow.export_dump.no_dependency.tooltip")));
|
||||||
|
exportDumpButton.setDisable(true);
|
||||||
|
}
|
||||||
|
|
||||||
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
JFXButton clearButton = new JFXButton(i18n("button.clear"));
|
||||||
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
clearButton.setOnMouseClicked(e -> getSkinnable().onClear());
|
||||||
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, clearButton);
|
hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpButton, clearButton);
|
||||||
|
|
||||||
vbox.getChildren().add(bottom);
|
vbox.getChildren().add(bottom);
|
||||||
}
|
}
|
||||||
|
@ -723,6 +723,10 @@ logwindow.title=Log
|
|||||||
logwindow.help=You can go to the HMCL community and find others to help
|
logwindow.help=You can go to the HMCL community and find others to help
|
||||||
logwindow.autoscroll=Auto-scroll
|
logwindow.autoscroll=Auto-scroll
|
||||||
logwindow.export_game_crash_logs=Export Crash Logs
|
logwindow.export_game_crash_logs=Export Crash Logs
|
||||||
|
logwindow.export_dump.dependency_ok.button=Export Game Stack Dump
|
||||||
|
logwindow.export_dump.dependency_ok.doing_button=Exporting Game Stack Dump (May take up to 15 seconds)
|
||||||
|
logwindow.export_dump.no_dependency.button=Export Game Stack Dump (Not compatible)
|
||||||
|
logwindow.export_dump.no_dependency.tooltip=Your Java does not contain the dependencies to create the stack dump. Please turn to HMCL KOOK or HMCL Discord for help.
|
||||||
|
|
||||||
main_page=Home
|
main_page=Home
|
||||||
|
|
||||||
|
@ -668,6 +668,11 @@ logwindow.title=Registro
|
|||||||
logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar
|
logwindow.help=Puede ir a la comunidad HMCL y encontrar a otros para ayudar
|
||||||
logwindow.autoscroll=Desplazamiento automático
|
logwindow.autoscroll=Desplazamiento automático
|
||||||
logwindow.export_game_crash_logs=Exportar registros de errores
|
logwindow.export_game_crash_logs=Exportar registros de errores
|
||||||
|
logwindow.export_dump.dependency_ok.button=Exportar volcado de pila de juegos
|
||||||
|
logwindow.export_dump.dependency_ok.doing_button=Exportación de volcado de pila de juego (puede tardar hasta 15 segundos)
|
||||||
|
logwindow.export_dump.no_dependency.button=Exportar volcado de pila de juegos (no compatible)
|
||||||
|
logwindow.export_dump.no_dependency.tooltip=Su Java no contiene las dependencias para crear el volcado de pila. Dirígete a HMCL KOOK o HMCL Discord para obtener ayuda.
|
||||||
|
|
||||||
|
|
||||||
main_page=Inicio
|
main_page=Inicio
|
||||||
|
|
||||||
|
@ -597,6 +597,10 @@ logwindow.title=記錄
|
|||||||
logwindow.help=你可以前往 HMCL 社區,尋找他人幫助
|
logwindow.help=你可以前往 HMCL 社區,尋找他人幫助
|
||||||
logwindow.autoscroll=自動滾動
|
logwindow.autoscroll=自動滾動
|
||||||
logwindow.export_game_crash_logs=導出遊戲崩潰訊息
|
logwindow.export_game_crash_logs=導出遊戲崩潰訊息
|
||||||
|
logwindow.export_dump.dependency_ok.button=導出遊戲運行棧
|
||||||
|
logwindow.export_dump.dependency_ok.doing_button=正在導出遊戲運行棧(可能需要 15 秒)
|
||||||
|
logwindow.export_dump.no_dependency.button=導出遊戲運行棧(不兼容)
|
||||||
|
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用於創建遊戲運行棧的依賴。請前往 HMCL KOOK 或 HMCL Discord 尋求幫助。
|
||||||
|
|
||||||
main_page=首頁
|
main_page=首頁
|
||||||
|
|
||||||
|
@ -596,6 +596,10 @@ logwindow.title=日志
|
|||||||
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
|
logwindow.help=你可以前往 HMCL 社区,寻找他人帮助
|
||||||
logwindow.autoscroll=自动滚动
|
logwindow.autoscroll=自动滚动
|
||||||
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
logwindow.export_game_crash_logs=导出游戏崩溃信息
|
||||||
|
logwindow.export_dump.dependency_ok.button=导出游戏运行栈
|
||||||
|
logwindow.export_dump.dependency_ok.doing_button=正在导出游戏运行栈(可能需要 15 秒)
|
||||||
|
logwindow.export_dump.no_dependency.button=导出游戏运行栈(不兼容)
|
||||||
|
logwindow.export_dump.no_dependency.tooltip=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL KOOK 或 HMCL Discord 寻求帮助。
|
||||||
|
|
||||||
main_page=主页
|
main_page=主页
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import kotlin.streams.toList
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
`java-library`
|
`java-library`
|
||||||
}
|
}
|
||||||
@ -13,4 +15,20 @@ dependencies {
|
|||||||
api("org.nanohttpd:nanohttpd:2.3.1")
|
api("org.nanohttpd:nanohttpd:2.3.1")
|
||||||
api("org.apache.commons:commons-compress:1.23.0")
|
api("org.apache.commons:commons-compress:1.23.0")
|
||||||
compileOnlyApi("org.jetbrains:annotations:24.0.1")
|
compileOnlyApi("org.jetbrains:annotations:24.0.1")
|
||||||
|
compileOnlyApi("com.github.burningtnt:BytecodeImplGenerator:b45b6638eeaeb903aa22ea947d37c45e5716a18c")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.getByName<JavaCompile>("compileJava") {
|
||||||
|
val bytecodeClasses = listOf(
|
||||||
|
"org/jackhuang/hmcl/util/platform/ManagedProcess"
|
||||||
|
)
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
javaexec {
|
||||||
|
classpath(project.sourceSets["main"].compileClasspath)
|
||||||
|
mainClass.set("net.burningtnt.bcigenerator.BytecodeImplGenerator")
|
||||||
|
System.getProperty("bci.debug.address")?.let { address -> jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,address=$address,suspend=y") }
|
||||||
|
args(bytecodeClasses.stream().map { s -> project.layout.buildDirectory.file("classes/java/main/$s.class").get().asFile.path }.toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
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.launch.StreamPump;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
@ -33,7 +36,6 @@ import java.util.function.Predicate;
|
|||||||
* @see org.jackhuang.hmcl.launch.StreamPump
|
* @see org.jackhuang.hmcl.launch.StreamPump
|
||||||
*/
|
*/
|
||||||
public class ManagedProcess {
|
public class ManagedProcess {
|
||||||
|
|
||||||
private final Process process;
|
private final Process process;
|
||||||
private final List<String> commands;
|
private final List<String> commands;
|
||||||
private final String classpath;
|
private final String classpath;
|
||||||
@ -81,6 +83,58 @@ public class ManagedProcess {
|
|||||||
return process;
|
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.
|
* The command line.
|
||||||
*
|
*
|
||||||
|
@ -41,8 +41,12 @@ public final class SystemUtils {
|
|||||||
return managedProcess.getProcess().waitFor();
|
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) {
|
private static void onLogLine(String log) {
|
||||||
LOG.info(log);
|
LOG.info(log);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ subprojects {
|
|||||||
}
|
}
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven(url = "https://jitpack.io")
|
maven(url = "https://jitpack.io")
|
||||||
|
maven(url = "https://libraries.minecraft.net")
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.withType<JavaCompile> {
|
tasks.withType<JavaCompile> {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user