Minecraft Logging

This commit is contained in:
huangyuhui 2017-08-20 19:47:28 +08:00
parent cbf2a4e7a8
commit 563ea993c6
19 changed files with 519 additions and 90 deletions

View File

@ -18,10 +18,13 @@
package org.jackhuang.hmcl package org.jackhuang.hmcl
import javafx.application.Application import javafx.application.Application
import javafx.application.Platform
import javafx.stage.Stage import javafx.stage.Stage
import javafx.stage.StageStyle
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.Scheduler import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.ui.Controllers import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.runOnUiThread
import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT import org.jackhuang.hmcl.util.DEFAULT_USER_AGENT
import org.jackhuang.hmcl.util.LOG import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.OS import org.jackhuang.hmcl.util.OS
@ -41,7 +44,10 @@ fun i18n(key: String): String {
class Main : Application() { class Main : Application() {
override fun start(stage: Stage) { override fun start(stage: Stage) {
PRIMARY_STAGE = stage // When launcher visibility is set to "hide and reopen" without [Platform.implicitExit] = false,
// Stage.show() cannot work again because JavaFX Toolkit have already shut down.
Platform.setImplicitExit(false)
Controllers.initialize(stage) Controllers.initialize(stage)
stage.isResizable = false stage.isResizable = false
@ -55,7 +61,6 @@ class Main : Application() {
val NAME = "HMCL" val NAME = "HMCL"
val TITLE = "$NAME $VERSION" val TITLE = "$NAME $VERSION"
val APPDATA = getWorkingDirectory("hmcl") val APPDATA = getWorkingDirectory("hmcl")
lateinit var PRIMARY_STAGE: Stage
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
@ -79,8 +84,9 @@ class Main : Application() {
fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft") fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft")
fun stop() { fun stop() = runOnUiThread {
PRIMARY_STAGE.close() Controllers.stage.close()
Platform.exit()
Scheduler.shutdown() Scheduler.shutdown()
} }

View File

@ -17,19 +17,27 @@
*/ */
package org.jackhuang.hmcl.game package org.jackhuang.hmcl.game
import org.jackhuang.hmcl.Main
import org.jackhuang.hmcl.auth.AuthInfo
import org.jackhuang.hmcl.auth.AuthenticationException import org.jackhuang.hmcl.auth.AuthenticationException
import org.jackhuang.hmcl.launch.DefaultLauncher import org.jackhuang.hmcl.launch.DefaultLauncher
import org.jackhuang.hmcl.launch.ProcessListener
import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
import org.jackhuang.hmcl.setting.LauncherVisibility
import org.jackhuang.hmcl.setting.Settings import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.task.* import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.ui.Controllers import org.jackhuang.hmcl.ui.Controllers
import org.jackhuang.hmcl.ui.DialogController import org.jackhuang.hmcl.ui.DialogController
import org.jackhuang.hmcl.ui.LaunchingStepsPane import org.jackhuang.hmcl.ui.LaunchingStepsPane
import org.jackhuang.hmcl.ui.runOnUiThread import org.jackhuang.hmcl.ui.runOnUiThread
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.Log4jLevel
import java.util.concurrent.ConcurrentSkipListSet
object LauncherHelper { object LauncherHelper {
val launchingStepsPane = LaunchingStepsPane() private val launchingStepsPane = LaunchingStepsPane()
val PROCESS = ConcurrentSkipListSet<JavaProcess>()
fun launch() { fun launch() {
val profile = Settings.selectedProfile val profile = Settings.selectedProfile
@ -37,6 +45,7 @@ object LauncherHelper {
val dependency = profile.dependency val dependency = profile.dependency
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here") val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
val version = repository.getVersion(profile.selectedVersion) val version = repository.getVersion(profile.selectedVersion)
val setting = profile.getVersionSetting(profile.selectedVersion)
var finished = 0 var finished = 0
Controllers.dialog(launchingStepsPane) Controllers.dialog(launchingStepsPane)
@ -61,13 +70,17 @@ object LauncherHelper {
it["launcher"] = HMCLGameLauncher( it["launcher"] = HMCLGameLauncher(
repository = repository, repository = repository,
versionId = profile.selectedVersion, versionId = profile.selectedVersion,
options = profile.getVersionSetting(profile.selectedVersion).toLaunchOptions(profile.gameDir), options = setting.toLaunchOptions(profile.gameDir),
listener = HMCLProcessListener(it["account"], setting.launcherVisibility),
account = it["account"] account = it["account"]
) )
}) })
.then { it.get<DefaultLauncher>("launcher").launchAsync() } .then { it.get<DefaultLauncher>("launcher").launchAsync() }
.then(task {
if (setting.launcherVisibility == LauncherVisibility.CLOSE)
Main.stop()
})
.then(task(Scheduler.JAVAFX) { emitStatus(LoadingState.DONE) })
.executor() .executor()
.apply { .apply {
taskListener = object : TaskListener { taskListener = object : TaskListener {
@ -79,19 +92,87 @@ object LauncherHelper {
override fun onTerminate() { override fun onTerminate() {
runOnUiThread { Controllers.closeDialog() } runOnUiThread { Controllers.closeDialog() }
} }
override fun end() {
runOnUiThread { Controllers.closeDialog() }
}
} }
}.start() }.start()
} }
fun emitStatus(state: LoadingState) { fun emitStatus(state: LoadingState) {
if (state == LoadingState.DONE) {
Controllers.closeDialog()
}
launchingStepsPane.lblCurrentState.text = state.toString() launchingStepsPane.lblCurrentState.text = state.toString()
launchingStepsPane.lblSteps.text = "${state.ordinal + 1} / ${LoadingState.values().size}" launchingStepsPane.lblSteps.text = "${state.ordinal + 1} / ${LoadingState.values().size}"
} }
/**
* The managed process listener.
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
* Because every time we launched a game, we generates a new [HMCLProcessListener]
*/
class HMCLProcessListener(authInfo: AuthInfo?, private val launcherVisibility: LauncherVisibility) : ProcessListener {
val forbiddenTokens: List<Pair<String, String>> = if (authInfo == null) emptyList() else
listOf(
authInfo.authToken to "<access token>",
authInfo.userId to "<uuid>",
authInfo.username to "<player>"
)
private lateinit var process: JavaProcess
private var lwjgl = false
override fun setProcess(process: JavaProcess) {
this.process = process
}
override fun onLog(log: String, level: Log4jLevel) {
if (level.lessOrEqual(Log4jLevel.ERROR))
System.err.print(log)
else
System.out.print(log)
if (!lwjgl && log.contains("LWJGL Version: ")) {
lwjgl = true
when (launcherVisibility) {
LauncherVisibility.HIDE_AND_REOPEN -> {
runOnUiThread {
Controllers.stage.hide()
emitStatus(LoadingState.DONE)
}
}
LauncherVisibility.CLOSE -> {
throw Error("Never come to here")
}
LauncherVisibility.KEEP -> {
// No operations here.
}
LauncherVisibility.HIDE -> {
runOnUiThread {
Controllers.stage.close()
emitStatus(LoadingState.DONE)
}
}
}
}
}
override fun onExit(exitCode: Int, exitType: ProcessListener.ExitType) {
checkExit(launcherVisibility)
}
}
private fun checkExit(launcherVisibility: LauncherVisibility) {
when (launcherVisibility) {
LauncherVisibility.HIDE_AND_REOPEN -> runOnUiThread { Controllers.stage.show() }
LauncherVisibility.KEEP -> {}
LauncherVisibility.CLOSE -> {}
LauncherVisibility.HIDE -> Main.stop()
}
}
fun stopManagedProcess() {
PROCESS.forEach(JavaProcess::stop)
}
enum class LoadingState { enum class LoadingState {
DEPENDENCIES, DEPENDENCIES,
MODS, MODS,

View File

@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXDialog import com.jfoenix.controls.JFXDialog
import javafx.fxml.FXMLLoader
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.Scene import javafx.scene.Scene
import javafx.scene.layout.Region import javafx.scene.layout.Region

View File

@ -96,7 +96,7 @@ class YggdrasilAccount private constructor(override val username: String): Accou
isOnline = true isOnline = true
return return
} }
logIn1(ROUTE_REFRESH, RefreshRequest(clientToken = clientToken, accessToken = accessToken!!, selectedProfile = selectedProfile), proxy) logIn1(ROUTE_REFRESH, RefreshRequest(clientToken = clientToken, accessToken = accessToken!!), proxy)
} else if (isNotBlank(password)) { } else if (isNotBlank(password)) {
logIn1(ROUTE_AUTHENTICATE, AuthenticationRequest(username, password!!, clientToken), proxy) logIn1(ROUTE_AUTHENTICATE, AuthenticationRequest(username, password!!, clientToken), proxy)
} else } else

View File

@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.event package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.task.Scheduler
import java.util.* import java.util.*
class EventBus { class EventBus {
@ -25,7 +26,7 @@ class EventBus {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
fun <T : EventObject> channel(classOfT: Class<T>): EventManager<T> { fun <T : EventObject> channel(classOfT: Class<T>): EventManager<T> {
if (!events.containsKey(classOfT)) if (!events.containsKey(classOfT))
events.put(classOfT, EventManager<T>()) events.put(classOfT, EventManager<T>(Scheduler.COMPUTATION))
return events[classOfT] as EventManager<T> return events[classOfT] as EventManager<T>
} }

View File

@ -17,10 +17,11 @@
*/ */
package org.jackhuang.hmcl.event package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.util.SimpleMultimap import org.jackhuang.hmcl.util.SimpleMultimap
import java.util.* import java.util.*
class EventManager<T : EventObject> { class EventManager<T : EventObject>(val scheduler: Scheduler = Scheduler.IMMEDIATE) {
private val handlers = SimpleMultimap<EventPriority, (T) -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet) private val handlers = SimpleMultimap<EventPriority, (T) -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet)
private val handlers2 = SimpleMultimap<EventPriority, () -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet) private val handlers2 = SimpleMultimap<EventPriority, () -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet)
@ -43,11 +44,13 @@ class EventManager<T : EventObject> {
} }
fun fireEvent(event: T) { fun fireEvent(event: T) {
for (priority in EventPriority.values()) { scheduler.schedule {
for (handler in handlers[priority]) for (priority in EventPriority.values()) {
handler(event) for (handler in handlers[priority])
for (handler in handlers2[priority]) handler(event)
handler() for (handler in handlers2[priority])
handler()
}
} }
} }

View File

@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.event package org.jackhuang.hmcl.event
import org.jackhuang.hmcl.util.JavaProcess
import java.util.EventObject import java.util.EventObject
/** /**
@ -48,4 +49,34 @@ class RefreshedVersionsEvent(source: Any) : EventObject(source)
* @param version the version id. * @param version the version id.
* @author huangyuhui * @author huangyuhui
*/ */
class LoadedOneVersionEvent(source: Any, val version: String) : EventObject(source) class LoadedOneVersionEvent(source: Any, val version: String) : EventObject(source)
/**
* This event gets fired when a JavaProcess exited abnormally and the exit code is not zero.
* <br></br>
* This event is fired on the [org.jackhuang.hmcl.api.HMCLApi.EVENT_BUS]
* @param source [org.jackhuang.hmcl.util.sys.JavaProcessMonitor]
* @param JavaProcess The process that exited abnormally.
* @author huangyuhui
*/
class JavaProcessExitedAbnormallyEvent(source: Any, val value: JavaProcess) : EventObject(source)
/**
* This event gets fired when minecraft process exited successfully and the exit code is 0.
* <br></br>
* This event is fired on the [org.jackhuang.hmcl.api.HMCLApi.EVENT_BUS]
* @param source [org.jackhuang.hmcl.util.sys.JavaProcessMonitor]
* @param JavaProcess minecraft process
* @author huangyuhui
*/
class JavaProcessStoppedEvent(source: Any, val value: JavaProcess) : EventObject(source)
/**
* This event gets fired when we launch the JVM and it got crashed.
* <br></br>
* This event is fired on the [org.jackhuang.hmcl.api.HMCLApi.EVENT_BUS]
* @param source [org.jackhuang.hmcl.util.sys.JavaProcessMonitor]
* @param JavaProcess the crashed process.
* @author huangyuhui
*/
class JVMLaunchFailedEvent(source: Any, val value: JavaProcess) : EventObject(source)

View File

@ -0,0 +1,31 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.launch
fun parseCrashReport(lines: List<String>) {
var errorText: String? = null
for (line in lines) {
errorText = line.substringAfterLast("#@!@#")
if (errorText.isNotBlank())
break
}
if (errorText != null && errorText.isNotBlank()) {
}
}

View File

@ -25,6 +25,7 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.coroutines.experimental.EmptyCoroutineContext.plus
/** /**
* @param versionId The version to be launched. * @param versionId The version to be launched.
@ -279,14 +280,19 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
} }
private fun startMonitors(javaProcess: JavaProcess) { private fun startMonitors(javaProcess: JavaProcess) {
thread(name = "stdout-pump", isDaemon = true, block = StreamPump(javaProcess.process.inputStream)::run) javaProcess.relatedThreads += thread(name = "stdout-pump", isDaemon = true, block = StreamPump(javaProcess.process.inputStream)::run)
thread(name = "stderr-pump", isDaemon = true, block = StreamPump(javaProcess.process.errorStream)::run) javaProcess.relatedThreads += thread(name = "stderr-pump", isDaemon = true, block = StreamPump(javaProcess.process.errorStream)::run)
} }
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) { private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, processListener::onLog)::run) processListener.setProcess(javaProcess)
thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, processListener::onErrorLog)::run) val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); javaProcess.stdOutLines += line }.apply { start() }
thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess.process, processListener::onExit)::run) javaProcess.relatedThreads += logHandler
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, { logHandler.newLine(it) } )::run)
javaProcess.relatedThreads += stdout
val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); javaProcess.stdErrLines += it })::run)
javaProcess.relatedThreads += stderr
javaProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
} }
companion object { companion object {

View File

@ -0,0 +1,112 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* 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 {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.task.Scheduler
import org.jackhuang.hmcl.util.Log4jLevel
import org.jackhuang.hmcl.util.OS
import org.xml.sax.Attributes
import org.xml.sax.InputSource
import org.xml.sax.helpers.DefaultHandler
import org.xml.sax.helpers.XMLReaderFactory
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.text.SimpleDateFormat
import java.util.*
/**
* This class is to parse log4j classic XML layout logging, since only vanilla Minecraft will enable this layout.
*/
internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread() {
val reader = XMLReaderFactory.createXMLReader().apply {
contentHandler = Log4jHandlerImpl()
}
private val outputStream = PipedOutputStream()
private val inputStream = PipedInputStream(outputStream)
override fun run() {
name = "log4j-handler"
newLine("<output>")
reader.parse(InputSource(inputStream))
}
fun onStopped() {
Scheduler.NEW_THREAD.schedule {
newLine("</output>")?.get()
outputStream.close()
join()
}!!.get()
}
/**
* Should be called in [ProcessListener.onErrorLog] and [ProcessListener.onLog] manually.
*/
fun newLine(content: String) =
Scheduler.COMPUTATION.schedule {
outputStream.write((content + OS.LINE_SEPARATOR).replace("log4j:Event", "log4j_Event").replace("log4j:Message", "log4j_Message").replace("log4j:Throwable", "log4j_Throwable").toByteArray())
outputStream.flush()
}
inner class Log4jHandlerImpl : DefaultHandler() {
private val df = SimpleDateFormat("HH:mm:ss")
var date = ""
var thread = ""
var logger = ""
var message: StringBuilder? = null
var l: Log4jLevel? = null
var readingMessage = false
override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
when (localName) {
"log4j_Event" -> {
message = StringBuilder()
val d = Date(attributes.getValue("timestamp").toLong())
date = df.format(d)
try {
l = Log4jLevel.valueOf(attributes.getValue("level"))
} catch (e: IllegalArgumentException) {
l = Log4jLevel.INFO
}
thread = attributes.getValue("thread")
logger = attributes.getValue("logger")
if ("STDERR" == logger)
l = Log4jLevel.ERROR
}
"log4j_Message" -> readingMessage = true
}
}
override fun endElement(uri: String?, localName: String?, qName: String?) {
when (localName) {
"log4j_Event" -> callback("[" + date + "] [" + thread + "/" + l!!.name + "] [" + logger + "] " + message.toString(), l!!)
"log4j_Message" -> readingMessage = false
}
}
override fun characters(ch: CharArray?, start: Int, length: Int) {
val line = String(ch!!, start, length)
if (line.trim { it <= ' ' }.isEmpty()) return
if (readingMessage)
message!!.append(line).append(OS.LINE_SEPARATOR)
else
callback(line, Log4jLevel.guessLevel(line) ?: Log4jLevel.INFO)
}
}
}

View File

@ -17,25 +17,33 @@
*/ */
package org.jackhuang.hmcl.launch package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.Log4jLevel
interface ProcessListener { interface ProcessListener {
/**
* When a game launched, this method will be called to get the new process.
* You should not override this method when your ProcessListener is shared with all processes.
*/
fun setProcess(process: JavaProcess) {}
/** /**
* Called when receiving a log from stdout * Called when receiving a log from stdout
* *
* @param log the log * @param log the log
*/ */
fun onLog(log: String) fun onLog(log: String, level: Log4jLevel)
/**
* Called when receiving a log from stderr.
*
* @param log the log
*/
fun onErrorLog(log: String)
/** /**
* Called when the game process stops. * Called when the game process stops.
* *
* @param exitCode the exit code * @param exitCode the exit code
*/ */
fun onExit(exitCode: Int) fun onExit(exitCode: Int, exitType: ExitType)
enum class ExitType {
JVM_ERROR,
APPLICATION_ERROR,
NORMAL
}
} }

View File

@ -26,46 +26,42 @@ interface Scheduler {
fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() }) fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() })
fun schedule(block: Callable<Unit>): Future<*>? fun schedule(block: Callable<Unit>): Future<*>?
companion object Schedulers { private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater) override fun schedule(block: Callable<Unit>): Future<*>? {
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater) val latch = CountDownLatch(1)
private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler { val wrapper = AtomicReference<Exception>()
override fun schedule(block: Callable<Unit>): Future<*>? { executor.invoke(Runnable {
val latch = CountDownLatch(1) try {
val wrapper = AtomicReference<Exception>() block.call()
executor.invoke(Runnable { } catch (e: Exception) {
try { wrapper.set(e)
block.call() } finally {
} catch (e: Exception) { latch.countDown()
wrapper.set(e)
} finally {
latch.countDown()
}
})
return object : Future<Unit> {
override fun get(timeout: Long, unit: TimeUnit) {
latch.await(timeout, unit)
val e = wrapper.get()
if (e != null) throw ExecutionException(e)
}
override fun get() {
latch.await()
val e = wrapper.get()
if (e != null) throw ExecutionException(e)
}
override fun isDone() = latch.count == 0L
override fun isCancelled() = false
override fun cancel(mayInterruptIfRunning: Boolean) = false
} }
})
return object : Future<Unit> {
override fun get(timeout: Long, unit: TimeUnit) {
latch.await(timeout, unit)
val e = wrapper.get()
if (e != null) throw ExecutionException(e)
}
override fun get() {
latch.await()
val e = wrapper.get()
if (e != null) throw ExecutionException(e)
}
override fun isDone() = latch.count == 0L
override fun isCancelled() = false
override fun cancel(mayInterruptIfRunning: Boolean) = false
} }
} }
val NEW_THREAD: Scheduler = object : Scheduler { }
override fun schedule(block: Callable<Unit>) = CACHED_EXECUTOR.submit(block)
} private class SchedulerExecutorService(val executorService: ExecutorService) : Scheduler {
val IO: Scheduler = object : Scheduler { override fun schedule(block: Callable<Unit>) = executorService.submit(block)
override fun schedule(block: Callable<Unit>) = IO_EXECUTOR.submit(block) }
}
val DEFAULT = NEW_THREAD companion object Schedulers {
private val CACHED_EXECUTOR: ExecutorService by lazy { private val CACHED_EXECUTOR: ExecutorService by lazy {
ThreadPoolExecutor(0, Integer.MAX_VALUE, ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS, 60L, TimeUnit.SECONDS,
@ -80,9 +76,31 @@ interface Scheduler {
} }
} }
private val SINGLE_EXECUTOR: ExecutorService by lazy {
Executors.newSingleThreadExecutor { r: Runnable ->
val thread: Thread = Executors.defaultThreadFactory().newThread(r)
thread.isDaemon = true
thread
}
}
val IMMEDIATE: Scheduler = object : Scheduler {
override fun schedule(block: Callable<Unit>): Future<*>? {
block.call()
return null
}
}
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater)
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater)
val NEW_THREAD: Scheduler = SchedulerExecutorService(CACHED_EXECUTOR)
val IO: Scheduler = SchedulerExecutorService(IO_EXECUTOR)
val COMPUTATION: Scheduler = SchedulerExecutorService(SINGLE_EXECUTOR)
val DEFAULT = NEW_THREAD
fun shutdown() { fun shutdown() {
CACHED_EXECUTOR.shutdown() CACHED_EXECUTOR.shutdown()
IO_EXECUTOR.shutdown() IO_EXECUTOR.shutdown()
SINGLE_EXECUTOR.shutdown()
} }
} }
} }

View File

@ -66,8 +66,6 @@ class TaskExecutor() {
} }
if (canceled || Thread.interrupted()) if (canceled || Thread.interrupted())
taskListener?.onTerminate() taskListener?.onTerminate()
else
taskListener?.end()
}) })
} }

View File

@ -24,5 +24,4 @@ interface TaskListener : EventListener {
fun onFinished(task: Task) {} fun onFinished(task: Task) {}
fun onFailed(task: Task) {} fun onFailed(task: Task) {}
fun onTerminate() {} fun onTerminate() {}
fun end() {}
} }

View File

@ -17,17 +17,49 @@
*/ */
package org.jackhuang.hmcl.util package org.jackhuang.hmcl.util
import org.jackhuang.hmcl.event.EVENT_BUS
import org.jackhuang.hmcl.event.JVMLaunchFailedEvent
import org.jackhuang.hmcl.event.JavaProcessExitedAbnormallyEvent
import org.jackhuang.hmcl.event.JavaProcessStoppedEvent
import org.jackhuang.hmcl.launch.ProcessListener
import java.util.*
/** /**
* @param process the process to wait for * @param process the process to wait for
* @param watcher the callback that will be called after process stops. * @param watcher the callback that will be called after process stops.
*/ */
internal class ExitWaiter(val process: Process, val watcher: (Int) -> Unit) : Runnable { internal class ExitWaiter(val process: JavaProcess, val joins: Collection<Thread>, val watcher: (Int, ProcessListener.ExitType) -> Unit) : Runnable {
override fun run() { override fun run() {
try { try {
process.waitFor() process.process.waitFor()
watcher(process.exitValue())
joins.forEach { it.join() }
val exitCode = process.exitCode
val lines = LinkedList<String>()
lines.addAll(process.stdErrLines)
lines.addAll(process.stdOutLines)
val errorLines = lines.filter(::guessLogLineError)
val exitType: ProcessListener.ExitType
// LaunchWrapper will catch the exception logged and will exit normally.
if (exitCode != 0 || errorLines.containsOne("Unable to launch")) {
EVENT_BUS.fireEvent(JavaProcessExitedAbnormallyEvent(this, process))
exitType = ProcessListener.ExitType.APPLICATION_ERROR
} else if (exitCode != 0 && errorLines.containsOne(
"Could not create the Java Virtual Machine.",
"Error occurred during initialization of VM",
"A fatal exception has occurred. Program will exit.",
"Unable to launch")) {
EVENT_BUS.fireEvent(JVMLaunchFailedEvent(this, process))
exitType = ProcessListener.ExitType.JVM_ERROR
} else
exitType = ProcessListener.ExitType.NORMAL
EVENT_BUS.fireEvent(JavaProcessStoppedEvent(this, process))
watcher(exitCode, exitType)
} catch (e: InterruptedException) { } catch (e: InterruptedException) {
watcher(1) watcher(1, ProcessListener.ExitType.NORMAL)
} }
} }
} }

View File

@ -17,13 +17,16 @@
*/ */
package org.jackhuang.hmcl.util package org.jackhuang.hmcl.util
import java.util.* import java.util.concurrent.ConcurrentLinkedQueue
class JavaProcess( class JavaProcess(
val process: Process, val process: Process,
val commands: List<String> val commands: List<String>
) { ) {
val stdOutLines: List<String> = Collections.synchronizedList(LinkedList<String>()) val properties = mutableMapOf<String, Any>()
val stdOutLines: MutableCollection<String> = ConcurrentLinkedQueue<String>()
val stdErrLines: MutableCollection<String> = ConcurrentLinkedQueue<String>()
val relatedThreads = mutableListOf<Thread>()
val isRunning: Boolean = try { val isRunning: Boolean = try {
process.exitValue() process.exitValue()
true true
@ -32,11 +35,10 @@ class JavaProcess(
} }
val exitCode: Int get() = process.exitValue() val exitCode: Int get() = process.exitValue()
override fun toString(): String { override fun toString() = "JavaProcess[commands=$commands, isRunning=$isRunning]"
return "JavaProcess[commands=$commands, isRunning=$isRunning]"
}
fun stop() { fun stop() {
process.destroy() process.destroy()
relatedThreads.forEach(Thread::interrupt)
} }
} }

View File

@ -122,6 +122,14 @@ fun parseParams(beforeFunc: (Any?) -> String, params: Array<*>?, afterFunc: (Any
return sb.toString() return sb.toString()
} }
fun Collection<String>.containsOne(vararg matcher: String): Boolean {
for (a in this)
for (b in matcher)
if (a.toLowerCase().contains(b.toLowerCase()))
return true
return false
}
fun <T> Property<in T>.updateAsync(newValue: T, update: AtomicReference<T>) { fun <T> Property<in T>.updateAsync(newValue: T, update: AtomicReference<T>) {
if (update.getAndSet(newValue) == null) { if (update.getAndSet(newValue) == null) {
UI_THREAD_SCHEDULER { UI_THREAD_SCHEDULER {

View File

@ -24,6 +24,8 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.logging.* import java.util.logging.*
import java.util.logging.Formatter import java.util.logging.Formatter
import javafx.scene.paint.Color
import java.util.regex.Pattern
val LOG = Logger.getLogger("HMCL").apply { val LOG = Logger.getLogger("HMCL").apply {
level = Level.FINER level = Level.FINER
@ -50,4 +52,90 @@ internal object DefaultFormatter : Formatter() {
return s return s
} }
} }
/**
*
* @author huangyuhui
*/
enum class Log4jLevel constructor(val level: Int, val color: Color) {
FATAL(1, Color.RED),
ERROR(2, Color.RED),
WARN(3, Color.ORANGE),
INFO(4, Color.BLACK),
DEBUG(5, Color.BLUE),
TRACE(6, Color.BLUE),
ALL(2147483647, Color.BLACK);
fun lessOrEqual(level: Log4jLevel): Boolean {
return this.level <= level.level
}
companion object {
val MINECRAFT_LOGGER = Pattern.compile("\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\]")
val MINECRAFT_LOGGER_CATEGORY = Pattern.compile("\\[(?<timestamp>[0-9:]+)\\] \\[[^/]+/(?<level>[^\\]]+)\\] \\[(?<category>[^\\]]+)\\]")
val JAVA_SYMBOL = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"
fun guessLevel(line: String): Log4jLevel? {
var level: Log4jLevel? = null
val m = MINECRAFT_LOGGER.matcher(line)
if (m.find()) {
// New style logs from log4j
val levelStr = m.group("level")
if (null != levelStr)
when (levelStr) {
"INFO" -> level = INFO
"WARN" -> level = WARN
"ERROR" -> level = ERROR
"FATAL" -> level = FATAL
"TRACE" -> level = TRACE
"DEBUG" -> level = DEBUG
else -> {
}
}
val m2 = MINECRAFT_LOGGER_CATEGORY.matcher(line)
if (m2.find()) {
val level2Str = m2.group("category")
if (null != level2Str)
when (level2Str) {
"STDOUT" -> level = INFO
"STDERR" -> level = ERROR
}
}
} else {
if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]")
|| line.contains("[FINER]") || line.contains("[FINEST]"))
level = INFO
if (line.contains("[SEVERE]") || line.contains("[STDERR]"))
level = ERROR
if (line.contains("[WARNING]"))
level = WARN
if (line.contains("[DEBUG]"))
level = DEBUG
}
return if (line.contains("overwriting existing")) FATAL else level
/*if (line.contains("Exception in thread")
|| line.matches("\\s+at " + JAVA_SYMBOL)
|| line.matches("Caused by: " + JAVA_SYMBOL)
|| line.matches("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")
|| line.matches("... \\d+ more$"))
return ERROR;*/
}
fun isError(a: Log4jLevel?): Boolean {
return a?.lessOrEqual(ERROR) ?: false
}
fun mergeLevel(a: Log4jLevel?, b: Log4jLevel?): Log4jLevel? {
return if (a == null) b
else if (b == null) a
else if (a.level < b.level) a else b
}
}
}
fun guessLogLineError(log: String) = Log4jLevel.isError(Log4jLevel.guessLevel(log))

View File

@ -22,17 +22,23 @@ import java.io.InputStream
import java.util.logging.Level import java.util.logging.Level
internal class StreamPump @JvmOverloads constructor( internal class StreamPump @JvmOverloads constructor(
val inputStream: InputStream, private val inputStream: InputStream,
val callback: (String) -> Unit = {} private val callback: (String) -> Unit = {}
) : Runnable { ) : Runnable {
override fun run() { override fun run() {
try { try {
inputStream.bufferedReader(SYSTEM_CHARSET).useLines { inputStream.bufferedReader(SYSTEM_CHARSET).useLines { lines ->
it.forEach(callback) for (line in lines) {
if (Thread.currentThread().isInterrupted) {
Thread.currentThread().interrupt()
break
}
callback(line)
}
} }
} catch (e: IOException) { } catch (e: IOException) {
LOG.log(Level.SEVERE, "An error occured when reading stream", e) LOG.log(Level.SEVERE, "An error occurred when reading stream", e)
} }
} }
} }