From 9bc10fb27d5d212e78e15c91ae17ec8195cfbea1 Mon Sep 17 00:00:00 2001 From: Haowei Wen Date: Thu, 19 Aug 2021 23:21:00 +0800 Subject: [PATCH] use separate tasks for forge installer processors --- .../hmcl/ui/construct/TaskListPane.java | 7 +- .../hmcl/download/forge/ForgeInstallTask.java | 3 +- .../download/forge/ForgeNewInstallTask.java | 287 ++++++++++-------- .../download/forge/ForgeOldInstallTask.java | 4 +- .../java/org/jackhuang/hmcl/task/Task.java | 21 +- 5 files changed, 193 insertions(+), 129 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index a765b36d8..e91c2b76d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui 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"))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 01c4ca9f6..75bf77dfe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui 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 { this.dependencyManager = dependencyManager; this.version = version; this.remote = remoteVersion; + setSignificance(TaskSignificance.MODERATE); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java index edd7eac35..70b3920fc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui 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 { + private class ProcessorTask extends Task { + + private Processor processor; + private Map vars; + + public ProcessorTask(@NotNull Processor processor, @NotNull Map vars) { + this.processor = processor; + this.vars = vars; + setSignificance(TaskSignificance.MODERATE); + } + + @Override + public void execute() throws Exception { + Map outputs = new HashMap<>(); + boolean miss = false; + + for (Map.Entry 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 command = new ArrayList<>(); + command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); + command.add("-cp"); + + List 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 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 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 { private final List> dependencies = new LinkedList<>(); private ForgeNewInstallProfile profile; + private List 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 { this.installer = installer; this.selfVersion = selfVersion; - setSignificance(TaskSignificance.MINOR); + setSignificance(TaskSignificance.MAJOR); } private static String replaceTokens(Map tokens, String value) { @@ -150,6 +269,7 @@ public class ForgeNewInstallTask extends Task { 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 { 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 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 vars = new HashMap<>(); + try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { - List processors = profile.getProcessors(); - Map data = profile.getData(); - - updateProgress(0, processors.size()); - - for (Map.Entry entry : data.entrySet()) { + for (Map.Entry 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 outputs = new HashMap<>(); - boolean miss = false; - - for (Map.Entry 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 command = new ArrayList<>(); - command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); - command.add("-cp"); - - List 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 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 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()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java index d7eefd85a..60d68e44a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui 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 { this.installer = installer; this.selfVersion = selfVersion; - setSignificance(TaskSignificance.MINOR); + setSignificance(TaskSignificance.MAJOR); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java index 00b88969c..956652b3e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2021 huangyuhui 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 { }; } + /** + * 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,