Feature: Verify intergrity before lauching terracotta.

This commit is contained in:
burningtnt 2025-08-10 12:22:31 +08:00
parent d48cfc378e
commit aead89da3a
No known key found for this signature in database
GPG Key ID: 18A43F21F9ACE8C4
9 changed files with 137 additions and 72 deletions

View File

@ -91,7 +91,11 @@ public class TerracottaControllerPage extends StackPane {
holder.add(FXUtils.onWeakChangeAndOperate(UI_STATE, state -> {
progressProperty.unbind();
if (state instanceof TerracottaState.Uninitialized) {
if (state instanceof TerracottaState.Bootstrap) {
statusProperty.set(i18n("terracotta.status.bootstrap"));
progressProperty.set(-1);
nodesProperty.setAll();
} else if (state instanceof TerracottaState.Uninitialized) {
statusProperty.set(i18n("terracotta.status.uninitialized"));
progressProperty.set(0);

View File

@ -0,0 +1,47 @@
package org.jackhuang.hmcl.ui.terracotta.core;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.DigestUtils;
import org.jackhuang.hmcl.util.Hex;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public final class TerracottaDaemon {
private final List<URI> links;
private final FileDownloadTask.IntegrityCheck checking;
private final Path path;
public TerracottaDaemon(List<URI> links, String classifier, FileDownloadTask.IntegrityCheck checking) {
this.links = links;
this.checking = checking;
this.path = Metadata.DEPENDENCIES_DIRECTORY.resolve(
String.format("terracota/%s/terracotta-%s", TerracottaMetadata.VERSION, classifier)
).toAbsolutePath();
}
public Path getPath() {
return path;
}
public FileDownloadTask create() {
return new FileDownloadTask(links, path, checking);
}
public boolean exists() throws IOException {
if (!Files.exists(path)) {
return false;
}
String checksum;
try (InputStream is = Files.newInputStream(path)) {
checksum = Hex.encodeHex(DigestUtils.digest(checking.getAlgorithm(), is));
}
return checksum.equalsIgnoreCase(checking.getChecksum());
}
}

View File

@ -30,24 +30,29 @@ public final class TerracottaManager {
private TerracottaManager() {
}
private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>();
static {
if (TerracottaMetadata.PROVIDER == null) {
STATE_V.setPlain(TerracottaState.Fatal.INSTANCE);
} else if (TerracottaMetadata.PROVIDER.exist()) {
TerracottaState.Launching launching = new TerracottaState.Launching();
STATE_V.setPlain(launching);
launch(launching);
} else {
STATE_V.setPlain(TerracottaState.Uninitialized.INSTANCE);
}
}
private static final AtomicReference<TerracottaState> STATE_V = new AtomicReference<>(TerracottaState.Bootstrap.INSTANCE);
private static final ReadOnlyObjectWrapper<TerracottaState> STATE = new ReadOnlyObjectWrapper<>(STATE_V.getPlain());
private static final InvocationDispatcher<TerracottaState> STATE_D = InvocationDispatcher.runOn(Platform::runLater, STATE::set);
static {
Task.runAsync(() -> {
if (TerracottaMetadata.PROVIDER == null) {
setState(TerracottaState.Fatal.INSTANCE);
LOG.warning("Terracotta hasn't support your OS: " + org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM);
} else if (TerracottaMetadata.PROVIDER.exists()) {
TerracottaState.Launching launching = new TerracottaState.Launching();
setState(launching);
launch(launching);
} else {
setState(TerracottaState.Uninitialized.INSTANCE);
}
}).whenComplete(exception -> {
if (exception != null) {
compareAndSet(TerracottaState.Bootstrap.INSTANCE, TerracottaState.Fatal.INSTANCE);
}
}).start();
}
public static ReadOnlyObjectProperty<TerracottaState> stateProperty() {
return STATE.getReadOnlyProperty();
}

View File

@ -1,5 +1,6 @@
package org.jackhuang.hmcl.ui.terracotta.core;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.ui.terracotta.core.provider.GeneralProvider;
import org.jackhuang.hmcl.ui.terracotta.core.provider.ITerracottaProvider;
import org.jackhuang.hmcl.ui.terracotta.core.provider.MacOSProvider;
@ -11,21 +12,41 @@ public final class TerracottaMetadata {
private TerracottaMetadata() {
}
public static final TerracottaDaemon WINDOWS_X86_64 = create(
"windows-x86_64.exe", "b1badefb1e503d4e9b886edab1bf3fb6b1ff75763b29a06fe7cc2f2343610d02"
);
public static final TerracottaDaemon WINDOWS_ARM64 = create(
"windows-arm64.exe", "05f376bcf3a8317a36fd51b6335ad8e6821af03af78a90cc1b0ff91771e095f3"
);
public static final TerracottaDaemon LINUX_X86_64 = create(
"linux-x86_64", "ca197ab3780834a58e51d17fa57157f82486bc6b22bf57242eca169c6e408ede"
);
public static final TerracottaDaemon LINUX_ARM64 = create(
"linux-arm64", "85949ef696668f0a6c08944c998342bc1bbad62f112d6c2663acc2a0cc3e1b3c"
);
public static final TerracottaDaemon MACOS_INSTALLER_X86_64 = create(
"macos-x86_64.pkg", "e46c71f0c446f9ba0bd67f7216b64bad811417a00e54d4841eb1c71e7f70f189"
);
public static final TerracottaDaemon MACOS_INSTALLER_ARM64 = create(
"macos-arm64.pkg", "223ab9964c05867bd76fd66b0bc9dde18f3c2958356c9c15be8205dcb7bdee00"
);
public static final TerracottaDaemon MACOS_BIN_X86_64 = create(
"macos-x86_64", "49b4813538e1c6c495d69760a289bd8d4bd3a7ef51cc4a7db7a6a33f45846440");
public static final TerracottaDaemon MACOS_BIN_ARM64 = create(
"macos-arm64", "9e4da85595301fec392a4efa7aff44f05c3c81666d99a0c9df5d1c368617dfff"
);
public static final String VERSION = "0.3.8-rc.1";
public static final List<URI> WINDOWS_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-windows-x86_64.exe", VERSION));
public static final List<URI> WINDOWS_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-windows-arm64.exe", VERSION));
private static TerracottaDaemon create(String classifier, String hash) {
String link = String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-%2$s", VERSION, classifier);
public static final List<URI> LINUX_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-linux-x86_64", VERSION));
public static final List<URI> LINUX_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-linux-arm64", VERSION));
public static final List<URI> MACOS_INSTALLER_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-x86_64.pkg", VERSION));
public static final List<URI> MACOS_INSTALLER_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-arm64.pkg", VERSION));
public static final List<URI> MACOS_BIN_X86_64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-x86_64", VERSION));
public static final List<URI> MACOS_BIN_ARM64 = create(String.format("https://github.com/burningtnt/Terracotta/releases/download/V%1$s/terracotta-%1$s-macos-arm64", VERSION));
private static List<URI> create(String s) {
return List.of(URI.create("https://ghfast.top/" + s), URI.create("https://cdn.crashmc.com/" + s), URI.create(s));
return new TerracottaDaemon(
List.of(URI.create("https://ghfast.top/" + link), URI.create("https://cdn.crashmc.com/" + link), URI.create(link)),
classifier, new FileDownloadTask.IntegrityCheck("SHA-256", hash)
);
}
public static final ITerracottaProvider PROVIDER = locateProvider();
@ -39,9 +60,4 @@ public final class TerracottaMetadata {
return null;
}
}
public static String getFileName(URI uri) {
String p = uri.getPath();
return p.substring(p.lastIndexOf('/') + 1);
}
}

View File

@ -6,6 +6,13 @@ public abstract class TerracottaState {
protected TerracottaState() {
}
public static final class Bootstrap extends TerracottaState {
static final Bootstrap INSTANCE = new Bootstrap();
private Bootstrap() {
}
}
public static final class Uninitialized extends TerracottaState {
static final Uninitialized INSTANCE = new Uninitialized();

View File

@ -1,49 +1,40 @@
package org.jackhuang.hmcl.ui.terracotta.core.provider;
import javafx.beans.property.DoubleProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.terracotta.core.TerracottaDaemon;
import org.jackhuang.hmcl.ui.terracotta.core.TerracottaMetadata;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import java.net.URI;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class GeneralProvider implements ITerracottaProvider {
public static final List<URI> TARGET = Map.of(
public static final TerracottaDaemon TARGET = Map.of(
Platform.WINDOWS_X86_64, TerracottaMetadata.WINDOWS_X86_64,
Platform.WINDOWS_ARM64, TerracottaMetadata.WINDOWS_ARM64,
Platform.LINUX_X86_64, TerracottaMetadata.LINUX_X86_64,
Platform.LINUX_ARM64, TerracottaMetadata.LINUX_ARM64
).get(Platform.SYSTEM_PLATFORM);
private static final Path PATH = TARGET != null ? Metadata.DEPENDENCIES_DIRECTORY.resolve(String.format(
"terracota/%s/%s", TerracottaMetadata.VERSION, TerracottaMetadata.getFileName(TARGET.get(0))
)).toAbsolutePath() : null;
@Override
public boolean exist() {
return Files.exists(PATH);
public boolean exists() throws IOException {
return TARGET.exists();
}
@Override
public Task<?> install(DoubleProperty progress) {
Path tmp = PATH.resolveSibling(PATH.getFileName() + ".tmp");
Task<?> task = new FileDownloadTask(TARGET, tmp);
Task<?> task = TARGET.create();
progress.bind(task.progressProperty());
task = task.thenRunAsync(() -> Files.move(tmp, PATH, StandardCopyOption.REPLACE_EXISTING));
if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(PATH, Set.of(
task = task.thenRunAsync(() -> Files.setPosixFilePermissions(TARGET.getPath(), Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
@ -58,6 +49,6 @@ public final class GeneralProvider implements ITerracottaProvider {
@Override
public List<String> launch(Path path) {
return List.of(PATH.toString(), "--hmcl", path.toString());
return List.of(TARGET.getPath().toString(), "--hmcl", path.toString());
}
}

View File

@ -8,7 +8,7 @@ import java.nio.file.Path;
import java.util.List;
public interface ITerracottaProvider {
boolean exist();
boolean exists() throws IOException;
Task<?> install(DoubleProperty progress) throws IOException;

View File

@ -1,19 +1,16 @@
package org.jackhuang.hmcl.ui.terracotta.core.provider;
import javafx.beans.property.DoubleProperty;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.terracotta.core.TerracottaDaemon;
import org.jackhuang.hmcl.ui.terracotta.core.TerracottaMetadata;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.PosixFilePermission;
import java.util.List;
import java.util.Set;
@ -21,7 +18,7 @@ import java.util.Set;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class MacOSProvider implements ITerracottaProvider {
public static final List<URI> INSTALLER, BINARY;
public static final TerracottaDaemon INSTALLER, BINARY;
static {
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
@ -36,24 +33,20 @@ public final class MacOSProvider implements ITerracottaProvider {
}
}
private static final Path PATH = BINARY != null ? Metadata.DEPENDENCIES_DIRECTORY.resolve(String.format(
"terracota/%s/%s", TerracottaMetadata.VERSION, TerracottaMetadata.getFileName(BINARY.get(0))
)).toAbsolutePath() : null;
@Override
public boolean exist() {
return Files.exists(Path.of("/Applications/terracotta.app")) && Files.exists(PATH);
public boolean exists() throws IOException {
assert BINARY != null;
return Files.exists(Path.of("/Applications/terracotta.app")) && BINARY.exists();
}
@Override
public Task<?> install(DoubleProperty progress) throws IOException {
Path installer = Files.createTempFile("hmcl-terracotta-installer-", ".pkg").toAbsolutePath();
Task<?> installerTask = new FileDownloadTask(INSTALLER, installer);
assert INSTALLER != null && BINARY != null;
Path binary = PATH.resolveSibling(PATH.getFileName() + ".tmp");
Task<?> binaryTask = new FileDownloadTask(BINARY, binary);
progress.bind(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.3));
Task<?> installerTask = INSTALLER.create();
Task<?> binaryTask = BINARY.create();
progress.bind(installerTask.progressProperty().add(binaryTask.progressProperty()).multiply(0.4)); // (1 + 1) * 0.4 = 0.8
installerTask = installerTask.thenComposeAsync(() -> {
ManagedProcess process = new ManagedProcess(new ProcessBuilder(
@ -61,7 +54,7 @@ public final class MacOSProvider implements ITerracottaProvider {
"-e",
String.format(
"do shell script \"%s\" with prompt \"%s\" with administrator privileges",
String.format("installer -pkg %s -target /Applications", installer),
String.format("installer -pkg %s -target /Applications", INSTALLER.getPath()),
i18n("terracotta.sudo_installing")
)
));
@ -71,8 +64,7 @@ public final class MacOSProvider implements ITerracottaProvider {
return Task.fromCompletableFuture(process.getProcess().onExit());
});
binaryTask = binaryTask.thenRunAsync(() -> {
Files.move(binary, PATH, StandardCopyOption.REPLACE_EXISTING);
Files.setPosixFilePermissions(PATH, Set.of(
Files.setPosixFilePermissions(BINARY.getPath(), Set.of(
PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE,
@ -88,6 +80,8 @@ public final class MacOSProvider implements ITerracottaProvider {
@Override
public List<String> launch(Path path) {
return List.of(PATH.toString(), "--hmcl", path.toString());
assert BINARY != null;
return List.of(BINARY.getPath().toString(), "--hmcl", path.toString());
}
}

View File

@ -1197,6 +1197,7 @@ terracotta.status=联机大厅
terracotta.back=退出
terracotta.network_warning=多人联机基于 p2p最终联机体验和您的网络情况有较大关系。
terracotta.sudo_installing=HMCL 需要验证您的密码才能安装联机核心
terracotta.status.bootstrap=正在收集信息
terracotta.status.uninitialized=未下载联机核心
terracotta.status.uninitialized.title=下载联机核心(约 8MB
terracotta.status.uninitialized.desc=您承诺,在多人联机全过程中,您将严格遵守您所在国家或地区的全部法律法规