Merge branch 'javafx' into patch-1

This commit is contained in:
Yuhui Huang 2020-02-05 12:40:33 +08:00 committed by GitHub
commit e67a1d16f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 691 additions and 298 deletions

View File

@ -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 + " ***");

View File

@ -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())))

View File

@ -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;
@ -55,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;
@ -64,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;
@ -203,7 +213,7 @@ public final class LauncherHelper {
});
}
})
.executor();
.cancellableExecutor();
launchingStepsPane.setExecutor(executor, false);
executor.addTaskListener(new TaskListener() {
@ -239,9 +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);
}

View File

@ -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);

View File

@ -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(() -> {

View File

@ -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/)

View File

@ -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/)

View File

@ -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/)

View File

@ -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/)

View File

@ -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/)

View File

@ -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;
}
}

View File

@ -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<Void> {
private final File assetIndexFile;
private final List<Task<?>> dependents = new LinkedList<>();
private final List<Task<?>> dependencies = new LinkedList<>();
private AssetIndex index;
private boolean retry = false;
/**
* Constructor.
@ -62,8 +61,15 @@ public final class GameAssetDownloadTask extends Task<Void> {
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<Void> {
}
@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;

View File

@ -217,9 +217,12 @@ public class Version implements Comparable<Version>, 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();
}

View File

@ -0,0 +1,250 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <https://www.gnu.org/licenses/>.
*/
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<Boolean> 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<Exception> executeTasks(Collection<Task<?>> 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;
}
}

View File

@ -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<Future<?>> 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<? extends Task<?>> 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<? extends Task<?>> 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<? extends Task<?>> 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();
}
}
}
}

View File

@ -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<Exception> 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<Void>() {
@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();
}
};
}
}

View File

@ -327,18 +327,35 @@ public abstract class Task<T> {
}
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;
}
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;
}

View File

@ -1,48 +1,18 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 <https://www.gnu.org/licenses/>.
*/
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<TaskListener> taskListeners = new LinkedList<>();
private Exception exception;
private final AtomicInteger totTask = new AtomicInteger(0);
private CompletableFuture<Boolean> future;
private final AtomicBoolean cancelled = new AtomicBoolean(false);
public abstract class TaskExecutor {
protected final Task<?> firstTask;
protected final List<TaskListener> 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,232 +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");
}
cancelled.set(true);
future.cancel(true);
}
public abstract void cancel();
public boolean isCancelled() {
return cancelled.get();
}
private CompletableFuture<Exception> executeTasks(Collection<Task<?>> 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 CancellationException) {
throw (CancellationException)resolved;
}
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;
}
}