Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
46d77bb
refactor: 引入 ProcessInfo 接口解耦游戏进程管理
fishdry1000 Sep 30, 2026
6833a66
chore: 清理 import
fishdry1000 Sep 30, 2026
4279e76
feat: 新增 HMCLMonitor 子项目,设置 HMCL_USE_HMCLMONITOR 环境变量启用。使用轻量进程监督游戏进程。
fishdry1000 Sep 30, 2026
95f95f8
feat: 游戏异常退出后由 monitor 重拉 HMCL 并复用崩溃窗
fishdry1000 Sep 30, 2026
1cf1ac9
chore: 清理代码
fishdry1000 Sep 30, 2026
958b4e4
fix: 修复手动取消会弹出崩溃窗口
fishdry1000 Oct 1, 2026
d23d263
style: 优化格式
fishdry1000 Oct 1, 2026
6524116
fix: 修复几处 corner case
fishdry1000 Oct 1, 2026
6f441d6
docs: 修缮注释
fishdry1000 Oct 1, 2026
d354dcb
fix: 使 GameRepository 的加载标志与快照发布原子可见
fishdry1000 Oct 4, 2026
36f8bfb
fix: 崩溃恢复按游戏目录持久化 ID 定位实例,替代全目录扫描
fishdry1000 Oct 4, 2026
42eb85c
fix: 限制日志读取长度
fishdry1000 Oct 4, 2026
2a6d4a0
docs: 优化注释
fishdry1000 Oct 4, 2026
9e54096
fix: 仓库加载失败时等待不再无限挂起
fishdry1000 Oct 4, 2026
fc82931
fix: 以协议流写入失败判定启动器存活,修复 Linux 下 relaunch 不触发
fishdry1000 Oct 5, 2026
20b3191
docs: 精简 monitor 相关注释
fishdry1000 Oct 5, 2026
ad7f93b
chore: 清理代码
fishdry1000 Oct 6, 2026
ab354dc
refactor: 重构拉起 monitor 的模型
fishdry1000 Oct 12, 2026
853374f
chore: 优化 import
fishdry1000 Oct 12, 2026
f1eb162
Merge branch 'main' into feature/daemon
fishdry1000 Oct 12, 2026
28008aa
refactor: 重构 MonitorCrashReporter 和 spec
fishdry1000 Oct 12, 2026
ac1583d
docs: 优化注释
fishdry1000 Oct 12, 2026
a19ee4b
Merge branch 'main' into feature/daemon
fishdry1000 Oct 20, 2026
52249ca
refactor: 监控进程内复用启动器 UI 展示崩溃窗口,替代重新拉起启动器
fishdry1000 Oct 20, 2026
5b8d22b
refactor: 封装 MonitorProtocol 协议流并统一父进程存活检测
fishdry1000 Oct 20, 2026
f8353e5
refactor: 移除 monitor 会话日志临时文件,崩溃窗口改用进程缓冲的输出行
fishdry1000 Oct 21, 2026
0f091d0
refactor: 复用默认 Gson 构建器并将 Path 注册为类型层级适配器
fishdry1000 Oct 21, 2026
75de472
refactor: 精简 MonitorSupervisor 启动流程
fishdry1000 Oct 21, 2026
f556211
refactor: 将 MonitorProtocol 消息封装为 sealed 类型并统一解析
fishdry1000 Oct 21, 2026
c3fcf70
refactor: 重命名 MonitorClient 为 MonitorLauncher 并将 MonitorSupervisor 改为…
fishdry1000 Oct 21, 2026
54f59c6
refactor: 将 ResultSpec 内联为 Monitor.Result 并复用取消标记常量
fishdry1000 Oct 21, 2026
df8c48c
refactor: 将 ProcessSpec 改为 record 并改用访问器方法
fishdry1000 Oct 21, 2026
23e9c3d
refactor: 将 ProcessSpec 内联为 Monitor.ProcessSpec
fishdry1000 Oct 21, 2026
f1286ef
refactor: 将 ProcessSpec 重命名为 MonitorLaunchSpec 并移动到 MonitorLauncher
fishdry1000 Oct 21, 2026
7843301
refactor: 收紧 Monitor 结果与启动参数的 gameDirectoryId 非空约束并简化启动流程
fishdry1000 Oct 21, 2026
ca0be5c
style: 添加空行
fishdry1000 Oct 21, 2026
c189018
refactor: 移除无用的字段
fishdry1000 Oct 21, 2026
fb9d9fb
docs: 优化注释
fishdry1000 Oct 21, 2026
1047089
refactor: 使用模式匹配简化 HMCLGameInstance 类型判断
fishdry1000 Oct 21, 2026
774c006
refactor: 将 MonitorLauncher 移至 HMCL 并简化监控启动与协议处理
fishdry1000 Oct 22, 2026
6cf505f
refactor: 将 MonitorGameProcess 重构为记录
fishdry1000 Oct 22, 2026
9b4427c
chore: 移除不需要的 import
fishdry1000 Oct 22, 2026
873bf1c
fix: 修复 token 泄露
fishdry1000 Oct 23, 2026
4fcf6d8
refactor: 使用控制通道管理 Monitor 的取消与退出处理
fishdry1000 Oct 23, 2026
3c9cfbb
fix: 跳过 QuickPlayOption
fishdry1000 Oct 23, 2026
7e4fa65
chore: 移除无用的导入
fishdry1000 Oct 23, 2026
0ac424b
refactor: 简化 Monitor 结果处理与崩溃窗口显示流程
fishdry1000 Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ language-subtag-registry
/HMCL/build/
/HMCLCore/build/
/HMCLBoot/build/
/HMCLMonitor/build/
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
/minecraft/libraries/HMCLMultiMCBootstrap/build/
/buildSrc/build/
Expand All @@ -42,6 +43,7 @@ language-subtag-registry
/out/
/HMCL/out/
/HMCLCore/out/
/HMCLMonitor/out/
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
/minecraft/libraries/HMCLMultiMCBootstrap/out/

Expand All @@ -50,6 +52,7 @@ language-subtag-registry
/HMCL/bin/
/HMCLBoot/bin/
/HMCLCore/bin/
/HMCLMonitor/bin/
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
/minecraft/libraries/HMCLMultiMCBootstrap/bin/
.classpath
Expand Down
2 changes: 2 additions & 0 deletions HMCL/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ val embedResources = configurations.register("embedResources")
dependencies {
implementation(project(":HMCLCore"))
implementation(project(":HMCLBoot"))
implementation(project(":HMCLMonitor"))
implementation("libs:JFoenix")
implementation(libs.jwebp)
implementation(libs.fxsvgimage)
Expand Down Expand Up @@ -212,6 +213,7 @@ tasks.shadowJar {
exclude(dependency("net.java.dev.jna:jna:.*"))
exclude(dependency("libs:JFoenix:.*"))
exclude(project(":HMCLBoot"))
exclude(project(":HMCLMonitor"))
}

manifest.attributes(
Expand Down
6 changes: 6 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/EntryPoint.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.util.SwingUtils;
import org.jackhuang.hmcl.monitor.Monitor;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
Expand Down Expand Up @@ -50,6 +51,11 @@ public static void main(String[] args) {
createHMCLDirectories();
LOG.start(Metadata.HMCL_LOCAL_HOME.resolve("logs"));

// start HMCL monitor before JavaFX initialized
if (Monitor.processArguments(args)) {
exit(0);
}

checkWine();

if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Expand Down
6 changes: 6 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
import javafx.stage.Screen;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.monitor.MonitorCrashReporter;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.monitor.Monitor;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
Expand Down Expand Up @@ -145,6 +147,10 @@ public void start(Stage primaryStage) {
UpdateChecker.init();

primaryStage.show();

// check whether to show log from HMCL monitor
if (Monitor.wasCrashed() && Monitor.getResult() != null)
MonitorCrashReporter.show(Monitor.getResult());
});
} catch (Throwable e) {
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
Expand Down
15 changes: 13 additions & 2 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@
import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.launch.DefaultLauncher;
import org.jackhuang.hmcl.launch.ProcessListener;
import org.jackhuang.hmcl.monitor.MonitorLauncher;
import org.jackhuang.hmcl.util.NativePatcher;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.ProcessInfo;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.*;
import java.util.*;
import java.util.stream.Stream;
Expand Down Expand Up @@ -154,11 +156,20 @@ private static String normalizedLanguageTag(Locale locale, GameVersionNumber gam
}

@Override
public ManagedProcess launch() throws IOException, InterruptedException {
public ProcessInfo launch() throws IOException, InterruptedException {
generateOptionsTxt();
return super.launch();
}

@Override
protected ProcessInfo startProcess(List<String> commandLine, Map<String, String> envVars, Charset encoding) throws IOException {
if (!MonitorLauncher.MONITOR_ENABLED || !(instance instanceof HMCLGameInstance hmclGameInstance))
return super.startProcess(commandLine, envVars, encoding);

/// using monitor to launch game if enabled
return MonitorLauncher.launchNewMonitor(commandLine, envVars, encoding, listener, hmclGameInstance, options);
}

@Override
public void makeLaunchScript(Path scriptFile) throws IOException {
generateOptionsTxt();
Expand Down
25 changes: 16 additions & 9 deletions HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
import org.jackhuang.hmcl.monitor.MonitorGameProcess;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.game.*;
Expand Down Expand Up @@ -836,7 +837,7 @@ private final class HMCLProcessListener implements ProcessListener {

private final ReentrantLock lock = new ReentrantLock();
private final LaunchOptions launchOptions;
private ManagedProcess process;
private ProcessInfo process;
private volatile boolean lwjgl;
private LogWindow logWindow;
private final boolean detectWindow;
Expand All @@ -855,14 +856,14 @@ public HMCLProcessListener(AuthInfo authInfo, LaunchOptions launchOptions, Count
}

@Override
public void setProcess(ManagedProcess process) {
public void setProcess(ProcessInfo process) {
this.process = process;

String command = new CommandBuilder().addAll(process.getCommands()).toString();
String command = new CommandBuilder().addAll(process.commands()).toString();

LOG.info("Launched process: " + command);

String classpath = process.getClasspath();
String classpath = process.classpath();
if (classpath != null) {
LOG.info("Process ClassPath: " + classpath);
}
Expand Down Expand Up @@ -970,7 +971,7 @@ public void onLog(String log, boolean isErrorStream) {
if (forbiddenAccessToken != null)
log = log.replace(forbiddenAccessToken, "<access token>");

Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
@Nullable Log4jLevel level = isErrorStream && !log.startsWith("[authlib-injector]") ? Log4jLevel.ERROR : null;
if (showLogs) {
if (level == null)
level = Objects.requireNonNullElse(Log4jLevel.guessLevel(log), Log4jLevel.INFO);
Expand All @@ -993,7 +994,13 @@ public void onLog(String log, boolean isErrorStream) {
try {
if (!lwjgl) {
lwjgl = true;
finishLaunch();
if (process instanceof MonitorGameProcess monitorProcess && !showLogs && launcherVisibility != LauncherVisibility.KEEP) {
monitorProcess.detach();
launchingLatch.countDown();
runLater(Launcher::stopApplication);
} else {
finishLaunch();
}
}
} finally {
lock.unlock();
Expand Down Expand Up @@ -1040,18 +1047,18 @@ public void onExit(int exitCode, ExitType exitType) {

}

private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
private static final Queue<WeakReference<ProcessInfo>> PROCESSES = new ConcurrentLinkedQueue<>();

public static int countMangedProcesses() {
PROCESSES.removeIf(it -> {
ManagedProcess process = it.get();
ProcessInfo process = it.get();
return process == null || !process.isRunning();
});
return PROCESSES.size();
}

public static void stopManagedProcesses() {
while (!PROCESSES.isEmpty())
Optional.ofNullable(PROCESSES.poll()).map(WeakReference::get).ifPresent(ManagedProcess::stop);
Optional.ofNullable(PROCESSES.poll()).map(WeakReference::get).ifPresent(ProcessInfo::stop);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 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.monitor;

import javafx.application.Platform;
import org.jackhuang.hmcl.game.GameInstanceID;
import org.jackhuang.hmcl.game.HMCLGameInstance;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Log;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.GameDirectoryID;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.GameCrashWindow;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.logging.Logger;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;

import java.util.List;
import java.util.Optional;

import static org.jackhuang.hmcl.util.logging.Logger.LOG;

/// @see show
@NotNullByDefault
public final class MonitorCrashReporter {

private MonitorCrashReporter() {
}

/// find [HMCLGameInstance] from [Monitor.Result] and show [GameCrashWindow]
public static void show(Monitor.Result result) {
findInstanceAsync(result).thenAcceptAsync(Schedulers.javafx(), instance -> {
if (instance == null) {
LOG.warning("Cannot resolve the launched instance " + result.launchOptions().getInstanceId() + ", skip showing the crash window");
return;
}
instance.markLaunchedAbnormally();
@Unmodifiable List<String> logs = result.process().getLines(null);
Platform.runLater(() -> new GameCrashWindow(
result.process(),
result.exitType(),
instance,
result.launchOptions(),
logs.subList(Math.max(0, logs.size() - Log.getLogLines()), logs.size()).stream()
.map(line -> new Log(Logger.filterForbiddenToken(line), Log4jLevel.guessLevel(line)))
.toList()).show());
}).start();
}

/// find [HMCLGameInstance] from [Monitor.Result]
private static Task<@Nullable HMCLGameInstance> findInstanceAsync(Monitor.Result result) {
// find repository first
GameDirectoryID id = GameDirectoryID.parse(result.gameDirectoryId());
Optional<GameDirectory> match = GameDirectoryManager.getGameDirectories().stream()
.filter(directory -> directory.getId().equals(id))
.findFirst();
if (match.isEmpty())
return Task.completed(null);
GameDirectoryManager.setSelectedGameDirectory(match.get());
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();

GameInstanceID instanceId = result.launchOptions().getInstanceId();
HMCLGameInstance instance = repository.findInstance(instanceId);

if (instance != null) // instance found
return Task.completed(instance);

LOG.info("Waiting for the game repository to load before showing the crash window");
return repository.refreshAsync().thenSupplyAsync(() -> repository.findInstance(instanceId));
}
}
Loading
Loading