mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-08 19:35:36 -04:00
Minecraft Logging
This commit is contained in:
parent
cbf2a4e7a8
commit
563ea993c6
@ -18,10 +18,13 @@
|
||||
package org.jackhuang.hmcl
|
||||
|
||||
import javafx.application.Application
|
||||
import javafx.application.Platform
|
||||
import javafx.stage.Stage
|
||||
import javafx.stage.StageStyle
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.task.Scheduler
|
||||
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.LOG
|
||||
import org.jackhuang.hmcl.util.OS
|
||||
@ -41,7 +44,10 @@ fun i18n(key: String): String {
|
||||
class Main : Application() {
|
||||
|
||||
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)
|
||||
|
||||
stage.isResizable = false
|
||||
@ -55,7 +61,6 @@ class Main : Application() {
|
||||
val NAME = "HMCL"
|
||||
val TITLE = "$NAME $VERSION"
|
||||
val APPDATA = getWorkingDirectory("hmcl")
|
||||
lateinit var PRIMARY_STAGE: Stage
|
||||
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
@ -79,8 +84,9 @@ class Main : Application() {
|
||||
|
||||
fun getMinecraftDirectory(): File = getWorkingDirectory("minecraft")
|
||||
|
||||
fun stop() {
|
||||
PRIMARY_STAGE.close()
|
||||
fun stop() = runOnUiThread {
|
||||
Controllers.stage.close()
|
||||
Platform.exit()
|
||||
Scheduler.shutdown()
|
||||
}
|
||||
|
||||
|
@ -17,19 +17,27 @@
|
||||
*/
|
||||
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.launch.DefaultLauncher
|
||||
import org.jackhuang.hmcl.launch.ProcessListener
|
||||
import org.jackhuang.hmcl.mod.CurseForgeModpackCompletionTask
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility
|
||||
import org.jackhuang.hmcl.setting.Settings
|
||||
import org.jackhuang.hmcl.task.*
|
||||
import org.jackhuang.hmcl.ui.Controllers
|
||||
import org.jackhuang.hmcl.ui.DialogController
|
||||
import org.jackhuang.hmcl.ui.LaunchingStepsPane
|
||||
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 {
|
||||
val launchingStepsPane = LaunchingStepsPane()
|
||||
private val launchingStepsPane = LaunchingStepsPane()
|
||||
val PROCESS = ConcurrentSkipListSet<JavaProcess>()
|
||||
|
||||
fun launch() {
|
||||
val profile = Settings.selectedProfile
|
||||
@ -37,6 +45,7 @@ object LauncherHelper {
|
||||
val dependency = profile.dependency
|
||||
val account = Settings.selectedAccount ?: throw IllegalStateException("No account here")
|
||||
val version = repository.getVersion(profile.selectedVersion)
|
||||
val setting = profile.getVersionSetting(profile.selectedVersion)
|
||||
var finished = 0
|
||||
|
||||
Controllers.dialog(launchingStepsPane)
|
||||
@ -61,13 +70,17 @@ object LauncherHelper {
|
||||
it["launcher"] = HMCLGameLauncher(
|
||||
repository = repository,
|
||||
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"]
|
||||
)
|
||||
})
|
||||
.then { it.get<DefaultLauncher>("launcher").launchAsync() }
|
||||
.then(task {
|
||||
if (setting.launcherVisibility == LauncherVisibility.CLOSE)
|
||||
Main.stop()
|
||||
})
|
||||
|
||||
.then(task(Scheduler.JAVAFX) { emitStatus(LoadingState.DONE) })
|
||||
.executor()
|
||||
.apply {
|
||||
taskListener = object : TaskListener {
|
||||
@ -79,19 +92,87 @@ object LauncherHelper {
|
||||
override fun onTerminate() {
|
||||
runOnUiThread { Controllers.closeDialog() }
|
||||
}
|
||||
|
||||
override fun end() {
|
||||
runOnUiThread { Controllers.closeDialog() }
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
fun emitStatus(state: LoadingState) {
|
||||
if (state == LoadingState.DONE) {
|
||||
Controllers.closeDialog()
|
||||
}
|
||||
|
||||
launchingStepsPane.lblCurrentState.text = state.toString()
|
||||
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 {
|
||||
DEPENDENCIES,
|
||||
MODS,
|
||||
|
@ -18,7 +18,6 @@
|
||||
package org.jackhuang.hmcl.ui
|
||||
|
||||
import com.jfoenix.controls.JFXDialog
|
||||
import javafx.fxml.FXMLLoader
|
||||
import javafx.scene.Node
|
||||
import javafx.scene.Scene
|
||||
import javafx.scene.layout.Region
|
||||
|
@ -96,7 +96,7 @@ class YggdrasilAccount private constructor(override val username: String): Accou
|
||||
isOnline = true
|
||||
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)) {
|
||||
logIn1(ROUTE_AUTHENTICATE, AuthenticationRequest(username, password!!, clientToken), proxy)
|
||||
} else
|
||||
|
@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.event
|
||||
|
||||
import org.jackhuang.hmcl.task.Scheduler
|
||||
import java.util.*
|
||||
|
||||
class EventBus {
|
||||
@ -25,7 +26,7 @@ class EventBus {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T : EventObject> channel(classOfT: Class<T>): EventManager<T> {
|
||||
if (!events.containsKey(classOfT))
|
||||
events.put(classOfT, EventManager<T>())
|
||||
events.put(classOfT, EventManager<T>(Scheduler.COMPUTATION))
|
||||
return events[classOfT] as EventManager<T>
|
||||
}
|
||||
|
||||
|
@ -17,10 +17,11 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.event
|
||||
|
||||
import org.jackhuang.hmcl.task.Scheduler
|
||||
import org.jackhuang.hmcl.util.SimpleMultimap
|
||||
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 handlers2 = SimpleMultimap<EventPriority, () -> Unit>({ EnumMap(EventPriority::class.java) }, ::HashSet)
|
||||
|
||||
@ -43,11 +44,13 @@ class EventManager<T : EventObject> {
|
||||
}
|
||||
|
||||
fun fireEvent(event: T) {
|
||||
for (priority in EventPriority.values()) {
|
||||
for (handler in handlers[priority])
|
||||
handler(event)
|
||||
for (handler in handlers2[priority])
|
||||
handler()
|
||||
scheduler.schedule {
|
||||
for (priority in EventPriority.values()) {
|
||||
for (handler in handlers[priority])
|
||||
handler(event)
|
||||
for (handler in handlers2[priority])
|
||||
handler()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.event
|
||||
|
||||
import org.jackhuang.hmcl.util.JavaProcess
|
||||
import java.util.EventObject
|
||||
|
||||
/**
|
||||
@ -48,4 +49,34 @@ class RefreshedVersionsEvent(source: Any) : EventObject(source)
|
||||
* @param version the version id.
|
||||
* @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)
|
||||
|
@ -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()) {
|
||||
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.experimental.EmptyCoroutineContext.plus
|
||||
|
||||
/**
|
||||
* @param versionId The version to be launched.
|
||||
@ -279,14 +280,19 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
||||
}
|
||||
|
||||
private fun startMonitors(javaProcess: JavaProcess) {
|
||||
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 = "stdout-pump", isDaemon = true, block = StreamPump(javaProcess.process.inputStream)::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) {
|
||||
thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, processListener::onLog)::run)
|
||||
thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, processListener::onErrorLog)::run)
|
||||
thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess.process, processListener::onExit)::run)
|
||||
processListener.setProcess(javaProcess)
|
||||
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); javaProcess.stdOutLines += line }.apply { start() }
|
||||
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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -17,25 +17,33 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.launch
|
||||
|
||||
import org.jackhuang.hmcl.util.JavaProcess
|
||||
import org.jackhuang.hmcl.util.Log4jLevel
|
||||
|
||||
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
|
||||
*
|
||||
* @param log the log
|
||||
*/
|
||||
fun onLog(log: String)
|
||||
|
||||
/**
|
||||
* Called when receiving a log from stderr.
|
||||
*
|
||||
* @param log the log
|
||||
*/
|
||||
fun onErrorLog(log: String)
|
||||
fun onLog(log: String, level: Log4jLevel)
|
||||
|
||||
/**
|
||||
* Called when the game process stops.
|
||||
*
|
||||
* @param exitCode the exit code
|
||||
*/
|
||||
fun onExit(exitCode: Int)
|
||||
fun onExit(exitCode: Int, exitType: ExitType)
|
||||
|
||||
enum class ExitType {
|
||||
JVM_ERROR,
|
||||
APPLICATION_ERROR,
|
||||
NORMAL
|
||||
}
|
||||
}
|
@ -26,46 +26,42 @@ interface Scheduler {
|
||||
fun schedule(block: () -> Unit): Future<*>? = schedule(Callable { block() })
|
||||
fun schedule(block: Callable<Unit>): Future<*>?
|
||||
|
||||
companion object Schedulers {
|
||||
val JAVAFX: Scheduler = SchedulerImpl(Platform::runLater)
|
||||
val SWING: Scheduler = SchedulerImpl(SwingUtilities::invokeLater)
|
||||
private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
|
||||
override fun schedule(block: Callable<Unit>): Future<*>? {
|
||||
val latch = CountDownLatch(1)
|
||||
val wrapper = AtomicReference<Exception>()
|
||||
executor.invoke(Runnable {
|
||||
try {
|
||||
block.call()
|
||||
} catch (e: Exception) {
|
||||
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
|
||||
private class SchedulerImpl(val executor: (Runnable) -> Unit) : Scheduler {
|
||||
override fun schedule(block: Callable<Unit>): Future<*>? {
|
||||
val latch = CountDownLatch(1)
|
||||
val wrapper = AtomicReference<Exception>()
|
||||
executor.invoke(Runnable {
|
||||
try {
|
||||
block.call()
|
||||
} catch (e: Exception) {
|
||||
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
|
||||
}
|
||||
}
|
||||
val NEW_THREAD: Scheduler = object : Scheduler {
|
||||
override fun schedule(block: Callable<Unit>) = CACHED_EXECUTOR.submit(block)
|
||||
}
|
||||
val IO: Scheduler = object : Scheduler {
|
||||
override fun schedule(block: Callable<Unit>) = IO_EXECUTOR.submit(block)
|
||||
}
|
||||
val DEFAULT = NEW_THREAD
|
||||
}
|
||||
|
||||
private class SchedulerExecutorService(val executorService: ExecutorService) : Scheduler {
|
||||
override fun schedule(block: Callable<Unit>) = executorService.submit(block)
|
||||
}
|
||||
|
||||
companion object Schedulers {
|
||||
private val CACHED_EXECUTOR: ExecutorService by lazy {
|
||||
ThreadPoolExecutor(0, Integer.MAX_VALUE,
|
||||
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() {
|
||||
CACHED_EXECUTOR.shutdown()
|
||||
IO_EXECUTOR.shutdown()
|
||||
SINGLE_EXECUTOR.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
@ -66,8 +66,6 @@ class TaskExecutor() {
|
||||
}
|
||||
if (canceled || Thread.interrupted())
|
||||
taskListener?.onTerminate()
|
||||
else
|
||||
taskListener?.end()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -24,5 +24,4 @@ interface TaskListener : EventListener {
|
||||
fun onFinished(task: Task) {}
|
||||
fun onFailed(task: Task) {}
|
||||
fun onTerminate() {}
|
||||
fun end() {}
|
||||
}
|
@ -17,17 +17,49 @@
|
||||
*/
|
||||
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 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() {
|
||||
try {
|
||||
process.waitFor()
|
||||
watcher(process.exitValue())
|
||||
process.process.waitFor()
|
||||
|
||||
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) {
|
||||
watcher(1)
|
||||
watcher(1, ProcessListener.ExitType.NORMAL)
|
||||
}
|
||||
}
|
||||
}
|
@ -17,13 +17,16 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util
|
||||
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
|
||||
class JavaProcess(
|
||||
val process: Process,
|
||||
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 {
|
||||
process.exitValue()
|
||||
true
|
||||
@ -32,11 +35,10 @@ class JavaProcess(
|
||||
}
|
||||
val exitCode: Int get() = process.exitValue()
|
||||
|
||||
override fun toString(): String {
|
||||
return "JavaProcess[commands=$commands, isRunning=$isRunning]"
|
||||
}
|
||||
override fun toString() = "JavaProcess[commands=$commands, isRunning=$isRunning]"
|
||||
|
||||
fun stop() {
|
||||
process.destroy()
|
||||
relatedThreads.forEach(Thread::interrupt)
|
||||
}
|
||||
}
|
@ -122,6 +122,14 @@ fun parseParams(beforeFunc: (Any?) -> String, params: Array<*>?, afterFunc: (Any
|
||||
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>) {
|
||||
if (update.getAndSet(newValue) == null) {
|
||||
UI_THREAD_SCHEDULER {
|
||||
|
@ -24,6 +24,8 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.logging.*
|
||||
import java.util.logging.Formatter
|
||||
import javafx.scene.paint.Color
|
||||
import java.util.regex.Pattern
|
||||
|
||||
val LOG = Logger.getLogger("HMCL").apply {
|
||||
level = Level.FINER
|
||||
@ -50,4 +52,90 @@ internal object DefaultFormatter : Formatter() {
|
||||
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))
|
@ -22,17 +22,23 @@ import java.io.InputStream
|
||||
import java.util.logging.Level
|
||||
|
||||
internal class StreamPump @JvmOverloads constructor(
|
||||
val inputStream: InputStream,
|
||||
val callback: (String) -> Unit = {}
|
||||
private val inputStream: InputStream,
|
||||
private val callback: (String) -> Unit = {}
|
||||
) : Runnable {
|
||||
|
||||
override fun run() {
|
||||
try {
|
||||
inputStream.bufferedReader(SYSTEM_CHARSET).useLines {
|
||||
it.forEach(callback)
|
||||
inputStream.bufferedReader(SYSTEM_CHARSET).useLines { lines ->
|
||||
for (line in lines) {
|
||||
if (Thread.currentThread().isInterrupted) {
|
||||
Thread.currentThread().interrupt()
|
||||
break
|
||||
}
|
||||
callback(line)
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user