From 2b604bfd93f215d3c123c9edfa2654d96416eaf6 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 15:19:31 +0800 Subject: [PATCH 1/9] add: friendly prompt for corrupt authlib-injector --- .../main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 60360ea32..e35c1aa57 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -23,7 +23,9 @@ import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthenticationException; +import org.jackhuang.hmcl.auth.CharacterDeletedException; import org.jackhuang.hmcl.auth.CredentialExpiredException; +import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.MaintainTask; import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask; @@ -242,6 +244,10 @@ public final class LauncherHelper { message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause()); } else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) { message = i18n("assets.index.malformed"); + } else if (ex instanceof AuthlibInjectorDownloadException) { + message = i18n("account.failed.injector_download_failure"); + } else if (ex instanceof CharacterDeletedException) { + message = i18n("account.failed.character_deleted"); } else { message = StringUtils.getStackTrace(ex); } From 31e39900b616f5408222817bfef023364aa646e8 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 15:20:07 +0800 Subject: [PATCH 2/9] fix: unable to launch when asset index not downloaded --- .../download/game/GameAssetDownloadTask.java | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java index 413394bf6..0ea5376ea 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java @@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.CacheRepository; import org.jackhuang.hmcl.util.gson.JsonUtils; import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.LinkedList; import java.util.List; @@ -47,8 +48,6 @@ public final class GameAssetDownloadTask extends Task { private final File assetIndexFile; private final List> dependents = new LinkedList<>(); private final List> dependencies = new LinkedList<>(); - private AssetIndex index; - private boolean retry = false; /** * Constructor. @@ -62,8 +61,15 @@ public final class GameAssetDownloadTask extends Task { this.assetIndexInfo = this.version.getAssetIndex(); this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); - if (!assetIndexFile.exists() || forceDownloadingIndex) + if (!assetIndexFile.exists() || forceDownloadingIndex) { dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.version)); + } else { + try { + JsonUtils.GSON.fromJson(FileUtils.readText(assetIndexFile), AssetIndex.class); + } catch (IOException | JsonSyntaxException e) { + dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.version)); + } + } } @Override @@ -77,28 +83,12 @@ public final class GameAssetDownloadTask extends Task { } @Override - public boolean doPreExecute() { - return true; - } - - @Override - public void preExecute() throws Exception { + public void execute() throws Exception { + AssetIndex index; try { index = JsonUtils.GSON.fromJson(FileUtils.readText(assetIndexFile), AssetIndex.class); - } catch (JsonSyntaxException e) { - dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.version)); - retry = true; - } - } - - @Override - public void execute() throws Exception { - if (retry) { - try { - index = JsonUtils.GSON.fromJson(FileUtils.readText(assetIndexFile), AssetIndex.class); - } catch (JsonSyntaxException e) { - throw new GameAssetIndexDownloadTask.GameAssetIndexMalformedException(); - } + } catch (IOException | JsonSyntaxException e) { + throw new GameAssetIndexDownloadTask.GameAssetIndexMalformedException(); } int progress = 0; From 9c1905bb510fe2775bb751371ad301dfe1c48800 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 15:26:37 +0800 Subject: [PATCH 3/9] no longer download file from BMCLAPI when the official download provider is activated --- .../org/jackhuang/hmcl/download/MojangDownloadProvider.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java index 3e0873edf..21e71c42f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MojangDownloadProvider.java @@ -72,7 +72,6 @@ public class MojangDownloadProvider implements DownloadProvider { @Override public String injectURL(String baseURL) { - return baseURL - .replaceFirst("https?://files\\.minecraftforge\\.net/maven", "https://bmclapi2.bangbang93.com/maven"); + return baseURL; } } From c7e363915532e1effda3e609c19c0fc365a41a7d Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 22:38:59 +0800 Subject: [PATCH 4/9] fix: download missing library more than one time --- HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java index 8a19df160..fd1346304 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java @@ -217,9 +217,12 @@ public class Version implements Comparable, Validation { } /** - * Resolve given version + * Resolve given version. + * Resolving version will list all patches within this version and its parents, + * which is for analysis. */ public Version resolve(VersionProvider provider) throws VersionNotFoundException { + if (isResolved()) return this; return resolve(provider, new HashSet<>()).setResolved(); } From b154871207d2ed8787d870a8a68574d6071988e2 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 22:45:54 +0800 Subject: [PATCH 5/9] add: friendly prompt for 404 --- .../jackhuang/hmcl/game/LauncherHelper.java | 31 +++++++++++++++++-- .../ui/download/InstallerWizardProvider.java | 15 ++++++++- .../resources/assets/lang/I18N.properties | 4 +-- .../resources/assets/lang/I18N_es.properties | 4 +-- .../resources/assets/lang/I18N_ru.properties | 4 +-- .../resources/assets/lang/I18N_zh.properties | 4 +-- .../assets/lang/I18N_zh_CN.properties | 4 +-- 7 files changed, 53 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index e35c1aa57..b8083d003 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -57,6 +57,7 @@ import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.function.ExceptionalSupplier; import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter; +import org.jackhuang.hmcl.util.io.ResponseCodeException; import org.jackhuang.hmcl.util.platform.CommandBuilder; import org.jackhuang.hmcl.util.platform.JavaVersion; import org.jackhuang.hmcl.util.platform.ManagedProcess; @@ -66,7 +67,14 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.*; +import java.net.URL; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; @@ -241,13 +249,32 @@ public final class LauncherHelper { } else if (ex instanceof NotDecompressingNativesException) { message = i18n("launch.failed.decompressing_natives") + ex.getLocalizedMessage(); } else if (ex instanceof LibraryDownloadException) { - message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(ex.getCause()); + message = i18n("launch.failed.download_library", ((LibraryDownloadException) ex).getLibrary().getName()) + "\n"; + if (ex.getCause() instanceof ResponseCodeException) { + ResponseCodeException rce = (ResponseCodeException) ex.getCause(); + int responseCode = rce.getResponseCode(); + URL url = rce.getUrl(); + if (responseCode == 404) + message += i18n("download.code.404", url); + else + message += i18n("download.failed", url, responseCode); + } else { + message += StringUtils.getStackTrace(ex.getCause()); + } } else if (ex instanceof GameAssetIndexDownloadTask.GameAssetIndexMalformedException) { message = i18n("assets.index.malformed"); } else if (ex instanceof AuthlibInjectorDownloadException) { message = i18n("account.failed.injector_download_failure"); } else if (ex instanceof CharacterDeletedException) { message = i18n("account.failed.character_deleted"); + } else if (ex instanceof ResponseCodeException) { + ResponseCodeException rce = (ResponseCodeException) ex; + int responseCode = rce.getResponseCode(); + URL url = rce.getUrl(); + if (responseCode == 404) + message = i18n("download.code.404", url); + else + message = i18n("download.failed", url, responseCode); } else { message = StringUtils.getStackTrace(ex); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java index e8e788350..c15554468 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallerWizardProvider.java @@ -39,6 +39,7 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.ResponseCodeException; import java.net.SocketTimeoutException; +import java.net.URL; import java.util.Map; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -103,7 +104,19 @@ public final class InstallerWizardProvider implements WizardProvider { public static void alertFailureMessage(Exception exception, Runnable next) { if (exception instanceof LibraryDownloadException) { - Controllers.dialog(i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n" + StringUtils.getStackTrace(exception.getCause()), i18n("install.failed.downloading"), MessageType.ERROR, next); + String message = i18n("launch.failed.download_library", ((LibraryDownloadException) exception).getLibrary().getName()) + "\n"; + if (exception.getCause() instanceof ResponseCodeException) { + ResponseCodeException rce = (ResponseCodeException) exception.getCause(); + int responseCode = rce.getResponseCode(); + URL url = rce.getUrl(); + if (responseCode == 404) + message += i18n("download.code.404", url); + else + message += i18n("download.failed", url, responseCode); + } else { + message += StringUtils.getStackTrace(exception.getCause()); + } + Controllers.dialog(message, i18n("install.failed.downloading"), MessageType.ERROR, next); } else if (exception instanceof DownloadException) { if (exception.getCause() instanceof SocketTimeoutException) { Controllers.dialog(i18n("install.failed.downloading.timeout", ((DownloadException) exception).getUrl()), i18n("install.failed.downloading"), MessageType.ERROR, next); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 39c4788aa..bdf3a998e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -92,8 +92,8 @@ crash.NoClassDefFound=Please verify that the "Hello Minecraft! Launcher" softwar crash.user_fault=Your OS or Java environment may not be properly installed which may result in a crash, please check your Java Runtime Environment or your computer! download=Download -download.code.404=File not found on the remote server -download.failed=Failed to download +download.code.404=File not found on the remote server: %s +download.failed=Failed to download %1$s, response code: %2$d download.failed.empty=No candidates. Click here to return. download.failed.refresh=Unable to download version list. Click here to retry. download.provider.mcbbs=MCBBS (https://www.mcbbs.net/) diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index fcc50a6d2..9c4b0e524 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -91,8 +91,8 @@ crash.NoClassDefFound=Favor verificar que el software "Hello Minecraft! Launcher crash.user_fault=Su SO o ambiente de Java podría estar mal instalado resultando en fallos de este software, ¡por favor verifique su ambiente de Java o computadora! download=Descargar -download.code.404=Archivo no encontrado en servidor remoto -download.failed=Falló en descargar +download.code.404=Archivo no encontrado en servidor remoto: %s +download.failed=Falló en descargar: %1$s download.failed.empty=No hay candidatos. Clic aquí para regresar. download.failed.refresh=No se pudo cargar lista de versiones. Clic aquí para reintentar. download.provider.mcbbs=MCBBS (https://www.mcbbs.net/) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index a23dff43b..0db512026 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -91,8 +91,8 @@ crash.NoClassDefFound=Пожалуйста, проверьте, что прог crash.user_fault=Ваша ОС или среда Java могут быть неправильно установлены, что приведет к сбою этого программного обеспечения, пожалуйста, проверьте свою среду Java или свой компьютер\! download=Загрузка -download.code.404=Файл не найден на удаленном сервере -download.failed=Не удалось загрузить +download.code.404=Файл не найден на удаленном сервере: %s +download.failed=Не удалось загрузить: %1$s download.failed.empty=Нет вариантов. Нажмите здесь, чтобы вернуться. download.failed.refresh=Невозможно загрузить список версий. Нажмите здесь, чтобы повторить попытку. download.provider.mcbbs=MCBBS (https://www.mcbbs.net/) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index d4909fc5d..8345b1c2c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -91,8 +91,8 @@ crash.NoClassDefFound=請確認 Hello Minecraft! Launcher 本體是否完整, crash.user_fault=您的系統或 Java 環境可能安裝不當導致本軟體崩潰,請檢查您的 Java 環境或您的電腦!可以嘗試重新安裝 Java。 download=下載 -download.code.404=遠程伺服器不包含需要下載的文件 -download.failed=下載失敗 +download.code.404=遠程伺服器不包含需要下載的文件: %s +download.failed=下載失敗: %1$s,錯誤碼:%2$d download.failed.empty=沒有可供安裝的版本,點擊此處返回。 download.failed.refresh=載入版本列表失敗,點擊此處重試。 download.provider.mcbbs=我的世界中文論壇 (MCBBS, https://www.mcbbs.net/) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 983217a14..cabe72dc6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -91,8 +91,8 @@ crash.NoClassDefFound=请确认 Hello Minecraft! Launcher 本体是否完整, crash.user_fault=您的系统或 Java 环境可能安装不当导致本软件崩溃,请检查您的 Java 环境或您的电脑!可以尝试重新安装 Java。 download=下载 -download.code.404=远程服务器不包含需要下载的文件 -download.failed=下载失败 +download.code.404=远程服务器不包含需要下载的文件: %s +download.failed=下载失败: %1$s,错误码:%2$d download.failed.empty=没有可供安装的版本,点击此处返回。 download.failed.refresh=加载版本列表失败,点击此处重试。 download.provider.mcbbs=我的世界中文论坛 (MCBBS, https://www.mcbbs.net/) From a162828f0af9f0ddabb6d8fbe8b78c493b3ff9da Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 22:49:23 +0800 Subject: [PATCH 6/9] fix: recongnizing vanilla as patched --- .../java/org/jackhuang/hmcl/game/HMCLGameRepository.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index c7bfcbe1a..166095bda 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -154,12 +154,11 @@ public class HMCLGameRepository extends DefaultGameRepository { if (id == null || !isLoaded()) return newImage("/assets/img/grass.png"); - Version version = getVersion(id); + Version version = getVersion(id).resolve(this); File iconFile = getVersionIconFile(id); if (iconFile.exists()) return new Image("file:" + iconFile.getAbsolutePath()); - else if (!version.getPatches().isEmpty() || - version.getMainClass() != null && + else if (version.getMainClass() != null && ("net.minecraft.launchwrapper.Launch".equals(version.getMainClass()) || version.getMainClass().startsWith("net.fabricmc") || "cpw.mods.modlauncher.Launcher".equals(version.getMainClass()))) From 1b466eb33ff31bedfaac6736be046ed37e16d53d Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Tue, 4 Feb 2020 22:49:49 +0800 Subject: [PATCH 7/9] fix: recognizing CancellationException as crash --- .../src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java index 4db7dcb26..4366c1f25 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java @@ -108,6 +108,8 @@ public final class TaskExecutor { throw new IllegalStateException("Cannot cancel a not started TaskExecutor"); } + Logging.LOG.log(Level.INFO, "Cancelling task " + firstTask); + cancelled.set(true); future.cancel(true); } @@ -132,9 +134,6 @@ public final class TaskExecutor { .thenApplyAsync(unused -> (Exception) null) .exceptionally(throwable -> { Throwable resolved = resolveException(throwable); - if (resolved instanceof CancellationException) { - throw (CancellationException)resolved; - } if (resolved instanceof Exception) { return (Exception) resolved; } else { From 04aa257b4837aa6eb5a6638d1010a033cc8ea9ae Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Wed, 5 Feb 2020 11:58:28 +0800 Subject: [PATCH 8/9] refactor: extract superclass of TaskExecutor --- .../java/org/jackhuang/hmcl/Launcher.java | 4 +- .../hmcl/task/AsyncTaskExecutor.java | 250 +++++++++++++++++ .../java/org/jackhuang/hmcl/task/Task.java | 6 +- .../org/jackhuang/hmcl/task/TaskExecutor.java | 262 +----------------- 4 files changed, 270 insertions(+), 252 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java index 4f2f070c9..2c5781ee9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java @@ -22,7 +22,7 @@ import javafx.application.Platform; import javafx.stage.Stage; import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.TaskExecutor; +import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.CrashReporter; @@ -78,7 +78,7 @@ public final class Launcher extends Application { public static void main(String[] args) { Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER); - TaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false)); + AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false)); try { LOG.info("*** " + Metadata.TITLE + " ***"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java new file mode 100644 index 000000000..49c2588d4 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/AsyncTaskExecutor.java @@ -0,0 +1,250 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2020 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.task; + +import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.Logging; +import org.jackhuang.hmcl.util.function.ExceptionalRunnable; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.*; +import java.util.logging.Level; + +/** + * + * @author huangyuhui + */ +public final class AsyncTaskExecutor extends TaskExecutor { + + private CompletableFuture future; + + public AsyncTaskExecutor(Task task) { + super(task); + } + + @Override + public TaskExecutor start() { + taskListeners.forEach(TaskListener::onStart); + future = executeTasks(Collections.singleton(firstTask)) + .thenApplyAsync(exception -> { + boolean success = exception == null; + + if (!success) { + // We log exception stacktrace because some of exceptions occurred because of bugs. + Logging.LOG.log(Level.WARNING, "An exception occurred in task execution", exception); + + Throwable resolvedException = resolveException(exception); + if (resolvedException instanceof RuntimeException && + !(resolvedException instanceof CancellationException) && + !(resolvedException instanceof JsonParseException)) { + // Track uncaught RuntimeException which are thrown mostly by our mistake + if (uncaughtExceptionHandler != null) + uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), resolvedException); + } + } + + taskListeners.forEach(it -> it.onStop(success, this)); + return success; + }) + .exceptionally(e -> { + Lang.handleUncaughtException(resolveException(e)); + return false; + }); + return this; + } + + @Override + public boolean test() { + start(); + try { + return future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException ignore) { + // We have dealt with ExecutionException in exception handling and uncaught exception handler. + } catch (CancellationException e) { + Logging.LOG.log(Level.INFO, "Task " + firstTask + " has been cancelled.", e); + } + return false; + } + + @Override + public synchronized void cancel() { + // AsyncTaskExecutor does not support cancellation. + } + + private CompletableFuture executeTasks(Collection> tasks) { + if (tasks == null || tasks.isEmpty()) + return CompletableFuture.completedFuture(null); + + return CompletableFuture.completedFuture(null) + .thenComposeAsync(unused -> { + totTask.addAndGet(tasks.size()); + + return CompletableFuture.allOf(tasks.stream() + .map(task -> CompletableFuture.completedFuture(null) + .thenComposeAsync(unused2 -> executeTask(task)) + ).toArray(CompletableFuture[]::new)); + }) + .thenApplyAsync(unused -> (Exception) null) + .exceptionally(throwable -> { + Throwable resolved = resolveException(throwable); + if (resolved instanceof Exception) { + return (Exception) resolved; + } else { + // If an error occurred, we just rethrow it. + throw new CompletionException(throwable); + } + }); + } + + private CompletableFuture executeTask(Task task) { + return CompletableFuture.completedFuture(null) + .thenComposeAsync(unused -> { + task.setCancelled(this::isCancelled); + task.setState(Task.TaskState.READY); + + if (task.getSignificance().shouldLog()) + Logging.LOG.log(Level.FINE, "Executing task: " + task.getName()); + + taskListeners.forEach(it -> it.onReady(task)); + + if (task.doPreExecute()) { + return CompletableFuture.runAsync(wrap(task::preExecute), task.getExecutor()); + } else { + return CompletableFuture.completedFuture(null); + } + }) + .thenComposeAsync(unused -> executeTasks(task.getDependents())) + .thenComposeAsync(dependentsException -> { + boolean isDependentsSucceeded = dependentsException == null; + + if (!isDependentsSucceeded && task.isRelyingOnDependents()) { + task.setException(dependentsException); + rethrow(dependentsException); + } + + if (isDependentsSucceeded) + task.setDependentsSucceeded(); + + return CompletableFuture.runAsync(wrap(() -> { + task.setState(Task.TaskState.RUNNING); + taskListeners.forEach(it -> it.onRunning(task)); + task.execute(); + }), task.getExecutor()).whenComplete((unused, throwable) -> { + task.setState(Task.TaskState.EXECUTED); + rethrow(throwable); + }); + }) + .thenComposeAsync(unused -> executeTasks(task.getDependencies())) + .thenComposeAsync(dependenciesException -> { + boolean isDependenciesSucceeded = dependenciesException == null; + + if (isDependenciesSucceeded) + task.setDependenciesSucceeded(); + + if (task.doPostExecute()) { + return CompletableFuture.runAsync(wrap(task::postExecute), task.getExecutor()) + .thenApply(unused -> dependenciesException); + } else { + return CompletableFuture.completedFuture(dependenciesException); + } + }) + .thenAcceptAsync(dependenciesException -> { + boolean isDependenciesSucceeded = dependenciesException == null; + + if (!isDependenciesSucceeded && task.isRelyingOnDependencies()) { + Logging.LOG.severe("Subtasks failed for " + task.getName()); + task.setException(dependenciesException); + rethrow(dependenciesException); + } + + if (task.getSignificance().shouldLog()) { + Logging.LOG.log(Level.FINER, "Task finished: " + task.getName()); + } + + task.onDone().fireEvent(new TaskEvent(this, task, false)); + taskListeners.forEach(it -> it.onFinished(task)); + + task.setState(Task.TaskState.SUCCEEDED); + }) + .exceptionally(throwable -> { + Throwable resolved = resolveException(throwable); + if (resolved instanceof Exception) { + Exception e = (Exception) resolved; + if (e instanceof InterruptedException || e instanceof CancellationException) { + task.setException(e); + if (task.getSignificance().shouldLog()) { + Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName()); + } + task.onDone().fireEvent(new TaskEvent(this, task, true)); + taskListeners.forEach(it -> it.onFailed(task, e)); + } else { + task.setException(e); + exception = e; + if (task.getSignificance().shouldLog()) { + Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e); + } + task.onDone().fireEvent(new TaskEvent(this, task, true)); + taskListeners.forEach(it -> it.onFailed(task, e)); + } + + task.setState(Task.TaskState.FAILED); + } + + throw new CompletionException(resolved); // rethrow error + }); + } + + private static Throwable resolveException(Throwable e) { + if (e instanceof ExecutionException || e instanceof CompletionException) + return resolveException(e.getCause()); + else + return e; + } + + private static void rethrow(Throwable e) { + if (e == null) + return; + if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable + rethrow(e.getCause()); + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new CompletionException(e); + } + } + + private static Runnable wrap(ExceptionalRunnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Exception e) { + rethrow(e); + } + }; + } + + private static Thread.UncaughtExceptionHandler uncaughtExceptionHandler = null; + + public static void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { + AsyncTaskExecutor.uncaughtExceptionHandler = uncaughtExceptionHandler; + } +} 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 17e8d5f29..432b7ad50 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -327,18 +327,18 @@ public abstract class Task { } public final TaskExecutor executor() { - return new TaskExecutor(this); + return new AsyncTaskExecutor(this); } public final TaskExecutor executor(boolean start) { - TaskExecutor executor = new TaskExecutor(this); + TaskExecutor executor = new AsyncTaskExecutor(this); if (start) executor.start(); return executor; } public final TaskExecutor executor(TaskListener taskListener) { - TaskExecutor executor = new TaskExecutor(this); + TaskExecutor executor = new AsyncTaskExecutor(this); executor.addTaskListener(taskListener); return executor; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java index 4366c1f25..8489ac35c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/TaskExecutor.java @@ -1,48 +1,18 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 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 - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ package org.jackhuang.hmcl.task; -import com.google.gson.JsonParseException; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.Logging; -import org.jackhuang.hmcl.util.function.ExceptionalRunnable; +import org.jetbrains.annotations.Nullable; -import java.util.Collection; -import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -/** - * - * @author huangyuhui - */ -public final class TaskExecutor { - - private final Task firstTask; - private final List taskListeners = new LinkedList<>(); - private Exception exception; - private final AtomicInteger totTask = new AtomicInteger(0); - private CompletableFuture future; - private final AtomicBoolean cancelled = new AtomicBoolean(false); +public abstract class TaskExecutor { + protected final Task firstTask; + protected final List taskListeners = new LinkedList<>(); + protected final AtomicInteger totTask = new AtomicInteger(0); + protected final AtomicBoolean cancelled = new AtomicBoolean(false); + protected Exception exception; public TaskExecutor(Task task) { this.firstTask = task; @@ -52,231 +22,29 @@ public final class TaskExecutor { taskListeners.add(taskListener); } + /** + * Reason why the task execution failed. + * If cancelled, null is returned. + */ + @Nullable public Exception getException() { return exception; } - public TaskExecutor start() { - taskListeners.forEach(TaskListener::onStart); - future = executeTasks(Collections.singleton(firstTask)) - .thenApplyAsync(exception -> { - boolean success = exception == null; + public abstract TaskExecutor start(); - if (!success) { - // We log exception stacktrace because some of exceptions occurred because of bugs. - Logging.LOG.log(Level.WARNING, "An exception occurred in task execution", exception); - - Throwable resolvedException = resolveException(exception); - if (resolvedException instanceof RuntimeException && - !(resolvedException instanceof CancellationException) && - !(resolvedException instanceof JsonParseException)) { - // Track uncaught RuntimeException which are thrown mostly by our mistake - if (uncaughtExceptionHandler != null) - uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), resolvedException); - } - } - - taskListeners.forEach(it -> it.onStop(success, this)); - return success; - }) - .exceptionally(e -> { - Lang.handleUncaughtException(resolveException(e)); - return false; - }); - return this; - } - - public boolean test() { - start(); - try { - return future.get(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } catch (ExecutionException ignore) { - // We have dealt with ExecutionException in exception handling and uncaught exception handler. - } catch (CancellationException e) { - Logging.LOG.log(Level.INFO, "Task " + firstTask + " has been cancelled.", e); - } - return false; - } + public abstract boolean test(); /** * Cancel the subscription ant interrupt all tasks. */ - public synchronized void cancel() { - if (future == null) { - throw new IllegalStateException("Cannot cancel a not started TaskExecutor"); - } - - Logging.LOG.log(Level.INFO, "Cancelling task " + firstTask); - - cancelled.set(true); - future.cancel(true); - } + public abstract void cancel(); public boolean isCancelled() { return cancelled.get(); } - private CompletableFuture executeTasks(Collection> tasks) { - if (tasks == null || tasks.isEmpty()) - return CompletableFuture.completedFuture(null); - - return CompletableFuture.completedFuture(null) - .thenComposeAsync(unused -> { - totTask.addAndGet(tasks.size()); - - return CompletableFuture.allOf(tasks.stream() - .map(task -> CompletableFuture.completedFuture(null) - .thenComposeAsync(unused2 -> executeTask(task)) - ).toArray(CompletableFuture[]::new)); - }) - .thenApplyAsync(unused -> (Exception) null) - .exceptionally(throwable -> { - Throwable resolved = resolveException(throwable); - if (resolved instanceof Exception) { - return (Exception) resolved; - } else { - // If an error occurred, we just rethrow it. - throw new CompletionException(throwable); - } - }); - } - - private CompletableFuture executeTask(Task task) { - return CompletableFuture.completedFuture(null) - .thenComposeAsync(unused -> { - task.setCancelled(this::isCancelled); - task.setState(Task.TaskState.READY); - - if (task.getSignificance().shouldLog()) - Logging.LOG.log(Level.FINE, "Executing task: " + task.getName()); - - taskListeners.forEach(it -> it.onReady(task)); - - if (task.doPreExecute()) { - return CompletableFuture.runAsync(wrap(task::preExecute), task.getExecutor()); - } else { - return CompletableFuture.completedFuture(null); - } - }) - .thenComposeAsync(unused -> executeTasks(task.getDependents())) - .thenComposeAsync(dependentsException -> { - boolean isDependentsSucceeded = dependentsException == null; - - if (!isDependentsSucceeded && task.isRelyingOnDependents()) { - task.setException(dependentsException); - rethrow(dependentsException); - } - - if (isDependentsSucceeded) - task.setDependentsSucceeded(); - - return CompletableFuture.runAsync(wrap(() -> { - task.setState(Task.TaskState.RUNNING); - taskListeners.forEach(it -> it.onRunning(task)); - task.execute(); - }), task.getExecutor()).whenComplete((unused, throwable) -> { - task.setState(Task.TaskState.EXECUTED); - rethrow(throwable); - }); - }) - .thenComposeAsync(unused -> executeTasks(task.getDependencies())) - .thenComposeAsync(dependenciesException -> { - boolean isDependenciesSucceeded = dependenciesException == null; - - if (isDependenciesSucceeded) - task.setDependenciesSucceeded(); - - if (task.doPostExecute()) { - return CompletableFuture.runAsync(wrap(task::postExecute), task.getExecutor()) - .thenApply(unused -> dependenciesException); - } else { - return CompletableFuture.completedFuture(dependenciesException); - } - }) - .thenAcceptAsync(dependenciesException -> { - boolean isDependenciesSucceeded = dependenciesException == null; - - if (!isDependenciesSucceeded && task.isRelyingOnDependencies()) { - Logging.LOG.severe("Subtasks failed for " + task.getName()); - task.setException(dependenciesException); - rethrow(dependenciesException); - } - - if (task.getSignificance().shouldLog()) { - Logging.LOG.log(Level.FINER, "Task finished: " + task.getName()); - } - - task.onDone().fireEvent(new TaskEvent(this, task, false)); - taskListeners.forEach(it -> it.onFinished(task)); - - task.setState(Task.TaskState.SUCCEEDED); - }) - .exceptionally(throwable -> { - Throwable resolved = resolveException(throwable); - if (resolved instanceof Exception) { - Exception e = (Exception) resolved; - if (e instanceof InterruptedException || e instanceof CancellationException) { - task.setException(e); - if (task.getSignificance().shouldLog()) { - Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName()); - } - task.onDone().fireEvent(new TaskEvent(this, task, true)); - taskListeners.forEach(it -> it.onFailed(task, e)); - } else { - task.setException(e); - exception = e; - if (task.getSignificance().shouldLog()) { - Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e); - } - task.onDone().fireEvent(new TaskEvent(this, task, true)); - taskListeners.forEach(it -> it.onFailed(task, e)); - } - - task.setState(Task.TaskState.FAILED); - } - - throw new CompletionException(resolved); // rethrow error - }); - } - public int getRunningTasks() { return totTask.get(); } - - private static Throwable resolveException(Throwable e) { - if (e instanceof ExecutionException || e instanceof CompletionException) - return resolveException(e.getCause()); - else - return e; - } - - private static void rethrow(Throwable e) { - if (e == null) - return; - if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable - rethrow(e.getCause()); - } else if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } else { - throw new CompletionException(e); - } - } - - private static Runnable wrap(ExceptionalRunnable runnable) { - return () -> { - try { - runnable.run(); - } catch (Exception e) { - rethrow(e); - } - }; - } - - private static Thread.UncaughtExceptionHandler uncaughtExceptionHandler = null; - - public static void setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler uncaughtExceptionHandler) { - TaskExecutor.uncaughtExceptionHandler = uncaughtExceptionHandler; - } } From b864ea89d7ad7fac1f568550340177fecd9327f1 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Wed, 5 Feb 2020 11:58:50 +0800 Subject: [PATCH 9/9] add: restore old sync task executor --- .../jackhuang/hmcl/game/LauncherHelper.java | 2 +- .../TaskExecutorDialogWizardDisplayer.java | 2 +- .../hmcl/task/CancellableTaskExecutor.java | 254 ++++++++++++++++++ .../org/jackhuang/hmcl/task/Schedulers.java | 70 ++++- .../java/org/jackhuang/hmcl/task/Task.java | 17 ++ 5 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/task/CancellableTaskExecutor.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index b8083d003..52a1ff581 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -213,7 +213,7 @@ public final class LauncherHelper { }); } }) - .executor(); + .cancellableExecutor(); launchingStepsPane.setExecutor(executor, false); executor.addTaskListener(new TaskListener() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java index 6e6797405..53ddab7a8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/TaskExecutorDialogWizardDisplayer.java @@ -60,7 +60,7 @@ public interface TaskExecutorDialogWizardDisplayer extends AbstractWizardDisplay } runInFX(() -> { - TaskExecutor executor = task.executor(new TaskListener() { + TaskExecutor executor = task.cancellableExecutor(new TaskListener() { @Override public void onStop(boolean success, TaskExecutor executor) { runInFX(() -> { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CancellableTaskExecutor.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CancellableTaskExecutor.java new file mode 100644 index 000000000..d47dac599 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/CancellableTaskExecutor.java @@ -0,0 +1,254 @@ +package org.jackhuang.hmcl.task; + +import org.jackhuang.hmcl.util.Logging; +import org.jackhuang.hmcl.util.function.ExceptionalRunnable; + +import java.util.Collection; +import java.util.Collections; +import java.util.Objects; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +public class CancellableTaskExecutor extends TaskExecutor { + + private final ConcurrentLinkedQueue> workerQueue = new ConcurrentLinkedQueue<>(); + private Executor scheduler = Schedulers.newThread(); + + public CancellableTaskExecutor(Task task) { + super(task); + } + + @Override + public TaskExecutor start() { + taskListeners.forEach(TaskListener::onStart); + workerQueue.add(Schedulers.schedule(scheduler, wrap(() -> { + boolean flag = executeTasks(Collections.singleton(firstTask)); + taskListeners.forEach(it -> it.onStop(flag, this)); + }))); + return this; + } + + @Override + public boolean test() { + taskListeners.forEach(TaskListener::onStart); + AtomicBoolean flag = new AtomicBoolean(true); + Future future = Schedulers.schedule(scheduler, wrap(() -> { + flag.set(executeTasks(Collections.singleton(firstTask))); + taskListeners.forEach(it -> it.onStop(flag.get(), this)); + })); + workerQueue.add(future); + try { + future.get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException | CancellationException ignored) { + } + return flag.get(); + } + + @Override + public synchronized void cancel() { + cancelled.set(true); + + while (!workerQueue.isEmpty()) { + Future future = workerQueue.poll(); + if (future != null) + future.cancel(true); + } + } + + private boolean executeTasks(Collection> tasks) throws InterruptedException { + if (tasks.isEmpty()) + return true; + + totTask.addAndGet(tasks.size()); + AtomicBoolean success = new AtomicBoolean(true); + CountDownLatch latch = new CountDownLatch(tasks.size()); + for (Task task : tasks) { + if (cancelled.get()) + return false; + Invoker invoker = new Invoker(task, latch, success); + try { + Future future = Schedulers.schedule(scheduler, invoker); + workerQueue.add(future); + } catch (RejectedExecutionException e) { + throw new InterruptedException(); + } + } + + if (cancelled.get()) + return false; + + try { + latch.await(); + } catch (InterruptedException e) { + return false; + } + return success.get() && !cancelled.get(); + } + + private boolean executeTask(Task task) { + task.setCancelled(this::isCancelled); + + if (cancelled.get()) { + task.setState(Task.TaskState.FAILED); + task.setException(new CancellationException()); + return false; + } + + task.setState(Task.TaskState.READY); + + if (task.getSignificance().shouldLog()) + Logging.LOG.log(Level.FINE, "Executing task: " + task.getName()); + + taskListeners.forEach(it -> it.onReady(task)); + + boolean flag = false; + + try { + if (task.doPreExecute()) { + try { + Schedulers.schedule(task.getExecutor(), wrap(task::preExecute)).get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof Exception) + throw (Exception) e.getCause(); + else + throw e; + } + } + + Collection> dependents = task.getDependents(); + boolean doDependentsSucceeded = executeTasks(dependents); + Exception dependentsException = dependents.stream().map(Task::getException).filter(Objects::nonNull).findAny().orElse(null); + if (!doDependentsSucceeded && task.isRelyingOnDependents() || cancelled.get()) { + task.setException(dependentsException); + throw new CancellationException(); + } + + if (doDependentsSucceeded) + task.setDependentsSucceeded(); + + try { + Schedulers.schedule(task.getExecutor(), wrap(() -> { + task.setState(Task.TaskState.RUNNING); + taskListeners.forEach(it -> it.onRunning(task)); + task.execute(); + })).get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof Exception) + throw (Exception) e.getCause(); + else + throw e; + } finally { + task.setState(Task.TaskState.EXECUTED); + } + + Collection> dependencies = task.getDependencies(); + boolean doDependenciesSucceeded = executeTasks(dependencies); + Exception dependenciesException = dependencies.stream().map(Task::getException).filter(Objects::nonNull).findAny().orElse(null); + + if (doDependenciesSucceeded) + task.setDependenciesSucceeded(); + + if (task.doPostExecute()) { + try { + Schedulers.schedule(task.getExecutor(), wrap(task::postExecute)).get(); + } catch (ExecutionException e) { + if (e.getCause() instanceof Exception) + throw (Exception) e.getCause(); + else + throw e; + } + } + + if (!doDependenciesSucceeded && task.isRelyingOnDependencies()) { + Logging.LOG.severe("Subtasks failed for " + task.getName()); + task.setException(dependenciesException); + throw new CancellationException(); + } + + flag = true; + if (task.getSignificance().shouldLog()) { + Logging.LOG.log(Level.FINER, "Task finished: " + task.getName()); + } + + task.onDone().fireEvent(new TaskEvent(this, task, false)); + taskListeners.forEach(it -> it.onFinished(task)); + } catch (InterruptedException e) { + task.setException(e); + if (task.getSignificance().shouldLog()) { + Logging.LOG.log(Level.FINE, "Task aborted: " + task.getName()); + } + task.onDone().fireEvent(new TaskEvent(this, task, true)); + taskListeners.forEach(it -> it.onFailed(task, e)); + } catch (CancellationException | RejectedExecutionException e) { + if (task.getException() == null) + task.setException(e); + } catch (Exception e) { + task.setException(e); + exception = e; + if (task.getSignificance().shouldLog()) { + Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e); + } + task.onDone().fireEvent(new TaskEvent(this, task, true)); + taskListeners.forEach(it -> it.onFailed(task, e)); + } + task.setState(flag ? Task.TaskState.SUCCEEDED : Task.TaskState.FAILED); + return flag; + } + + private static void rethrow(Throwable e) { + if (e == null) + return; + if (e instanceof ExecutionException || e instanceof CompletionException) { // including UncheckedException and UncheckedThrowable + rethrow(e.getCause()); + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new CompletionException(e); + } + } + + private static Runnable wrap(ExceptionalRunnable runnable) { + return () -> { + try { + runnable.run(); + } catch (Exception e) { + rethrow(e); + } + }; + } + + private class Invoker implements Runnable { + + private final Task task; + private final CountDownLatch latch; + private final AtomicBoolean success; + + public Invoker(Task task, CountDownLatch latch, AtomicBoolean success) { + this.task = task; + this.latch = latch; + this.success = success; + } + + @Override + public void run() { + try { + Thread.currentThread().setName(task.getName()); + if (!executeTask(task)) + success.set(false); + } finally { + latch.countDown(); + } + } + + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java index 4fd3317e7..d72214c30 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Schedulers.java @@ -19,9 +19,20 @@ package org.jackhuang.hmcl.task; import javafx.application.Platform; import org.jackhuang.hmcl.util.Logging; +import org.jetbrains.annotations.NotNull; import javax.swing.*; -import java.util.concurrent.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; /** * @@ -92,4 +103,61 @@ public final class Schedulers { if (SINGLE_EXECUTOR != null) SINGLE_EXECUTOR.shutdownNow(); } + + public static Future schedule(Executor executor, Runnable command) { + if (executor instanceof ExecutorService) { + return ((ExecutorService) executor).submit(command); + } + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference wrapper = new AtomicReference<>(); + + executor.execute(() -> { + try { + command.run(); + } catch (Exception e) { + wrapper.set(e); + } finally { + latch.countDown(); + } + Thread.interrupted(); // clear the `interrupted` flag to prevent from interrupting EventDispatch thread. + }); + + return new Future() { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return latch.getCount() == 0; + } + + private Void getImpl() throws ExecutionException { + Exception e = wrapper.get(); + if (e != null) + throw new ExecutionException(e); + return null; + } + + @Override + public Void get() throws InterruptedException, ExecutionException { + latch.await(); + return getImpl(); + } + + @Override + public Void get(long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + if (!latch.await(timeout, unit)) + throw new TimeoutException(); + return getImpl(); + } + }; + } } 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 432b7ad50..ad9e5e8d7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/task/Task.java @@ -343,6 +343,23 @@ public abstract class Task { return executor; } + public final TaskExecutor cancellableExecutor() { + return new CancellableTaskExecutor(this); + } + + public final TaskExecutor cancellableExecutor(boolean start) { + TaskExecutor executor = new CancellableTaskExecutor(this); + if (start) + executor.start(); + return executor; + } + + public final TaskExecutor cancellableExecutor(TaskListener taskListener) { + TaskExecutor executor = new CancellableTaskExecutor(this); + executor.addTaskListener(taskListener); + return executor; + } + public final void start() { executor().start(); }