use separate tasks for forge installer processors

This commit is contained in:
Haowei Wen 2021-08-19 23:21:00 +08:00
parent 7d39d00a6a
commit 9bc10fb27d
5 changed files with 193 additions and 129 deletions

View File

@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 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
@ -29,7 +29,8 @@ import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.download.fabric.FabricInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask;
import org.jackhuang.hmcl.download.forge.ForgeOldInstallTask;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.download.game.GameInstallTask;
import org.jackhuang.hmcl.download.liteloader.LiteLoaderInstallTask;
@ -110,7 +111,7 @@ public final class TaskListPane extends StackPane {
task.setName(i18n("assets.download_all"));
} else if (task instanceof GameInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.game")));
} else if (task instanceof ForgeInstallTask) {
} else if (task instanceof ForgeNewInstallTask || task instanceof ForgeOldInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.forge")));
} else if (task instanceof LiteLoaderInstallTask) {
task.setName(i18n("install.installer.install", i18n("install.installer.liteloader")));

View File

@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 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
@ -60,6 +60,7 @@ public final class ForgeInstallTask extends Task<Version> {
this.dependencyManager = dependencyManager;
this.version = version;
this.remote = remoteVersion;
setSignificance(TaskSignificance.MODERATE);
}
@Override

View File

@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 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
@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.forge;
import org.jackhuang.hmcl.download.ArtifactMalformedException;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile.Processor;
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.game.DefaultGameRepository;
@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.JavaVersion;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import org.jetbrains.annotations.NotNull;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -45,6 +47,7 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
@ -55,6 +58,118 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
public class ForgeNewInstallTask extends Task<Version> {
private class ProcessorTask extends Task<Void> {
private Processor processor;
private Map<String, String> vars;
public ProcessorTask(@NotNull Processor processor, @NotNull Map<String, String> vars) {
this.processor = processor;
this.vars = vars;
setSignificance(TaskSignificance.MODERATE);
}
@Override
public void execute() throws Exception {
Map<String, String> outputs = new HashMap<>();
boolean miss = false;
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
key = parseLiteral(key, vars, ExceptionalFunction.identity());
value = parseLiteral(value, vars, ExceptionalFunction.identity());
if (key == null || value == null) {
throw new ArtifactMalformedException("Invalid forge installation configuration");
}
outputs.put(key, value);
Path artifact = Paths.get(key);
if (Files.exists(artifact)) {
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
}
if (!Objects.equals(code, value)) {
Files.delete(artifact);
LOG.info("Found existing file is not valid: " + artifact);
miss = true;
}
} else {
miss = true;
}
}
if (!processor.getOutputs().isEmpty() && !miss) {
return;
}
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
if (!Files.isRegularFile(jar))
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
String mainClass;
try (JarFile jarFile = new JarFile(jar.toFile())) {
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
}
if (StringUtils.isBlank(mainClass))
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
for (Artifact artifact : processor.getClasspath()) {
Path file = gameRepository.getArtifactFile(version, artifact);
if (!Files.isRegularFile(file))
throw new Exception("Game processor dependency missing");
classpath.add(file.toString());
}
classpath.add(jar.toString());
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
command.add(mainClass);
List<String> args = new ArrayList<>(processor.getArgs().size());
for (String arg : processor.getArgs()) {
String parsed = parseLiteral(arg, vars, ExceptionalFunction.identity());
if (parsed == null)
throw new ArtifactMalformedException("Invalid forge installation configuration");
args.add(parsed);
}
command.addAll(args);
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
int exitCode = SystemUtils.callExternalProcess(command);
if (exitCode != 0)
throw new IOException("Game processor exited abnormally with code " + exitCode);
for (Map.Entry<String, String> entry : outputs.entrySet()) {
Path artifact = Paths.get(entry.getKey());
if (!Files.isRegularFile(artifact))
throw new FileNotFoundException("File missing: " + artifact);
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
}
if (!Objects.equals(code, entry.getValue())) {
Files.delete(artifact);
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
}
}
}
}
private final DefaultDependencyManager dependencyManager;
private final DefaultGameRepository gameRepository;
private final Version version;
@ -63,9 +178,13 @@ public class ForgeNewInstallTask extends Task<Version> {
private final List<Task<?>> dependencies = new LinkedList<>();
private ForgeNewInstallProfile profile;
private List<Processor> processors;
private Version forgeVersion;
private final String selfVersion;
private Path tempDir;
private AtomicInteger processorDoneCount = new AtomicInteger(0);
ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) {
this.dependencyManager = dependencyManager;
this.gameRepository = dependencyManager.getGameRepository();
@ -73,7 +192,7 @@ public class ForgeNewInstallTask extends Task<Version> {
this.installer = installer;
this.selfVersion = selfVersion;
setSignificance(TaskSignificance.MINOR);
setSignificance(TaskSignificance.MAJOR);
}
private static String replaceTokens(Map<String, String> tokens, String value) {
@ -150,6 +269,7 @@ public class ForgeNewInstallTask extends Task<Version> {
public void preExecute() throws Exception {
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
processors = profile.getProcessors();
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
for (Library library : profile.getLibraries()) {
@ -167,151 +287,74 @@ public class ForgeNewInstallTask extends Task<Version> {
FileUtils.copyFile(mainJar, dest);
}
}
} catch (ZipException ex) {
throw new ArtifactMalformedException("Malformed forge installer file", ex);
}
dependents.add(new GameLibrariesTask(dependencyManager, version, true, profile.getLibraries()));
}
private Task<?> createProcessorTask(Processor processor, Map<String, String> vars) {
Task<?> task = new ProcessorTask(processor, vars);
task.onDone().register(
() -> updateProgress(processorDoneCount.incrementAndGet(), processors.size()));
return task;
}
@Override
public void execute() throws Exception {
Path temp = Files.createTempDirectory("forge_installer");
int finished = 0;
tempDir = Files.createTempDirectory("forge_installer");
Map<String, String> vars = new HashMap<>();
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
Map<String, String> data = profile.getData();
updateProgress(0, processors.size());
for (Map.Entry<String, String> entry : data.entrySet()) {
for (Map.Entry<String, String> entry : profile.getData().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
data.put(key, parseLiteral(value,
vars.put(key, parseLiteral(value,
Collections.emptyMap(),
str -> {
Path dest = Files.createTempFile(temp, null, null);
Path dest = Files.createTempFile(tempDir, null, null);
FileUtils.copyFile(fs.getPath(str), dest);
return dest.toString();
}));
}
data.put("SIDE", "client");
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
data.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath());
data.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath());
data.put("INSTALLER", installer.toAbsolutePath().toString());
data.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath());
for (ForgeNewInstallProfile.Processor processor : processors) {
Map<String, String> outputs = new HashMap<>();
boolean miss = false;
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
key = parseLiteral(key, data, ExceptionalFunction.identity());
value = parseLiteral(value, data, ExceptionalFunction.identity());
if (key == null || value == null) {
throw new ArtifactMalformedException("Invalid forge installation configuration");
}
outputs.put(key, value);
Path artifact = Paths.get(key);
if (Files.exists(artifact)) {
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
}
if (!Objects.equals(code, value)) {
Files.delete(artifact);
LOG.info("Found existing file is not valid: " + artifact);
miss = true;
}
} else {
miss = true;
}
}
if (!processor.getOutputs().isEmpty() && !miss) {
continue;
}
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
if (!Files.isRegularFile(jar))
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
String mainClass;
try (JarFile jarFile = new JarFile(jar.toFile())) {
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
}
if (StringUtils.isBlank(mainClass))
throw new Exception("Game processor jar does not have main class " + jar);
List<String> command = new ArrayList<>();
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
command.add("-cp");
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
for (Artifact artifact : processor.getClasspath()) {
Path file = gameRepository.getArtifactFile(version, artifact);
if (!Files.isRegularFile(file))
throw new Exception("Game processor dependency missing");
classpath.add(file.toString());
}
classpath.add(jar.toString());
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
command.add(mainClass);
List<String> args = new ArrayList<>(processor.getArgs().size());
for (String arg : processor.getArgs()) {
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
if (parsed == null)
throw new ArtifactMalformedException("Invalid forge installation configuration");
args.add(parsed);
}
command.addAll(args);
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
int exitCode = SystemUtils.callExternalProcess(command);
if (exitCode != 0)
throw new IOException("Game processor exited abnormally");
for (Map.Entry<String, String> entry : outputs.entrySet()) {
Path artifact = Paths.get(entry.getKey());
if (!Files.isRegularFile(artifact))
throw new FileNotFoundException("File missing: " + artifact);
String code;
try (InputStream stream = Files.newInputStream(artifact)) {
code = encodeHex(digest("SHA-1", stream));
}
if (!Objects.equals(code, entry.getValue())) {
Files.delete(artifact);
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
}
}
updateProgress(++finished, processors.size());
}
} catch (ZipException ex) {
throw new ArtifactMalformedException("Malformed forge installer file", ex);
}
vars.put("SIDE", "client");
vars.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
vars.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath());
vars.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath());
vars.put("INSTALLER", installer.toAbsolutePath().toString());
vars.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath());
updateProgress(0, processors.size());
Task<?> processorsTask = Task.runSequentially(
processors.stream()
.map(processor -> createProcessorTask(processor, vars))
.toArray(Task<?>[]::new));
dependencies.add(
processorsTask.thenComposeAsync(
dependencyManager.checkLibraryCompletionAsync(forgeVersion, true)));
setResult(forgeVersion
.setPriority(30000)
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
.setVersion(selfVersion));
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion, true));
}
FileUtils.deleteDirectory(temp.toFile());
@Override
public boolean doPostExecute() {
return true;
}
@Override
public void postExecute() throws Exception {
FileUtils.deleteDirectory(tempDir.toFile());
}
}

View File

@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 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
@ -49,7 +49,7 @@ public class ForgeOldInstallTask extends Task<Version> {
this.installer = installer;
this.selfVersion = selfVersion;
setSignificance(TaskSignificance.MINOR);
setSignificance(TaskSignificance.MAJOR);
}
@Override

View File

@ -1,6 +1,6 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
* Copyright (C) 2021 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
@ -902,6 +902,25 @@ public abstract class Task<T> {
};
}
/**
* Returns a new task that runs the given tasks sequentially
* and returns the result of the last task.
*
* @param tasks tasks to run sequentially
* @return the combination of these tasks
*/
public static Task<?> runSequentially(Task<?>... tasks) {
if (tasks.length == 0) {
return new SimpleTask<>(() -> null);
}
Task<?> task = tasks[0];
for (int i = 1; i < tasks.length; i++) {
task = task.thenComposeAsync(tasks[i]);
}
return task;
}
public enum TaskSignificance {
MAJOR,
MODERATE,