mirror of
https://github.com/HMCL-dev/HMCL
synced 2026-08-19 22:23:08 -04:00
修复异步任务 OOM 后界面假死并显示错误信息 (#6636)
This commit is contained in:
parent
78b8d37891
commit
a0d1fa8f38
3 changed files with 97 additions and 2 deletions
|
|
@ -68,14 +68,25 @@ public abstract class TaskExecutorDialogWizardDisplayer extends AbstractWizardDi
|
|||
else if (!settings.containsKey("forbid_success_message"))
|
||||
Controllers.dialog(i18n("message.success"), null, MessageType.SUCCESS, () -> onEnd());
|
||||
} else {
|
||||
if (executor.getException() == null)
|
||||
if (executor.getException() == null) {
|
||||
onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (executor.getException() instanceof CancellationException) {
|
||||
onEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
if (executor.getException().getCause() instanceof OutOfMemoryError outOfMemoryError) {
|
||||
try {
|
||||
Controllers.dialog(StringUtils.getStackTrace(outOfMemoryError), null, MessageType.ERROR, () -> onEnd());
|
||||
} catch (OutOfMemoryError ignored) {
|
||||
onEnd();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String appendix = StringUtils.getStackTrace(executor.getException());
|
||||
if (settings.get(WizardProvider.FailureCallback.KEY) != null)
|
||||
settings.get(WizardProvider.FailureCallback.KEY).onFail(settings, executor.getException(), () -> onEnd());
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||
return success;
|
||||
})
|
||||
.exceptionally(e -> {
|
||||
Lang.handleUncaughtException(resolveException(e));
|
||||
Throwable resolved = resolveException(e);
|
||||
if (resolved instanceof OutOfMemoryError)
|
||||
taskListeners.forEach(it -> it.onStop(false, this));
|
||||
Lang.handleUncaughtException(resolved);
|
||||
return false;
|
||||
});
|
||||
return this;
|
||||
|
|
@ -190,6 +193,8 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||
}
|
||||
|
||||
task.setState(Task.TaskState.FAILED);
|
||||
} else if (resolved instanceof OutOfMemoryError e) {
|
||||
handleOutOfMemoryError(task, e);
|
||||
}
|
||||
|
||||
throw new CompletionException(resolved); // rethrow error
|
||||
|
|
@ -301,6 +306,8 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||
taskListeners.forEach(it -> it.onFailed(task, e));
|
||||
|
||||
task.setState(Task.TaskState.FAILED);
|
||||
} else if (resolved instanceof OutOfMemoryError e) {
|
||||
handleOutOfMemoryError(task, e);
|
||||
}
|
||||
|
||||
throw new CompletionException(resolved); // rethrow error
|
||||
|
|
@ -315,6 +322,16 @@ public final class AsyncTaskExecutor extends TaskExecutor {
|
|||
}
|
||||
}
|
||||
|
||||
/// Completes the failed task lifecycle while preserving the original error for the global handler.
|
||||
private void handleOutOfMemoryError(Task<?> task, OutOfMemoryError error) {
|
||||
Exception taskException = new Exception(error);
|
||||
task.setException(taskException);
|
||||
exception = taskException;
|
||||
task.fireDoneEvent(this, true);
|
||||
taskListeners.forEach(it -> it.onFailed(task, error));
|
||||
task.setState(Task.TaskState.FAILED);
|
||||
}
|
||||
|
||||
private void checkCancellation() {
|
||||
if (isCancelled()) {
|
||||
throw new CancellationException("Cancelled by user");
|
||||
|
|
|
|||
|
|
@ -20,12 +20,15 @@ package org.jackhuang.hmcl.util;
|
|||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.task.TaskListener;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.EnabledIf;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
|
@ -47,6 +50,70 @@ public class TaskTest {
|
|||
assertInstanceOf(Error.class, throwable.get(), "Error has not been thrown to uncaught exception handler");
|
||||
}
|
||||
|
||||
/// Verifies that OOM completes the task lifecycle and still reaches the global handler.
|
||||
@Test
|
||||
public void testOutOfMemoryErrorLifecycle() {
|
||||
OutOfMemoryError normalTaskError = new OutOfMemoryError("normal task");
|
||||
assertOutOfMemoryErrorLifecycle(Task.runAsync(() -> {
|
||||
throw normalTaskError;
|
||||
}), normalTaskError);
|
||||
|
||||
OutOfMemoryError futureTaskError = new OutOfMemoryError("completable future task");
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
future.completeExceptionally(futureTaskError);
|
||||
assertOutOfMemoryErrorLifecycle(Task.fromCompletableFuture(future), futureTaskError);
|
||||
}
|
||||
|
||||
/// Checks the lifecycle events and recorded exceptions for one OOM task.
|
||||
private void assertOutOfMemoryErrorLifecycle(Task<?> task, OutOfMemoryError error) {
|
||||
AtomicInteger doneCount = new AtomicInteger();
|
||||
AtomicReference<@Nullable Boolean> doneFailed = new AtomicReference<>();
|
||||
AtomicInteger failedCount = new AtomicInteger();
|
||||
AtomicReference<@Nullable Throwable> failedThrowable = new AtomicReference<>();
|
||||
AtomicInteger stopCount = new AtomicInteger();
|
||||
AtomicReference<@Nullable Boolean> stopSuccess = new AtomicReference<>();
|
||||
AtomicReference<@Nullable Throwable> uncaught = new AtomicReference<>();
|
||||
|
||||
task.onDone().register(event -> {
|
||||
doneCount.incrementAndGet();
|
||||
doneFailed.set(event.isFailed());
|
||||
});
|
||||
TaskExecutor executor = task.executor(new TaskListener() {
|
||||
@Override
|
||||
public void onFailed(Task<?> failedTask, Throwable throwable) {
|
||||
failedCount.incrementAndGet();
|
||||
failedThrowable.set(throwable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor taskExecutor) {
|
||||
stopCount.incrementAndGet();
|
||||
stopSuccess.set(success);
|
||||
}
|
||||
});
|
||||
|
||||
@Nullable Thread.UncaughtExceptionHandler previousHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
try {
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> uncaught.set(throwable));
|
||||
assertFalse(executor.test());
|
||||
} finally {
|
||||
Thread.setDefaultUncaughtExceptionHandler(previousHandler);
|
||||
}
|
||||
|
||||
assertEquals(1, doneCount.get());
|
||||
assertEquals(Boolean.TRUE, doneFailed.get());
|
||||
assertEquals(1, failedCount.get());
|
||||
assertSame(error, failedThrowable.get());
|
||||
assertEquals(1, stopCount.get());
|
||||
assertEquals(Boolean.FALSE, stopSuccess.get());
|
||||
assertEquals(Task.TaskState.FAILED, task.getState());
|
||||
assertNotNull(task.getException());
|
||||
assertSame(error, task.getException().getCause());
|
||||
assertNotNull(executor.getException());
|
||||
assertSame(error, executor.getException().getCause());
|
||||
assertSame(error, uncaught.get());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue