Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c7f521f
ci: 添加自动构建和手动发布 GitHub Actions 工作流
creeperCN Apr 4, 2026
90056f5
feat: 添加客户端自身自动更新功能
creeperCN Apr 4, 2026
fb2afb5
feat: 实现客户端自动更新功能
creeperCN Apr 4, 2026
90e2bc8
fix: 修复 Java 8 兼容性问题
creeperCN Apr 4, 2026
311ff26
chore: 移除自动添加的 GitHub Actions 工作流
creeperCN Apr 4, 2026
db9143d
chore: 移除 docs 文件夹
creeperCN Apr 4, 2026
aa733ac
chore: 移除手动测试代码
creeperCN Apr 4, 2026
d34c2b6
chore: 移除调试代码
creeperCN Apr 4, 2026
ea7d380
fix: 恢复客户端更新检查日志
creeperCN Apr 4, 2026
713254b
fix: 恢复客户端更新检查调试日志
creeperCN Apr 4, 2026
a738522
chore: 删除未使用的 testAllMirrors 方法
creeperCN Apr 4, 2026
49edee0
chore: 删除废弃的兼容方法
creeperCN Apr 4, 2026
7cf6378
fix: 修复下载URL套娃问题
creeperCN Apr 4, 2026
50696af
fix: 调整下载镜像站顺序,hk.gh-proxy.org 优先
creeperCN Apr 4, 2026
2d5d787
feat: 实现多线程下载,提升下载速度
creeperCN Apr 4, 2026
3491532
perf: 下载线程数从4提升到8
creeperCN Apr 4, 2026
692432f
perf: 根据文件大小调整线程数为6(7.4MB每片1.23MB)
creeperCN Apr 4, 2026
3664a03
chore: 删除无效的测试文件
creeperCN Apr 4, 2026
5b5d13a
feat: 添加 ghfile.geekertao.top 为 API 和下载的首选镜像
creeperCN Apr 4, 2026
c719cc6
security: 修复3个安全漏洞
creeperCN Apr 4, 2026
f09edd0
security: 修复全项目安全漏洞
creeperCN Apr 4, 2026
29df511
security: 修复剩余安全漏洞
creeperCN Apr 4, 2026
c72f44d
security: Log线程安全 + HttpProtocol Response泄露修复
creeperCN Apr 4, 2026
28fb509
fix: 修复 SHA-256 校验失败(去掉 GitHub API digest 的 sha256: 前缀)
creeperCN Apr 4, 2026
d7ce18d
chore: 删除自动 release workflow,改为手动发布流程
creeperCN Apr 5, 2026
ef407ce
refactor: 移除 GitHub 依赖,改为从服主服务器获取更新
creeperCN Apr 5, 2026
f5aa5d9
feat: HashUtility 对象池 + MultiThreadDownloader 改用 OkHttp
creeperCN Apr 5, 2026
89e8075
feat: 批量下载优化 + 完成所有甲方要求
creeperCN Apr 5, 2026
26447e9
fix: 恢复 tag push 触发的构建 workflow
creeperCN Apr 5, 2026
0f4a12d
fix: 修复自更新绕过文件锁定问题
creeperCN Apr 5, 2026
d47d9c6
fix: 缓存更新目录路径,避免路径不一致导致只下载不替换
creeperCN Apr 5, 2026
d4faff2
fix: 添加 serverUrl 配置字段,修复配置不匹配问题
creeperCN Apr 5, 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
33 changes: 21 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: release publish
name: Build on Tag

on:
push:
Expand All @@ -8,21 +8,30 @@ on:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4

- name: Setup JDK
uses: actions/setup-java@v3
- name: Setup JDK 8
uses: actions/setup-java@v4
with:
java-version: 8
distribution: zulu
java-version: '8'
distribution: 'zulu'

- name: Build
uses: gradle/gradle-build-action@v2.4.0
with:
arguments: shadowJar
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3

- name: Build with Gradle
run: ./gradlew shadowJar --no-daemon

- name: Get version from tag
id: version
run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

- name: Release
- name: Upload to Release
uses: softprops/action-gh-release@v2
with:
files: build/libs/*
files: build/libs/*.jar
110 changes: 110 additions & 0 deletions SELF_UPDATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Mcpatch 客户端自我更新文档

## 概述

Mcpatch 客户端内置自我更新功能,允许客户端从服主 McPatch2 管理端自动获取新版本。更新采用延迟安装策略,确保在不同操作系统上都能安全可靠地完成。

## 工作原理

### 延迟安装策略

自我更新采用延迟安装策略,分两次启动完成:

第一次启动:检测更新 -> 下载新版本 -> 创建标记文件 -> 继续运行(旧版本)
第二次启动:检查标记 -> 替换 JAR -> 删除标记 -> 正常运行(新版本)

### 流程图

第一次启动:
main()/premain()
-> installPendingUpdate() -> 无标记
-> AppMain()
-> performSelfUpdate()
-> 检测更新
-> 下载新版本
-> 创建标记文件

第二次启动:
main()/premain()
-> installPendingUpdate()
-> 检测到标记文件
-> 重命名替换 JAR
-> 删除标记文件
-> AppMain() -> 使用新版本

## 文件锁定问题

### 问题背景

程序运行时,操作系统会锁定 JAR 文件:
- Windows:严格锁定,不允许修改
- Linux/macOS:较宽松,但仍有限制

### 解决方案:重命名交换

使用重命名操作绕过 Windows 文件锁定:

Mcpatch.jar(旧版本,被锁定)
Mcpatch.jar.new(新版本,未锁定)

步骤:
1. Mcpatch.jar -> Mcpatch.jar.old(重命名,Windows 允许)
2. Mcpatch.jar.new -> Mcpatch.jar(重命名)
3. 删除 Mcpatch.jar.old(清理)

## 配置说明

### mcpatch.yml 配置

client-update:
enabled: true
server-url: http://your-server:6700

## API 接口

### 版本信息格式

服主服务器需要提供 client-version.json:

{
version: 1.0.0,
download_url: http://server/Mcpatch-1.0.0.jar,
checksum: abc123,
changelog: 更新说明,
force_update: false
}

## 文件说明

| 文件 | 说明 |
|------|------|
| Mcpatch.jar | 当前版本 |
| Mcpatch.jar.new | 新版本(待安装)|
| .update-pending | 更新标记文件 |
| Mcpatch.jar.old | 旧版本备份(临时)|

## 故障排除

### 问题:只下载不替换

可能原因:
1. 标记文件未正确创建
2. installPendingUpdate() 未被调用
3. 重命名操作失败

排查步骤:
1. 检查是否存在 .update-pending 文件
2. 检查是否存在 .jar.new 文件
3. 查看日志中的错误信息

### 问题:Windows 上替换失败

可能原因:
1. 文件权限不足
2. 杀毒软件拦截
3. 文件被其他进程占用

解决方案:
1. 以管理员权限运行
2. 添加杀毒软件白名单
3. 关闭占用该文件的程序
68 changes: 52 additions & 16 deletions src/main/java/com/github/balloonupdate/mcpatch/client/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
import com.github.balloonupdate.mcpatch.client.logging.FileHandler;
import com.github.balloonupdate.mcpatch.client.logging.Log;
import com.github.balloonupdate.mcpatch.client.logging.LogLevel;
import com.github.balloonupdate.mcpatch.client.selfupdate.SelfUpdateManager;
import com.github.balloonupdate.mcpatch.client.selfupdate.SelfUpdateInstaller;
import com.github.balloonupdate.mcpatch.client.ui.McPatchWindow;
import com.github.balloonupdate.mcpatch.client.utils.BytesUtils;
import com.github.balloonupdate.mcpatch.client.utils.DialogUtility;
import com.github.balloonupdate.mcpatch.client.utils.Env;
import com.github.kasuminova.GUI.SetupSwing;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.constructor.Constructor;
import org.yaml.snakeyaml.parser.ParserException;

import java.awt.*;
Expand Down Expand Up @@ -47,6 +51,9 @@ public enum StartMethod {
}

public static void main(String[] args) throws Throwable {
// 最开始就执行待处理的更新替换(此时 JAR 锁定最弱)
SelfUpdateInstaller.installPendingUpdate();

boolean graphicsMode = Desktop.isDesktopSupported();

if (args.length > 0 && args[0].equals("windowless"))
Expand All @@ -56,6 +63,9 @@ public static void main(String[] args) throws Throwable {
}

public static void premain(String agentArgs, Instrumentation ins) throws Throwable {
// 最开始就执行待处理的更新替换(此时 JAR 锁定最弱)
SelfUpdateInstaller.installPendingUpdate();

boolean graphicsMode = Desktop.isDesktopSupported();

if (agentArgs != null && agentArgs.equals("windowless"))
Expand Down Expand Up @@ -126,13 +136,36 @@ static boolean AppMain(boolean graphicsMode, StartMethod startMethod, boolean en
window.show();
}

// // 点击窗口的叉时停止更新任务
// if (window != null) {
// window.onWindowClosing = w -> {
// if (workThread.isAlive())
// workThread.interrupt();
// };
// }
// ========== 客户端自身更新检查 ==========
// 调试信息
Log.debug("clientUpdate 配置: " + (config.clientUpdate != null ? "已加载" : "null"));
if (config.clientUpdate != null) {
Log.debug(" enabled: " + config.clientUpdate.enabled);
Log.debug(" githubRepo: " + config.clientUpdate.githubRepo);
}

if (config.clientUpdate != null && config.clientUpdate.enabled) {
try {
Log.info("正在检查客户端自身更新...");

// 设置系统属性传递配置
if (config.clientUpdate.serverUrl != null && !config.clientUpdate.serverUrl.isEmpty()) {
System.setProperty("mcpatch.selfupdate.server-url", config.clientUpdate.serverUrl);
}
if (config.clientUpdate.channel != null) {
System.setProperty("mcpatch.selfupdate.channel", config.clientUpdate.channel);
}

// 执行自更新检查(显示进度窗口)
SelfUpdateManager.performSelfUpdate(graphicsMode);

} catch (Exception e) {
Log.error("客户端自更新检查失败: " + e.getMessage());
// 自更新失败不影响正常更新流程
}
} else {
Log.debug("客户端自更新未启用,跳过检查");
}

Work work = new Work();
work.window = window;
Expand Down Expand Up @@ -295,20 +328,23 @@ static Path getUpdateDirectory(Path workDir, AppConfig config) throws McpatchBus
* 向上搜索,直到有一个父目录包含 .minecraft 目录
*/
static Path searchDotMinecraft(Path basedir) {
try {
File d = basedir.toFile();
File d = basedir.toFile();

for (int i = 0; i < 7; i++) {
if (d == null || !d.exists()) {
return null;
}

for (int i = 0; i < 7; i++) {
for (File f : d.listFiles()) {
File[] files = d.listFiles();
if (files != null) {
for (File f : files) {
if (f.getName().equals(".minecraft")) {
return d.toPath();
}
}

d = d.getParentFile();
}
} catch (NullPointerException e) {
return null;

d = d.getParentFile();
}

return null;
Expand All @@ -321,7 +357,7 @@ static Map<String, Object> readConfig(Path external) throws McpatchBusinessExcep

Map<String, Object> result;

Yaml yaml = new Yaml();
Yaml yaml = new Yaml(new Constructor(new LoaderOptions()));

// 如果外部配置文件存在,优先使用
if (Files.exists(external)) {
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/com/github/balloonupdate/mcpatch/client/Work.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ boolean run2(Servers server) throws IOException, McpatchBusinessException {
if (change instanceof FileChange.CreateFolder) {
FileChange.CreateFolder op = (FileChange.CreateFolder) change;

PathUtility.validateServerPath(op.path, baseDir);

RuntimeAssert.isTrue(!createFolders.contains(op.path));

// 先删除 deleteFolders 里的文件夹。没有的话,再加入 createFolders 里面
Expand All @@ -228,6 +230,8 @@ boolean run2(Servers server) throws IOException, McpatchBusinessException {
if (change instanceof FileChange.UpdateFile) {
FileChange.UpdateFile op = (FileChange.UpdateFile) change;

PathUtility.validateServerPath(op.path, baseDir);

// 删除已有的东西,避免下面重复添加报错
updateFiles.removeIf(e -> e.path.equals(op.path));

Expand All @@ -242,6 +246,8 @@ boolean run2(Servers server) throws IOException, McpatchBusinessException {
if (change instanceof FileChange.DeleteFolder) {
FileChange.DeleteFolder op = (FileChange.DeleteFolder) change;

PathUtility.validateServerPath(op.path, baseDir);

// 先删除 createFolders 里的文件夹。没有的话,再加入 deleteFolders 里面
if (createFolders.contains(op.path)) {
createFolders.remove(op.path);
Expand All @@ -253,6 +259,8 @@ boolean run2(Servers server) throws IOException, McpatchBusinessException {
if (change instanceof FileChange.DeleteFile) {
FileChange.DeleteFile op = (FileChange.DeleteFile) change;

PathUtility.validateServerPath(op.path, baseDir);

// 处理那些刚下载又马上要被删的文件,这些文件不用重复下载
if (updateFiles.stream().anyMatch(e -> e.path.equals(op.path))) {
updateFiles.removeIf(e -> e.path.equals(op.path));
Expand All @@ -264,6 +272,10 @@ boolean run2(Servers server) throws IOException, McpatchBusinessException {
if (change instanceof FileChange.MoveFile) {
FileChange.MoveFile op = (FileChange.MoveFile) change;

// 验证路径安全性
PathUtility.validateServerPath(op.from, baseDir);
PathUtility.validateServerPath(op.to, baseDir);

// 单独处理还没有下载的文件
Optional<TempUpdateFile> find = updateFiles.stream()
.filter(e -> e.path.equals(op.from))
Expand Down
Loading