From 06833a360734de6905e3f0f646c9452346593fe2 Mon Sep 17 00:00:00 2001 From: FalsePattern <30945458+FalsePattern@users.noreply.github.com> Date: Sun, 19 Jun 2022 22:42:52 +0200 Subject: [PATCH] port update checker to CompletableFuture --- .../falsepattern/lib/internal/Internet.java | 18 ++ .../lib/internal/proxy/ClientProxy.java | 29 +--- .../lib/internal/proxy/CommonProxy.java | 55 ++---- .../lib/updates/UpdateCheckException.java | 22 +++ .../lib/updates/UpdateChecker.java | 157 +++++++++--------- 5 files changed, 144 insertions(+), 137 deletions(-) create mode 100644 src/main/java/com/falsepattern/lib/updates/UpdateCheckException.java diff --git a/src/main/java/com/falsepattern/lib/internal/Internet.java b/src/main/java/com/falsepattern/lib/internal/Internet.java index a08b179..4d99332 100644 --- a/src/main/java/com/falsepattern/lib/internal/Internet.java +++ b/src/main/java/com/falsepattern/lib/internal/Internet.java @@ -6,6 +6,9 @@ import lombok.var; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; public class Internet { @@ -27,6 +30,21 @@ public class Internet { } } + public static CompletableFuture download(URL URL) { + return CompletableFuture.supplyAsync(() -> { + val result = new ByteArrayOutputStream(); + AtomicReference caught = new AtomicReference<>(); + connect(URL, caught::set, (input) -> { + try { + transferAndClose(input, result); + } catch (IOException e) { + throw new CompletionException(e); + } + }); + return result.toByteArray(); + }); + } + public static void transferAndClose(InputStream is, OutputStream target) throws IOException { var bytesRead = 0; diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java index 02772b1..5d5653f 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/ClientProxy.java @@ -3,7 +3,7 @@ package com.falsepattern.lib.internal.proxy; import com.falsepattern.lib.internal.FalsePatternLib; import com.falsepattern.lib.internal.Tags; import com.falsepattern.lib.updates.UpdateChecker; -import com.falsepattern.lib.util.AsyncUtil; +import cpw.mods.fml.client.IModGuiFactory; import cpw.mods.fml.common.event.FMLPostInitializationEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import cpw.mods.fml.common.eventhandler.SubscribeEvent; @@ -15,14 +15,13 @@ import net.minecraft.util.IChatComponent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.entity.EntityJoinWorldEvent; +import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; @SideOnly(Side.CLIENT) public class ClientProxy extends CommonProxy { - private Future> chatFuture; + private CompletableFuture> chatFuture; @Override public void preInit(FMLPreInitializationEvent e) { @@ -33,21 +32,11 @@ public class ClientProxy extends CommonProxy { @Override public void postInit(FMLPostInitializationEvent e) { super.postInit(e); - chatFuture = AsyncUtil.asyncWorker.submit(new Callable>() { - @Override - public List call() throws Exception { - //Deadlock avoidance - if (updatesFuture == null || updatesFuture.isCancelled()) { - chatFuture = null; - return null; - } - if (!updatesFuture.isDone()) { - chatFuture = AsyncUtil.asyncWorker.submit(this); - return null; - } - val updates = updatesFuture.get(); - return UpdateChecker.updateListToChatMessages(Tags.MODNAME, updates); + chatFuture = updatesFuture.handleAsync((updates, exception) -> { + if (exception != null || updates.isEmpty()) { + return Collections.emptyList(); } + return UpdateChecker.updateListToChatMessages(Tags.MODNAME, updates); }); } @@ -57,7 +46,7 @@ public class ClientProxy extends CommonProxy { !(e.entity instanceof EntityPlayerSP)) return; val player = (EntityPlayerSP) e.entity; try { - for (val line: chatFuture.get(1, TimeUnit.SECONDS)) { + for (val line: chatFuture.get()) { player.addChatMessage(line); } chatFuture = null; diff --git a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java index c0067ec..59111a3 100644 --- a/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java +++ b/src/main/java/com/falsepattern/lib/internal/proxy/CommonProxy.java @@ -6,21 +6,17 @@ import com.falsepattern.lib.internal.FalsePatternLib; import com.falsepattern.lib.internal.LibraryConfig; import com.falsepattern.lib.updates.ModUpdateInfo; import com.falsepattern.lib.updates.UpdateChecker; -import com.falsepattern.lib.util.AsyncUtil; -import cpw.mods.fml.common.event.FMLConstructionEvent; import cpw.mods.fml.common.event.FMLPostInitializationEvent; import cpw.mods.fml.common.event.FMLPreInitializationEvent; import lombok.val; -import lombok.var; +import java.util.Collections; import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; +import java.util.concurrent.CompletableFuture; public class CommonProxy { - protected Future> updatesFuture; + protected CompletableFuture> updatesFuture; public void preInit(FMLPreInitializationEvent e) { ConfigurationManager.registerBus(); @@ -31,37 +27,22 @@ public class CommonProxy { } if (LibraryConfig.ENABLE_UPDATE_CHECKER) { FalsePatternLib.getLog().info("Launching asynchronous update check."); - val updateCheckFuture = UpdateChecker.fetchUpdatesAsync(FalsePatternLib.UPDATE_URL); - updatesFuture = AsyncUtil.asyncWorker.submit(new Callable>() { - @Override - public List call() { - //Deadlock avoidance - if (updateCheckFuture.isCancelled()) { - updatesFuture = null; - return null; - } - if (!updateCheckFuture.isDone()) { - updatesFuture = AsyncUtil.asyncWorker.submit(this); - return null; - } - try { - var updates = updateCheckFuture.get(); - if (updates != null && updates.size() > 0) { - for (val update : updates) { - update.log(FalsePatternLib.getLog()); - } - } else if (updates == null) { - FalsePatternLib.getLog().warn("Unknown error while checking updates."); - } else { - FalsePatternLib.getLog().info("All checked mods up to date!"); - updates = null; - } - return updates; - } catch (InterruptedException | ExecutionException ex) { - FalsePatternLib.getLog().warn("Error while checking updates", ex); - } - return null; + updatesFuture = UpdateChecker.fetchUpdatesAsync(FalsePatternLib.UPDATE_URL).thenApplyAsync(updates -> { + if (updates == null) { + updates = Collections.emptyList(); } + if (updates.isEmpty()) { + FalsePatternLib.getLog().info("No updates found."); + } else { + FalsePatternLib.getLog().info("Found {} updates.", updates.size()); + for (val update : updates) { + update.log(FalsePatternLib.getLog()); + } + } + return updates; + }).exceptionally(ex -> { + FalsePatternLib.getLog().error("Failed to check for updates!", ex); + return Collections.emptyList(); }); } } diff --git a/src/main/java/com/falsepattern/lib/updates/UpdateCheckException.java b/src/main/java/com/falsepattern/lib/updates/UpdateCheckException.java new file mode 100644 index 0000000..c53dfb1 --- /dev/null +++ b/src/main/java/com/falsepattern/lib/updates/UpdateCheckException.java @@ -0,0 +1,22 @@ +package com.falsepattern.lib.updates; + +import com.falsepattern.lib.StableAPI; + +@StableAPI(since = "0.8.3") +public class UpdateCheckException extends Exception { + public UpdateCheckException(String message) { + super(message); + } + + public UpdateCheckException(String message, Throwable cause) { + super(message, cause); + } + + public UpdateCheckException(Throwable cause) { + super(cause); + } + + protected UpdateCheckException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java index 0767560..d7acd68 100644 --- a/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java +++ b/src/main/java/com/falsepattern/lib/updates/UpdateChecker.java @@ -4,41 +4,29 @@ import com.falsepattern.json.node.JsonNode; import com.falsepattern.lib.StableAPI; import com.falsepattern.lib.dependencies.DependencyLoader; import com.falsepattern.lib.dependencies.SemanticVersion; -import com.falsepattern.lib.internal.FalsePatternLib; import com.falsepattern.lib.internal.Internet; import com.falsepattern.lib.internal.LibraryConfig; import com.falsepattern.lib.internal.Tags; import com.falsepattern.lib.text.FormattedText; -import com.falsepattern.lib.util.AsyncUtil; import cpw.mods.fml.common.Loader; import lombok.val; import net.minecraft.client.resources.I18n; import net.minecraft.event.ClickEvent; import net.minecraft.util.IChatComponent; -import java.io.ByteArrayOutputStream; -import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; @StableAPI(since = "0.8.0") public class UpdateChecker { - private static final AtomicInteger jsonLibraryLoaded = new AtomicInteger(0); - /** - * Same this as {@link #fetchUpdates(String)}, but defers the check to a different thread. Useful for asynchronous - * update checks, if you don't want to block loading. - * @param url The URL to check - * @return A future that will contain the update info about mods that were both available on the URL and installed - */ - public static Future> fetchUpdatesAsync(String url) { - return AsyncUtil.asyncWorker.submit(() -> fetchUpdates(url)); - } + private static final AtomicBoolean jsonLibraryLoaded = new AtomicBoolean(false); + /** * Checks for updates. The URL should be a JSON file that contains a list of mods, each with a mod ID, one or more @@ -62,21 +50,20 @@ public class UpdateChecker { * @param url The URL to check * @return A list of mods that were both available on the URL and installed */ - public static List fetchUpdates(String url) { - if (!LibraryConfig.ENABLE_UPDATE_CHECKER) { - return null; - } - URL URL; - try { - URL = new URL(url); - } catch (MalformedURLException e) { - FalsePatternLib.getLog().error("Invalid URL: {}", url, e); - return null; - } - switch (jsonLibraryLoaded.get()) { - case 0: - DependencyLoader.addMavenRepo("https://maven.falsepattern.com/"); + public static CompletableFuture> fetchUpdatesAsync(String url) { + return CompletableFuture.supplyAsync(() -> { + if (!LibraryConfig.ENABLE_UPDATE_CHECKER) { + throw new CompletionException(new UpdateCheckException("Update checker is disabled in config!")); + } + URL URL; + try { + URL = new URL(url); + } catch (MalformedURLException e) { + throw new CompletionException(new UpdateCheckException("Invalid URL: " + url, e)); + } + if (!jsonLibraryLoaded.get()) { try { + DependencyLoader.addMavenRepo("https://maven.falsepattern.com/"); DependencyLoader.builder() .loadingModId(Tags.MODID) .groupId("com.falsepattern") @@ -86,61 +73,71 @@ public class UpdateChecker { .preferredVersion(new SemanticVersion(0, 4, 1)) .build(); } catch (Exception e) { - FalsePatternLib.getLog().error("Failed to load json library for update checker!", e); - jsonLibraryLoaded.set(-1); - return null; + throw new CompletionException(new UpdateCheckException("Failed to load json library for update checker!", e)); } - jsonLibraryLoaded.set(1); - break; - case -1: - return null; - } - AtomicReference loadedData = new AtomicReference<>(null); - Internet.connect(URL, (ex) -> FalsePatternLib.getLog().warn("Failed to check for updates from URL {}", url, ex), (input) -> { - val data = new ByteArrayOutputStream(); + jsonLibraryLoaded.set(true); + } + val result = new ArrayList(); + JsonNode parsed; try { - Internet.transferAndClose(input, data); - } catch (IOException e) { - throw new RuntimeException(e); + parsed = JsonNode.parse(Internet.download(URL).thenApply(String::new).join()); + } catch (CompletionException e) { + throw new CompletionException(new UpdateCheckException("Failed to download update checker JSON file!", e.getCause() == null ? e : e.getCause())); } - loadedData.set(data.toString()); - }); - if (loadedData.get() == null) return null; - val result = new ArrayList(); - val parsed = JsonNode.parse(loadedData.get()); - List modList; - if (parsed.isList()) { - modList = parsed.getJavaList(); - } else { - modList = Collections.singletonList(parsed); - } - val installedMods = Loader.instance().getIndexedModList(); - for (val node: modList) { - if (!node.isObject()) continue; - if (!node.containsKey("modid")) continue; - if (!node.containsKey("latestVersion")) continue; - val modid = node.getString("modid"); - if (!installedMods.containsKey(modid)) continue; - val mod = installedMods.get(modid); - val latestVersionsNode = node.get("latestVersion"); - List latestVersions; - if (latestVersionsNode.isString()) { - latestVersions = Collections.singletonList(latestVersionsNode.stringValue()); - } else if (latestVersionsNode.isList()) { - latestVersions = new ArrayList<>(); - for (val version: latestVersionsNode.getJavaList()) { - if (!version.isString()) continue; - latestVersions.add(version.stringValue()); - } + List modList; + if (parsed.isList()) { + modList = parsed.getJavaList(); } else { - continue; + modList = Collections.singletonList(parsed); + } + val installedMods = Loader.instance().getIndexedModList(); + for (val node: modList) { + if (!node.isObject()) continue; + if (!node.containsKey("modid")) continue; + if (!node.containsKey("latestVersion")) continue; + val modid = node.getString("modid"); + if (!installedMods.containsKey(modid)) continue; + val mod = installedMods.get(modid); + val latestVersionsNode = node.get("latestVersion"); + List latestVersions; + if (latestVersionsNode.isString()) { + latestVersions = Collections.singletonList(latestVersionsNode.stringValue()); + } else if (latestVersionsNode.isList()) { + latestVersions = new ArrayList<>(); + for (val version: latestVersionsNode.getJavaList()) { + if (!version.isString()) continue; + latestVersions.add(version.stringValue()); + } + } else { + continue; + } + val currentVersion = mod.getVersion(); + if (latestVersions.contains(currentVersion)) continue; + val updateURL = node.containsKey("updateURL") && node.get("updateURL").isString() ? node.getString("updateURL") : ""; + result.add(new ModUpdateInfo(modid, currentVersion, latestVersions.get(0), updateURL)); + } + return result; + }); + } + + /** + * Same this as {@link #fetchUpdatesAsync(String)}, but returns the result in a blocking fashion. + * @param url The URL to check + * @return A future that will contain the update info about mods that were both available on the URL and installed + * @throws UpdateCheckException If the update checker is disabled in config, the URL is invalid, or + */ + public static List fetchUpdates(String url) throws UpdateCheckException { + try { + return fetchUpdatesAsync(url).join(); + } catch (CompletionException e) { + try { + throw e.getCause(); + } catch (UpdateCheckException e1) { + throw e1; + } catch (Throwable e1) { + throw new UpdateCheckException("Failed to check for updates!", e1); } - val currentVersion = mod.getVersion(); - if (latestVersions.contains(currentVersion)) continue; - val updateURL = node.containsKey("updateURL") && node.get("updateURL").isString() ? node.getString("updateURL") : ""; - result.add(new ModUpdateInfo(modid, currentVersion, latestVersions.get(0), updateURL)); } - return result; } /**