Enhance instance manifest handling and add failed task creation method (#7163)
This commit is contained in:
1 parent
f81ba320da
commit
75d070d8c9
4 files changed
+84
-17
No files matched your search
@@ -5,6 +5,7 @@ package org.jackhuang.hmcl.ui.wizard;
|
||||
import javafx.scene.Node;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.SettingsMap;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@@ -117,9 +118,17 @@ public class WizardController implements Navigation {
|
||||
return pages.size() > 1;
|
||||
}
|
||||
|
||||
/// Finishes the provider and displays its result. Runtime exceptions thrown while preparing
|
||||
/// the result are passed to the displayer as failed tasks, using its normal failure and cleanup handling.
|
||||
@Override
|
||||
public void onFinish() {
|
||||
Object result = provider.finish(settings);
|
||||
WizardProvider currentProvider = Objects.requireNonNull(provider);
|
||||
@Nullable Object result;
|
||||
try {
|
||||
result = currentProvider.finish(settings);
|
||||
} catch (RuntimeException failure) {
|
||||
result = Task.failed(failure);
|
||||
}
|
||||
if (result instanceof Summary) displayer.navigateTo(((Summary) result).getComponent(), NavigationDirection.NEXT);
|
||||
else if (result instanceof Task<?>) displayer.handleTask(settings, ((Task<?>) result));
|
||||
else if (result != null) throw new IllegalStateException("Unrecognized wizard result: " + result);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -319,7 +320,7 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
json = conventionalJson;
|
||||
} else {
|
||||
List<Path> jsons = FileUtils.listFilesByExtension(dir, "json");
|
||||
@Nullable Path candidate = jsons.size() == 1 ? jsons.get(0) : findInstanceManifest(jsons);
|
||||
@Nullable Path candidate = jsons.size() == 1 ? jsons.get(0) : findInstanceManifest(jsons, id);
|
||||
if (candidate == null) {
|
||||
LOG.info("No available json file found, ignoring instance " + id);
|
||||
return null;
|
||||
@@ -335,17 +336,12 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
|
||||
GameInstanceManifest manifest;
|
||||
try {
|
||||
manifest = readInstanceManifest(json);
|
||||
manifest = readInstanceManifest(json, id);
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Malformed instance json " + id + " (" + json + ")", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Directory name is the repository identity; keep the on-disk files untouched.
|
||||
if (!id.equals(manifest.id())) {
|
||||
manifest = manifest.withId(id);
|
||||
}
|
||||
|
||||
return createInstance(snapshot, id, manifest, manifestFileOverride);
|
||||
}
|
||||
|
||||
@@ -353,11 +349,12 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
/// a parent instance, or non-empty patches, skipping unreadable and malformed candidates.
|
||||
///
|
||||
/// @param jsons the candidate JSON files; the list is not modified
|
||||
/// @param id the directory-derived instance identity
|
||||
/// @return the selected path, or `null` if no candidate matches
|
||||
private static @Nullable Path findInstanceManifest(List<Path> jsons) {
|
||||
private static @Nullable Path findInstanceManifest(List<Path> jsons, GameInstanceID id) {
|
||||
for (Path json : jsons.stream().sorted(Comparator.comparing(FileUtils::getName)).toList()) {
|
||||
try {
|
||||
GameInstanceManifest manifest = readInstanceManifest(json);
|
||||
GameInstanceManifest manifest = readInstanceManifest(json, id);
|
||||
@Nullable String mainClass = manifest.mainClass();
|
||||
if ((mainClass != null && !mainClass.isBlank())
|
||||
|| manifest.inheritsFrom() != null
|
||||
@@ -370,18 +367,24 @@ public abstract class DefaultGameRepository implements GameRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Reads a non-null instance manifest from a JSON file.
|
||||
/// Reads an instance manifest using the directory-derived identity.
|
||||
///
|
||||
/// Replaces the JSON object's `id` in memory before parsing the manifest, including when
|
||||
/// the original member is absent, null, or invalid. The file is not modified.
|
||||
///
|
||||
/// @param json the manifest file
|
||||
/// @return the parsed manifest
|
||||
/// @param id the directory-derived instance identity
|
||||
/// @return the parsed manifest with the supplied identity
|
||||
/// @throws IOException if the file cannot be read
|
||||
/// @throws JsonParseException if the JSON is malformed or does not describe a manifest
|
||||
private static GameInstanceManifest readInstanceManifest(Path json) throws IOException, JsonParseException {
|
||||
@Nullable GameInstanceManifest manifest = JsonUtils.fromJsonFile(json, GameInstanceManifest.class);
|
||||
if (manifest == null) {
|
||||
/// @throws JsonParseException if the JSON is malformed, is not an object, or cannot be parsed as a manifest
|
||||
private static GameInstanceManifest readInstanceManifest(Path json, GameInstanceID id) throws IOException, JsonParseException {
|
||||
@Nullable JsonObject object = JsonUtils.fromJsonFile(json, JsonObject.class);
|
||||
if (object == null) {
|
||||
throw new JsonParseException("Manifest is null");
|
||||
}
|
||||
return manifest;
|
||||
// Supply the repository identity before the manifest parser validates the required ID.
|
||||
object.addProperty("id", id.toString());
|
||||
return GameInstanceManifest.fromJson(object, false);
|
||||
}
|
||||
|
||||
static void moveInstanceFiles(Path baseDirectory, GameInstanceID from, GameInstanceID to) throws IOException {
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalConsumer;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalFunction;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.function.ExceptionalSupplier;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
@@ -964,6 +965,19 @@ public abstract class Task<T> {
|
||||
return fromCompletableFuture(CompletableFuture.completedFuture(value));
|
||||
}
|
||||
|
||||
/// Creates a task backed by a future that has already completed exceptionally.
|
||||
///
|
||||
/// The failure is processed by the task executor when the task is submitted;
|
||||
/// this method does not start execution or notify task listeners.
|
||||
///
|
||||
/// @param <V> the task's result type
|
||||
/// @param failure the exception completing the backing future
|
||||
/// @return a new task backed by the failed future
|
||||
/// @throws NullPointerException if failure is null
|
||||
public static <V> @NotNull Task<V> failed(@NotNull Exception failure) {
|
||||
return fromCompletableFuture(CompletableFuture.failedFuture(failure));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new Task that is completed when all of the given Tasks
|
||||
* complete. If any of the given Tasks complete exceptionally,
|
||||
|
||||
@@ -61,6 +61,47 @@ public final class DefaultGameRepositoryDraftTest {
|
||||
assertFalse(Files.exists(repository.getLayout().getInstanceJson(id)));
|
||||
}
|
||||
|
||||
/// Supplies missing IDs for conventional, single-candidate, and multiple-candidate manifests without rewriting files.
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"conventional", "single", "multiple"})
|
||||
public void testRefreshSuppliesMissingManifestId(String layout, @TempDir Path tempDirectory) throws IOException {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
Path root = Files.createDirectories(repository.getLayout().getInstanceRoot(id));
|
||||
Path manifestFile = root.resolve(layout.equals("conventional") ? "instance.json" : "game.json");
|
||||
String originalJson = "{\"mainClass\":\"example.Main\"}";
|
||||
Files.writeString(manifestFile, originalJson);
|
||||
if (layout.equals("multiple")) {
|
||||
Files.writeString(root.resolve("a-settings.json"), "{}");
|
||||
Files.writeString(root.resolve("b-broken.json"), "{");
|
||||
}
|
||||
|
||||
repository.refresh();
|
||||
|
||||
DefaultGameInstance instance = repository.getInstance(id);
|
||||
assertEquals(id, instance.getManifest().id());
|
||||
assertEquals("example.Main", instance.getManifest().mainClass());
|
||||
assertEquals(manifestFile, instance.getManifestFile());
|
||||
assertEquals(originalJson, Files.readString(manifestFile));
|
||||
}
|
||||
|
||||
/// Uses the directory identity even when the JSON ID is null, invalid, or different.
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"null", "42", "{}", "\"..\"", "\"original\""})
|
||||
public void testRefreshOverridesManifestId(String jsonId, @TempDir Path tempDirectory) throws IOException {
|
||||
TestRepository repository = new TestRepository(tempDirectory);
|
||||
GameInstanceID id = new GameInstanceID("instance");
|
||||
Files.createDirectories(repository.getLayout().getInstanceRoot(id));
|
||||
Path manifestFile = repository.getLayout().getInstanceJson(id);
|
||||
String originalJson = "{\"id\":" + jsonId + ",\"mainClass\":\"example.Main\"}";
|
||||
Files.writeString(manifestFile, originalJson);
|
||||
|
||||
repository.refresh();
|
||||
|
||||
assertEquals(id, repository.getInstance(id).getManifest().id());
|
||||
assertEquals(originalJson, Files.readString(manifestFile));
|
||||
}
|
||||
|
||||
/// Ignores directories whose multiple JSON files do not contain an instance manifest.
|
||||
@Test
|
||||
public void testRefreshIgnoresUnrelatedJsonFiles(@TempDir Path tempDirectory) throws IOException {
|
||||
|
||||
Reference in new issue
Block a user