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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

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.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 {

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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