diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt index 13b56d6b4..3aacca13c 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/game/HMCLModpack.kt @@ -50,7 +50,7 @@ fun readHMCLModpackManifest(f: File): Modpack { object HMCLModpackManifest -class HMCLModpackInstallTask(profile: Profile, private val zipFile: File, private val modpack: Modpack, private val name: String): Task() { +class HMCLModpackInstallTask(profile: Profile, private val zipFile: File, modpack: Modpack, private val name: String): Task() { private val dependency = profile.dependency private val repository = profile.repository override val dependencies = mutableListOf() @@ -63,6 +63,8 @@ class HMCLModpackInstallTask(profile: Profile, private val zipFile: File, privat version = version.copy(jar = null) dependents += dependency.gameBuilder().name(name).gameVersion(modpack.gameVersion!!).buildAsync() dependencies += VersionJSONSaveTask(repository, version) // override the json created by buildAsync() + + onDone += { event -> if (event.failed) repository.removeVersionFromDisk(name) } } private val run = repository.getRunDirectory(name) @@ -119,6 +121,11 @@ class HMCLModpackExportTask @JvmOverloads constructor( private val modpack: Modpack, private val output: File, override val id: String = ID): TaskResult() { + + init { + onDone += { event -> if (event.failed) output.delete() } + } + override fun execute() { val blackList = ArrayList(MODPACK_BLACK_LIST) blackList.add(version + ".jar") diff --git a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/AbstractWizardDisplayer.kt b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/AbstractWizardDisplayer.kt index 8ed42732f..79731c39f 100644 --- a/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/AbstractWizardDisplayer.kt +++ b/HMCL/src/main/kotlin/org/jackhuang/hmcl/ui/wizard/AbstractWizardDisplayer.kt @@ -94,26 +94,28 @@ interface AbstractWizardDisplayer : WizardDisplayer { navigateTo(StackPane().apply { children += vbox }, Navigation.NavigationDirection.FINISH) - task.executor().let { executor -> + task.with(org.jackhuang.hmcl.task.task(Scheduler.JAVAFX) { + navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH) + }).executor().apply { @Suppress("NAME_SHADOWING") - executor.taskListener = object : TaskListener { + taskListener = object : TaskListener { override fun onReady(task: Task) { - Platform.runLater { tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get()) } + Platform.runLater { tasksBar.progressProperty().set(finishedTasks * 1.0 / totTask.get()) } } override fun onFinished(task: Task) { Platform.runLater { label.text = task.title ++finishedTasks - tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get()) + tasksBar.progressProperty().set(finishedTasks * 1.0 / totTask.get()) } } - override fun onFailed(task: Task) { + override fun onFailed(task: Task, throwable: Throwable) { Platform.runLater { label.text = task.title ++finishedTasks - tasksBar.progressProperty().set(finishedTasks * 1.0 / executor.totTask.get()) + tasksBar.progressProperty().set(finishedTasks * 1.0 / totTask.get()) } } @@ -123,11 +125,7 @@ interface AbstractWizardDisplayer : WizardDisplayer { } - cancelQueue.add(executor) - - executor.submit(org.jackhuang.hmcl.task.task(Scheduler.JAVAFX) { - navigateTo(Label("Successful"), Navigation.NavigationDirection.FINISH) - }) + cancelQueue.add(this) }.start() } diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/DefaultGameBuilder.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/DefaultGameBuilder.kt index 722272e8b..6f59c3d8c 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/DefaultGameBuilder.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/DefaultGameBuilder.kt @@ -45,7 +45,7 @@ class DefaultGameBuilder(val dependencyManager: DefaultDependencyManager): GameB GameLoggingDownloadTask(dependencyManager, version), GameDownloadTask(version), GameLibrariesTask(dependencyManager, version) // Game libraries will be downloaded for multiple times partly, this time is for vanilla libraries. - ) then VersionJSONSaveTask(dependencyManager.repository, version) + ) with VersionJSONSaveTask(dependencyManager.repository, version) // using [with] because download failure here are tolerant. if (toolVersions.containsKey("forge")) result = result then libraryTaskHelper(gameVersion, "forge") diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/forge/ForgeInstallTask.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/forge/ForgeInstallTask.kt index e7eddf766..07b923954 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/forge/ForgeInstallTask.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/forge/ForgeInstallTask.kt @@ -43,6 +43,8 @@ class ForgeInstallTask(private val dependencyManager: DefaultDependencyManager, override val dependencies = mutableListOf() override val id = "version" + override val reliesOnDependencies = false + init { if (!forgeVersionList.loaded) dependents += forgeVersionList.refreshAsync(dependencyManager.downloadProvider).then { diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.kt index 62f7f1285..927d3f634 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.kt @@ -42,6 +42,8 @@ class OptiFineInstallTask(private val dependencyManager: DefaultDependencyManage override val dependencies = mutableListOf() override val id = "version" + override val reliesOnDependencies = false + init { if (!optiFineVersionList.loaded) dependents += optiFineVersionList.refreshAsync(dependencyManager.downloadProvider) then { diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/MultiMCModpack.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/MultiMCModpack.kt index 780e7b1cf..d767ac521 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/MultiMCModpack.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/mod/MultiMCModpack.kt @@ -213,6 +213,8 @@ class MMCModpackInstallTask(private val dependencyManager: DefaultDependencyMana init { check(!repository.hasVersion(name), { "Version $name already exists." }) dependents += dependencyManager.gameBuilder().name(name).gameVersion(manifest.gameVersion).buildAsync() + + onDone += { event -> if (event.failed) repository.removeVersionFromDisk(name) } } private val run = repository.getRunDirectory(name) diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/CoupleTask.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/CoupleTask.kt index 2e9c2d5a0..9afc30960 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/CoupleTask.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/CoupleTask.kt @@ -26,7 +26,7 @@ import org.jackhuang.hmcl.util.AutoTypingMap * @param succ a callback that returns the task runs after [pred], [succ] will be executed asynchronously. You can do something that relies on the result of [pred]. * @param reliant true if this task chain will be broken when task [pred] fails. */ -internal class CoupleTask(pred: P, private val succ: (AutoTypingMap) -> Task?, override val reliant: Boolean) : Task() { +internal class CoupleTask(pred: P, private val succ: (AutoTypingMap) -> Task?, override val reliesOnDependents: Boolean) : Task() { override val hidden: Boolean = true override val dependents: Collection = listOf(pred) diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/ParallelTask.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/ParallelTask.kt index 57237df4d..ac4f4a05e 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/ParallelTask.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/ParallelTask.kt @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.task /** * The tasks that provides a way to execute tasks parallelly. + * Fails when some of [tasks] failed. * * @param tasks the tasks that can be executed parallelly. */ diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt index a0faeb228..7fd0e6983 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/Task.kt @@ -44,11 +44,20 @@ abstract class Task { open val scheduler: Scheduler = Scheduler.DEFAULT /** - * True if requires all dependent tasks finishing successfully. + * True if requires all [dependents] finishing successfully. * * **Note** if this field is set false, you are not supposed to invoke [run] + * @defaultValue true */ - open val reliant: Boolean = true + open val reliesOnDependents: Boolean = true + + /** + * True if requires all [dependencies] finishing successfully. + * + * **Note** if this field is set false, you are not supposed to invoke [run] + * @defaultValue false + */ + open val reliesOnDependencies: Boolean = true open var title: String = this.javaClass.toString() @@ -102,7 +111,7 @@ abstract class Task { val onDone = EventManager() /** - * **Note** reliant does not work here, which is always treated as true here. + * **Note** [reliesOnDependents] and [reliesOnDependencies] does not work here, which is always treated as true here. */ @Throws(Exception::class) fun run() { @@ -120,12 +129,10 @@ abstract class Task { this.progressPropertyImpl.unbind() } - fun executor() = TaskExecutor().submit(this) - fun executor(taskListener: TaskListener) = TaskExecutor().submit(this).apply { this.taskListener = taskListener } + fun executor() = TaskExecutor(this) + fun executor(taskListener: TaskListener) = TaskExecutor(this).apply { this.taskListener = taskListener } fun start() = executor().start() - fun subscribe(subscriber: Task) = executor().apply { - submit(subscriber).start() - } + fun subscribe(subscriber: Task) = TaskExecutor(with(subscriber)).apply { start() } fun subscribe(scheduler: Scheduler = Scheduler.DEFAULT, closure: (AutoTypingMap) -> Unit) = subscribe(task(scheduler, closure)) diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt index b3ce3b8a2..7285d0a5e 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskExecutor.kt @@ -27,46 +27,22 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicInteger import java.util.logging.Level -class TaskExecutor() { +class TaskExecutor(private val task: Task) { var taskListener: TaskListener? = null var canceled = false private set val totTask = AtomicInteger(0) val variables = AutoTypingMap(mutableMapOf()) - private val taskQueue = ConcurrentLinkedQueue() + var lastException: Exception? = null private val workerQueue = ConcurrentLinkedQueue>() - /** - * Submit a task to subscription to run. - * You can submit a task even when started this subscription. - * Thread-safe function. - */ - fun submit(task: Task): TaskExecutor { - taskQueue.add(task) - return this - } - /** * Start the subscription and run all registered tasks asynchronously. */ fun start() { workerQueue.add(Scheduler.NEW_THREAD.schedule { - totTask.addAndGet(taskQueue.size) - while (!taskQueue.isEmpty()) { - if (canceled) break - val task = taskQueue.poll() - if (task != null) { - val future = task.scheduler.schedule { executeTask(task) } - try { - future?.get() - } catch (e: InterruptedException) { - Thread.currentThread().interrupt() - break - } - } - } - if (canceled || Thread.interrupted()) + if (!executeTasks(listOf(task))) taskListener?.onTerminate() }) } @@ -120,34 +96,40 @@ class TaskExecutor() { var flag = false try { - if (!doDependentsSucceeded && t.reliant || canceled) + if (!doDependentsSucceeded && t.reliesOnDependents || canceled) throw SilentException() t.variables = variables t.execute() if (t is TaskResult<*>) variables[t.id] = t.result + + if (!executeTasks(t.dependencies) && t.reliesOnDependencies) + throw IllegalStateException("Subtasks failed for ${t.title}") + flag = true if (!t.hidden) LOG.finer("Task finished: ${t.title}") - executeTasks(t.dependencies) + if (!t.hidden) { t.onDone(TaskEvent(source = this, task = t, failed = false)) taskListener?.onFinished(t) } } catch (e: InterruptedException) { if (!t.hidden) { + lastException = e LOG.log(Level.FINE, "Task aborted: ${t.title}", e) t.onDone(TaskEvent(source = this, task = t, failed = true)) - taskListener?.onFailed(t) + taskListener?.onFailed(t, e) } } catch (e: SilentException) { // nothing here } catch (e: Exception) { if (!t.hidden) { + lastException = e LOG.log(Level.SEVERE, "Task failed: ${t.title}", e) t.onDone(TaskEvent(source = this, task = t, failed = true)) - taskListener?.onFailed(t) + taskListener?.onFailed(t, e) } } finally { t.variables = null diff --git a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskListener.kt b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskListener.kt index 3412cbbea..d55a53b79 100644 --- a/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskListener.kt +++ b/HMCLCore/src/main/kotlin/org/jackhuang/hmcl/task/TaskListener.kt @@ -22,6 +22,6 @@ import java.util.* interface TaskListener : EventListener { fun onReady(task: Task) {} fun onFinished(task: Task) {} - fun onFailed(task: Task) {} + fun onFailed(task: Task, throwable: Throwable) {} fun onTerminate() {} } \ No newline at end of file diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/Test.kt b/HMCLCore/src/test/java/org/jackhuang/hmcl/Test.kt index 74531a9c2..23dc9ca32 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/Test.kt +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/Test.kt @@ -141,7 +141,7 @@ class Test { override fun onFinished(task: Task) { } - override fun onFailed(task: Task) { + override fun onFailed(task: Task, throwable: Throwable) { } override fun onTerminate() {