Compare commits

...
4 Commits
15 changed files with 94 additions and 174 deletions

No files matched your search

-1
View File
@@ -147,7 +147,6 @@ tasks.shadowJar {
"Microsoft-Auth-Secret" to microsoftAuthSecret,
"CurseForge-Api-Key" to curseForgeApiKey,
"Build-Channel" to versionType,
"Class-Path" to "pack200.jar",
"Add-Opens" to listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
@@ -181,6 +181,9 @@ public final class Config implements Observable {
@SerializedName("promptedVersion")
private StringProperty promptedVersion = new SimpleStringProperty();
@SerializedName("acceptPreviewUpdate")
private BooleanProperty acceptPreviewUpdate = new SimpleBooleanProperty(false);
@SerializedName("_version")
private IntegerProperty configVersion = new SimpleIntegerProperty(0);
@@ -671,6 +674,18 @@ public final class Config implements Observable {
this.promptedVersion.set(promptedVersion);
}
public boolean isAcceptPreviewUpdate() {
return acceptPreviewUpdate.get();
}
public BooleanProperty acceptPreviewUpdateProperty() {
return acceptPreviewUpdate;
}
public void setAcceptPreviewUpdate(boolean acceptPreviewUpdate) {
this.acceptPreviewUpdate.set(acceptPreviewUpdate);
}
public ObservableMap<String, Object> getShownTips() {
return shownTips;
}
@@ -48,11 +48,22 @@ public final class UpgradeDialog extends JFXDialogLayout {
setBody(new ProgressIndicator());
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
boolean isPreview = remoteVersion.isPreview();
Task.supplyAsync(Schedulers.io(), () -> {
Document document = Jsoup.parse(new URL(url), 30 * 1000);
Node node = document.selectFirst("#nowchange");
String id = null;
Node node = null;
if (isPreview) {
id = "nowpreview";
node = document.selectFirst("#" + id);
}
if (node == null) {
id = "nowchange";
node = document.selectFirst("#" + id);
}
if (node == null || !"h1".equals(node.nodeName()))
throw new IOException("Cannot find #nowchange in document");
throw new IOException("Cannot find current changelog in document");
HTMLRenderer renderer = new HTMLRenderer(uri -> {
LOG.info("Open link: " + uri);
@@ -60,7 +71,7 @@ public final class UpgradeDialog extends JFXDialogLayout {
});
do {
if ("h1".equals(node.nodeName()) && !"nowchange".equals(node.attr("id"))) {
if ("h1".equals(node.nodeName()) && !id.equals(node.attr("id"))) {
break;
}
renderer.appendNode(node);
@@ -106,9 +106,12 @@ public final class SettingsPage extends SettingsView {
chkUpdateStable.setUserData(UpdateChannel.STABLE);
ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class);
updateChannel.set(UpdateChannel.getChannel());
updateChannel.addListener((a, b, newValue) -> {
UpdateChecker.requestCheckUpdate(newValue);
});
InvalidationListener checkUpdateListener = e -> {
UpdateChecker.requestCheckUpdate(updateChannel.get(), previewPane.isSelected());
};
updateChannel.addListener(checkUpdateListener);
previewPane.selectedProperty().addListener(checkUpdateListener);
// ====
}
@@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.ComponentSublist;
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
@@ -56,6 +57,7 @@ public abstract class SettingsView extends StackPane {
protected final JFXRadioButton chkUpdateStable;
protected final JFXRadioButton chkUpdateDev;
protected final JFXButton btnUpdate;
protected final OptionToggleButton previewPane;
protected final ScrollPane scroll;
public SettingsView() {
@@ -145,6 +147,15 @@ public abstract class SettingsView extends StackPane {
settingsPane.getContent().add(updatePane);
}
{
previewPane = new OptionToggleButton();
previewPane.setTitle(i18n("update.preview"));
previewPane.selectedProperty().bindBidirectional(config().acceptPreviewUpdateProperty());
FXUtils.installFastTooltip(previewPane, i18n("update.preview.tooltip"));
settingsPane.getContent().add(previewPane);
}
{
fileCommonLocation = new MultiFileItem<>();
fileCommonLocationSublist = new ComponentSublist();
@@ -18,15 +18,9 @@
package org.jackhuang.hmcl.upgrade;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.tukaani.xz.XZInputStream;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarOutputStream;
class HMCLDownloadTask extends FileDownloadTask {
@@ -47,15 +41,6 @@ class HMCLDownloadTask extends FileDownloadTask {
switch (archiveFormat) {
case JAR:
break;
case PACK_XZ:
byte[] raw = Files.readAllBytes(target);
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
Pack200Utils.unpack(in, out);
}
break;
default:
throw new IllegalArgumentException("Unknown format: " + archiveFormat);
}
@@ -21,28 +21,23 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck;
import org.jackhuang.hmcl.util.Pack200Utils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import java.io.IOException;
import java.util.Optional;
public class RemoteVersion {
public final class RemoteVersion {
public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException {
public static RemoteVersion fetch(UpdateChannel channel, boolean preview, String url) throws IOException {
try {
JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class);
String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing"));
String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null);
String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null);
String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null);
String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null);
boolean force = Optional.ofNullable(response.get("force")).map(JsonElement::getAsBoolean).orElse(false);
if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) {
return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash), force);
} else if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), force);
if (jarUrl != null && jarHash != null) {
return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash), preview, force);
} else {
throw new IOException("No download url is available");
}
@@ -56,14 +51,16 @@ public class RemoteVersion {
private final String url;
private final Type type;
private final IntegrityCheck integrityCheck;
private final boolean preview;
private final boolean force;
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean force) {
public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck, boolean preview, boolean force) {
this.channel = channel;
this.version = version;
this.url = url;
this.type = type;
this.integrityCheck = integrityCheck;
this.preview = preview;
this.force = force;
}
@@ -87,6 +84,10 @@ public class RemoteVersion {
return integrityCheck;
}
public boolean isPreview() {
return preview;
}
public boolean isForce() {
return force;
}
@@ -97,7 +98,6 @@ public class RemoteVersion {
}
public enum Type {
PACK_XZ,
JAR
}
}
@@ -27,14 +27,15 @@ import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.IOException;
import java.util.LinkedHashMap;
import static org.jackhuang.hmcl.util.Lang.mapOf;
import static org.jackhuang.hmcl.util.Lang.thread;
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.*;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair;
public final class UpdateChecker {
private UpdateChecker() {}
private UpdateChecker() {
}
private static final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>();
private static final BooleanBinding outdated = Bindings.createBooleanBinding(
@@ -55,7 +56,7 @@ public final class UpdateChecker {
private static final ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false);
public static void init() {
requestCheckUpdate(UpdateChannel.getChannel());
requestCheckUpdate(UpdateChannel.getChannel(), config().isAcceptPreviewUpdate());
}
public static RemoteVersion getLatestVersion() {
@@ -82,16 +83,17 @@ public final class UpdateChecker {
return checkingUpdate.getReadOnlyProperty();
}
private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException {
private static RemoteVersion checkUpdate(UpdateChannel channel, boolean preview) throws IOException {
if (!IntegrityChecker.DISABLE_SELF_INTEGRITY_CHECK && !IntegrityChecker.isSelfVerified()) {
throw new IOException("Self verification failed");
}
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, mapOf(
pair("version", Metadata.VERSION),
pair("channel", channel.channelName)));
LinkedHashMap<String, String> query = new LinkedHashMap<>();
query.put("version", Metadata.VERSION);
query.put("channel", preview ? channel.channelName + "-preview" : channel.channelName);
return RemoteVersion.fetch(channel, url);
String url = NetworkUtils.withQuery(Metadata.HMCL_UPDATE_URL, query);
return RemoteVersion.fetch(channel, preview, url);
}
private static boolean isDevelopmentVersion(String version) {
@@ -99,7 +101,7 @@ public final class UpdateChecker {
version.contains("SNAPSHOT"); // eg. 3.5.SNAPSHOT
}
public static void requestCheckUpdate(UpdateChannel channel) {
public static void requestCheckUpdate(UpdateChannel channel, boolean preview) {
Platform.runLater(() -> {
if (isCheckingUpdate())
return;
@@ -108,8 +110,8 @@ public final class UpdateChecker {
thread(() -> {
RemoteVersion result = null;
try {
result = checkUpdate(channel);
LOG.info("Latest version (" + channel + ") is " + result);
result = checkUpdate(channel, preview);
LOG.info("Latest version (" + channel + ", preview=" + preview + ") is " + result);
} catch (IOException e) {
LOG.warning("Failed to check for update", e);
}
@@ -1423,6 +1423,8 @@ update.note=Beta and Nightly channels may have more features or fixes, but they
update.latest=This is the latest version
update.no_browser=Cannot open in system browser. But we copied the link to your clipboard, and you can open it manually.
update.tooltip=Update
update.preview=Preview HMCL releases early
update.preview.tooltip=Enable this option to receive new versions of HMCL early for testing before their official release.
version=Games
version.name=Instance Name
@@ -1419,6 +1419,8 @@ update.note=Advertencia: Las versiones beta y las versiones nocturnas pueden ten
update.latest=Esta es la última versión.
update.no_browser=No se puede abrir en el navegador del sistema. Pero, hemos copiado el enlace a su portapapeles y puede abrirlo manualmente.
update.tooltip=Actualización
update.preview=Vista previa de actualizaciones anticipadas
update.preview.tooltip=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas
version=Juegos
version.name=Nombre de instancia
@@ -1207,6 +1207,8 @@ update.note=開發版與預覽版包含更多的功能以及錯誤修復,但
update.latest=目前版本為最新版本
update.no_browser=無法開啟瀏覽器。網址已經複製到剪貼簿了,你可以手動複製網址開啟頁面。
update.tooltip=更新
update.preview=提前測試 HMCL 預覽版本
update.preview.tooltip=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。
version=遊戲
version.name=遊戲實例名稱
@@ -1217,6 +1217,8 @@ update.note=开发版与预览版包含更多的功能以及错误修复,但
update.latest=当前版本为最新版本
update.no_browser=无法打开浏览器。网址已经复制到剪贴板,你可以手动粘贴网址打开页面。
update.tooltip=更新
update.preview=提前预览 HMCL 版本
update.preview.tooltip=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。
version=游戏
version.name=游戏实例名称
@@ -231,7 +231,7 @@ public class CacheRepository {
String url = conn.getURL().toString();
String lastModified = conn.getHeaderField("Last-Modified");
CacheResult cacheResult = cacheSupplier.get();
ETagItem eTagItem = new ETagItem(url, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified);
ETagItem eTagItem = new ETagItem(url, eTag, cacheResult.hash, Files.getLastModifiedTime(cacheResult.cachedFile).toMillis(), lastModified, 0L);
Lock writeLock = lock.writeLock();
writeLock.lock();
try {
@@ -325,19 +325,24 @@ public class CacheRepository {
@SerializedName("remote")
private final String remoteLastModified;
// The `expires` field is not used in HMCL 3.6.x, but will be used in HMCL 3.7.x.
// We backported this field to 3.6.x to avoid breaking the etag.json file created by newer HMCL versions.
private final long expires;
/**
* For Gson.
*/
public ETagItem() {
this(null, null, null, 0, null);
this(null, null, null, 0, null, 0L);
}
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified) {
public ETagItem(String url, String eTag, String hash, long localLastModified, String remoteLastModified, long expires) {
this.url = url;
this.eTag = eTag;
this.hash = hash;
this.localLastModified = localLastModified;
this.remoteLastModified = remoteLastModified;
this.expires = expires;
}
public int compareTo(ETagItem other) {
@@ -361,12 +366,13 @@ public class CacheRepository {
Objects.equals(url, eTagItem.url) &&
Objects.equals(eTag, eTagItem.eTag) &&
Objects.equals(hash, eTagItem.hash) &&
Objects.equals(remoteLastModified, eTagItem.remoteLastModified);
Objects.equals(remoteLastModified, eTagItem.remoteLastModified) &&
expires == eTagItem.expires;
}
@Override
public int hashCode() {
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified);
return Objects.hash(url, eTag, hash, localLastModified, remoteLastModified, expires);
}
}
@@ -1,122 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <[email protected]> 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.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.jar.JarOutputStream;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class Pack200Utils {
private Pack200Utils() {
}
private static final String[] IMPL_NAMES = {
"java.util.jar.Pack200",
"org.glavo.pack200.Pack200",
"io.pack200.Pack200"
};
private static final MethodHandle newUnpackerHandle;
private static final MethodHandle unpackHandle;
private static final MethodHandle unpackFileHandle;
static {
Class<?> pack200Class = null;
Class<?> unpackerClass = null;
for (String implName : IMPL_NAMES) {
try {
pack200Class = Class.forName(implName);
unpackerClass = Class.forName(implName + "$Unpacker");
break;
} catch (ClassNotFoundException ignored) {
}
}
if (pack200Class == null) {
LOG.warning("Pack200 not found");
newUnpackerHandle = null;
unpackHandle = null;
unpackFileHandle = null;
} else {
final MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle newUnpacker = null;
MethodHandle unpack = null;
MethodHandle unpackFile = null;
try {
newUnpacker = lookup.findStatic(pack200Class, "newUnpacker", MethodType.methodType(unpackerClass));
unpack = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, InputStream.class, JarOutputStream.class));
unpackFile = lookup.findVirtual(unpackerClass, "unpack", MethodType.methodType(void.class, File.class, JarOutputStream.class));
} catch (Throwable e) {
LOG.warning("Failed to find pack200 methods", e);
}
if (newUnpacker != null) {
newUnpackerHandle = newUnpacker;
unpackHandle = unpack;
unpackFileHandle = unpackFile;
} else {
newUnpackerHandle = null;
unpackHandle = null;
unpackFileHandle = null;
}
}
}
public static boolean isSupported() {
return newUnpackerHandle != null;
}
public static void unpack(InputStream in, JarOutputStream out) throws IOException {
if (newUnpackerHandle == null) {
throw new UnsupportedOperationException("Pack200");
}
try {
unpackHandle.invoke(newUnpackerHandle.invoke(), in, out);
} catch (IOException | RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
public static void unpack(File in, JarOutputStream out) throws IOException {
if (newUnpackerHandle == null) {
throw new UnsupportedOperationException("Pack200");
}
try {
unpackFileHandle.invoke(newUnpackerHandle.invoke(), in, out);
} catch (IOException | RuntimeException | Error e) {
throw e;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
@@ -846,4 +846,6 @@
1.21.9-pre3
1.21.9-pre4
1.21.9-rc1
1.21.9
1.21.9
1.21.10-rc1
1.21.10