Log window category and show lines

This commit is contained in:
huangyuhui 2017-08-21 20:59:32 +08:00
parent be202a6e1a
commit a67bfabea4
13 changed files with 304 additions and 44 deletions

View File

@ -31,12 +31,13 @@ import org.jackhuang.hmcl.task.*
import org.jackhuang.hmcl.ui.*
import org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.Log4jLevel
import java.util.concurrent.ConcurrentSkipListSet
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
object LauncherHelper {
private val launchingStepsPane = LaunchingStepsPane()
val PROCESS = ConcurrentSkipListSet<JavaProcess>()
val PROCESS = ConcurrentLinkedQueue<JavaProcess>()
fun launch() {
val profile = Settings.selectedProfile
@ -76,6 +77,7 @@ object LauncherHelper {
})
.then { it.get<DefaultLauncher>("launcher").launchAsync() }
.then(task {
PROCESS.add(it[DefaultLauncher.LAUNCH_ASYNC_ID])
if (setting.launcherVisibility == LauncherVisibility.CLOSE)
Main.stop()
})
@ -120,6 +122,7 @@ object LauncherHelper {
private lateinit var process: JavaProcess
private var lwjgl = false
private var logWindow: LogWindow? = null
private val logs = LinkedList<Pair<String, Log4jLevel>>()
override fun setProcess(process: JavaProcess) {
this.process = process
@ -129,12 +132,21 @@ object LauncherHelper {
}
override fun onLog(log: String, level: Log4jLevel) {
var newLog = log
for ((original, replacement) in forbiddenTokens)
newLog = newLog.replace(original, replacement)
if (level.lessOrEqual(Log4jLevel.ERROR))
System.err.print(log)
else
System.out.print(log)
runOnUiThread { logWindow?.logLine(log, level) }
runOnUiThread {
logs += log to level
if (logs.size > Settings.logLines)
logs.removeFirst()
logWindow?.logLine(log, level)
}
if (!lwjgl && log.contains("LWJGL Version: ")) {
lwjgl = true
@ -162,6 +174,16 @@ object LauncherHelper {
}
override fun onExit(exitCode: Int, exitType: ProcessListener.ExitType) {
if (exitType != ProcessListener.ExitType.NORMAL && logWindow == null){
runOnUiThread {
LogWindow().apply {
for ((line, level) in logs)
logLine(line, level)
show()
}
}
}
checkExit(launcherVisibility)
}
@ -181,8 +203,11 @@ object LauncherHelper {
}
}
fun stopManagedProcess() {
PROCESS.forEach(JavaProcess::stop)
fun stopManagedProcesses() {
synchronized(PROCESS) {
while (PROCESS.isNotEmpty())
PROCESS.poll()?.stop()
}
}
enum class LoadingState {

View File

@ -200,6 +200,12 @@ object Settings {
SETTINGS.fontSize = value.size
}
var logLines: Int
get() = maxOf(SETTINGS.logLines, 100)
set(value) {
SETTINGS.logLines = value
}
var downloadProvider: DownloadProvider
get() = when (SETTINGS.downloadtype) {
0 -> MojangDownloadProvider

View File

@ -17,49 +17,133 @@
*/
package org.jackhuang.hmcl.ui
import com.jfoenix.controls.JFXComboBox
import javafx.beans.Observable
import javafx.beans.binding.Bindings
import javafx.beans.property.SimpleIntegerProperty
import javafx.concurrent.Worker
import javafx.fxml.FXML
import javafx.scene.Scene
import javafx.scene.control.ToggleButton
import javafx.scene.image.Image
import javafx.scene.layout.StackPane
import javafx.scene.web.WebEngine
import javafx.scene.web.WebView
import javafx.stage.Stage
import org.jackhuang.hmcl.game.LauncherHelper
import org.jackhuang.hmcl.i18n
import org.jackhuang.hmcl.setting.Settings
import org.jackhuang.hmcl.util.Log4jLevel
import org.jackhuang.hmcl.util.inc
import org.jackhuang.hmcl.util.readFullyAsString
import org.w3c.dom.Document
import org.w3c.dom.Node
import java.util.concurrent.Callable
class LogWindow : Stage() {
val contentPane = WebView()
val rootPane = StackPane().apply {
children.setAll(contentPane)
}
val engine: WebEngine
lateinit var body: Node
lateinit var document: Document
val fatalProperty = SimpleIntegerProperty(0)
val errorProperty = SimpleIntegerProperty(0)
val warnProperty = SimpleIntegerProperty(0)
val infoProperty = SimpleIntegerProperty(0)
val debugProperty = SimpleIntegerProperty(0)
val impl = LogWindowImpl()
init {
scene = Scene(rootPane, 800.0, 480.0)
scene = Scene(impl, 800.0, 480.0)
scene.stylesheets.addAll(*stylesheets)
title = i18n("logwindow.title")
contentPane.onScroll
engine = contentPane.engine
engine.loadContent(javaClass.getResourceAsStream("/assets/log-window-content.html").readFullyAsString().replace("\${FONT}", "${Settings.font.size}px \"${Settings.font.family}\""))
engine.loadWorker.stateProperty().addListener { _, _, newValue ->
if (newValue == Worker.State.SUCCEEDED) {
document = engine.document
body = document.getElementsByTagName("body").item(0)
}
}
icons += Image("/assets/img/icon.png")
}
fun logLine(line: String, level: Log4jLevel) {
body.appendChild(contentPane.engine.document.createElement("div").apply {
setAttribute("style", "background-color: #${level.color.toString().substring(2)};")
impl.body.appendChild(impl.engine.document.createElement("div").apply {
textContent = line
})
engine.executeScript("scrollToBottom()")
impl.engine.executeScript("checkNewLog(\"${level.name.toLowerCase()}\");scrollToBottom();")
when (level) {
Log4jLevel.FATAL -> fatalProperty.inc()
Log4jLevel.ERROR -> errorProperty.inc()
Log4jLevel.WARN -> warnProperty.inc()
Log4jLevel.INFO -> infoProperty.inc()
Log4jLevel.DEBUG -> debugProperty.inc()
else -> {}
}
}
inner class LogWindowImpl: StackPane() {
@FXML lateinit var webView: WebView
@FXML lateinit var btnFatals: ToggleButton
@FXML lateinit var btnErrors: ToggleButton
@FXML lateinit var btnWarns: ToggleButton
@FXML lateinit var btnInfos: ToggleButton
@FXML lateinit var btnDebugs: ToggleButton
@FXML lateinit var cboLines: JFXComboBox<String>
val engine: WebEngine
lateinit var body: Node
lateinit var document: Document
init {
loadFXML("/assets/fxml/log.fxml")
engine = webView.engine
engine.loadContent(javaClass.getResourceAsStream("/assets/log-window-content.html").readFullyAsString().replace("\${FONT}", "${Settings.font.size}px \"${Settings.font.family}\""))
engine.loadWorker.stateProperty().addListener { _, _, newValue ->
if (newValue == Worker.State.SUCCEEDED) {
document = engine.document
body = document.getElementsByTagName("body").item(0)
engine.executeScript("limitedLogs=${Settings.logLines};")
}
}
var flag = false
for (i in cboLines.items) {
if (i == Settings.logLines.toString()) {
cboLines.selectionModel.select(i)
flag = true
}
}
cboLines.selectionModel.selectedItemProperty().addListener { _, _, newValue ->
Settings.logLines = newValue.toInt()
engine.executeScript("limitedLogs=${Settings.logLines};")
}
if (!flag) {
cboLines.selectionModel.select(0)
}
btnFatals.textProperty().bind(Bindings.createStringBinding(Callable { fatalProperty.get().toString() + " fatals" }, fatalProperty))
btnErrors.textProperty().bind(Bindings.createStringBinding(Callable { errorProperty.get().toString() + " errors" }, errorProperty))
btnWarns.textProperty().bind(Bindings.createStringBinding(Callable { warnProperty.get().toString() + " warns" }, warnProperty))
btnInfos.textProperty().bind(Bindings.createStringBinding(Callable { infoProperty.get().toString() + " infos" }, infoProperty))
btnDebugs.textProperty().bind(Bindings.createStringBinding(Callable { debugProperty.get().toString() + " debugs" }, debugProperty))
btnFatals.selectedProperty().addListener(this::specificChanged)
btnErrors.selectedProperty().addListener(this::specificChanged)
btnWarns.selectedProperty().addListener(this::specificChanged)
btnInfos.selectedProperty().addListener(this::specificChanged)
btnDebugs.selectedProperty().addListener(this::specificChanged)
}
private fun specificChanged(observable: Observable) {
var res = ""
if (btnFatals.isSelected) res += "\"fatal\", "
if (btnErrors.isSelected) res += "\"error\", "
if (btnWarns.isSelected) res += "\"warn\", "
if (btnInfos.isSelected) res += "\"info\", "
if (btnDebugs.isSelected) res += "\"debug\", "
if (res.isNotBlank())
res = res.substringBeforeLast(", ")
engine.executeScript("specific([$res])")
}
fun onTerminateGame() {
LauncherHelper.stopManagedProcesses()
}
fun onClear() {
engine.executeScript("clear()")
}
}
}

View File

@ -794,6 +794,20 @@
-fx-background-color: null;
}
.log-toggle:selected {
-fx-background-color: transparent;
-fx-border: 1px;
-fx-border-color: black;
-fx-text-fill: black;
}
.log-toggle {
-fx-background-color: transparent;
-fx-border: 1px;
-fx-border-color: gray;
-fx-text-fill: gray;
}
/*******************************************************************************
* *
* JFX Spinner *

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.web.WebView?>
<?import javafx.scene.control.Label?>
<?import com.jfoenix.controls.JFXComboBox?>
<?import javafx.collections.FXCollections?>
<?import java.lang.String?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ToggleGroup?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane"
style="-fx-background-color: white; -fx-padding: 3 0 3 0;">
<VBox spacing="3">
<BorderPane style="-fx-padding: 0 3 0 3;">
<left>
<HBox alignment="CENTER_LEFT" style="-fx-padding: 0 0 0 4;" spacing="3">
<Label text="%logwindow.show_lines" />
<JFXComboBox fx:id="cboLines">
<items>
<FXCollections fx:factory="observableArrayList">
<String fx:value="500" />
<String fx:value="2000" />
<String fx:value="5000" />
</FXCollections>
</items>
</JFXComboBox>
</HBox>
</left>
<right>
<HBox spacing="3">
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #F7A699;" fx:id="btnFatals" text="0 fatals" selected="true">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #FFCCBB;" fx:id="btnErrors" text="0 errors" selected="true">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #FFEECC;" fx:id="btnWarns" text="0 warns" selected="true">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #FBFBFB;" fx:id="btnInfos" text="0 infos" selected="true">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
<ToggleButton styleClass="log-toggle" style="-fx-background-color: #EEE9E0;" fx:id="btnDebugs" text="0 debugs" selected="true">
<toggleGroup><ToggleGroup /></toggleGroup>
</ToggleButton>
</HBox>
</right>
</BorderPane>
<StackPane style="-fx-border: 1 0 1 0; -fx-border-color: #dddddd;" VBox.vgrow="ALWAYS">
<WebView fx:id="webView" />
</StackPane>
<HBox alignment="CENTER_RIGHT" style="-fx-padding: 0 3 0 3;" spacing="3">
<JFXButton onMouseClicked="#onTerminateGame" text="%logwindow.terminate_game" />
<JFXButton onMouseClicked="#onClear" text="%ui.button.clear" />
</HBox>
</VBox>
</fx:root>

View File

@ -8,6 +8,7 @@
margin: 0;
overflow-x: hidden;
}
div {
font: ${FONT};
margin: 0px;
@ -16,10 +17,55 @@
border-width: 0 0 1px 0;
border-color: #dddddd;
}
.fatal { background-color: #F7A699; }
.error { background-color: #FFCCBB; }
.warn { background-color: #FFEECC; }
.info { background-color: #FFFFFF; }
.debug { background-color: #EEE9E0; }
.trace { background-color: blue; }
</style>
<script>
var colors = ["fatal", "error", "warn", "info", "debug", "trace"]
var limitedLogs = 100;
function appendLog(log, level) {
var e = document.createElement("div");
e.textContent = log;
document.body.appendChild(e);
checkNewLog(level);
}
function checkNewLog(level) {
var e = document.body.lastElementChild;
e.className = level;
redisplay(e);
while (document.body.children.length > limitedLogs)
removeFirst()
}
function scrollToBottom() {
window.scrollTo(0, document.body.scrollHeight)
window.scrollTo(0, document.body.scrollHeight);
}
function removeFirst() {
document.body.removeChild(document.body.firstElementChild);
}
function clear() {
while (document.body.children.length > 0) removeFirst();
}
function specific(newColors) {
colors = newColors;
var c = document.body.children;
for (var i = 0; i < c.length; ++i)
redisplay(c[i]);
}
function redisplay(div) {
var flag = false
for (var j = 0; j < colors.length; ++j) {
if (div.className == colors[j]) {
flag = true;
break;
}
div.hidden = div.className != colors[j];
}
div.hidden = !flag;
}
</script>
</head>

View File

@ -286,11 +286,11 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
processListener.setProcess(javaProcess)
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); javaProcess.stdOutLines += line }.apply { start() }
val logHandler = Log4jHandler { line, level -> processListener.onLog(line, level); javaProcess.lines += 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)
val stderr = thread(name = "stderr-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.errorStream, { processListener.onLog(it + OS.LINE_SEPARATOR, Log4jLevel.ERROR); javaProcess.lines += 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)
}

View File

@ -15,13 +15,15 @@
* 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.util
package org.jackhuang.hmcl.launch
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 org.jackhuang.hmcl.util.JavaProcess
import org.jackhuang.hmcl.util.containsOne
import org.jackhuang.hmcl.util.guessLogLineError
import java.util.*
/**
@ -36,10 +38,7 @@ internal class ExitWaiter(val process: JavaProcess, val joins: Collection<Thread
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 errorLines = process.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")) {

View File

@ -24,10 +24,12 @@ 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.InterruptedIOException
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
/**
* This class is to parse log4j classic XML layout logging, since only vanilla Minecraft will enable this layout.
@ -38,18 +40,28 @@ internal class Log4jHandler(val callback: (String, Log4jLevel) -> Unit) : Thread
}
private val outputStream = PipedOutputStream()
private val inputStream = PipedInputStream(outputStream)
private val interrupted = AtomicBoolean(false)
override fun run() {
name = "log4j-handler"
newLine("<output>")
reader.parse(InputSource(inputStream))
try {
reader.parse(InputSource(inputStream))
} catch (e: InterruptedIOException) {
// Game has been interrupted.
interrupted.set(true)
}
}
fun onStopped() {
if (interrupted.get())
return
Scheduler.NEW_THREAD.schedule {
newLine("</output>")?.get()
outputStream.close()
join()
if (!interrupted.get()) {
newLine("</output>")?.get()
outputStream.close()
join()
}
}!!.get()
}

View File

@ -28,7 +28,9 @@ interface ProcessListener {
fun setProcess(process: JavaProcess) {}
/**
* Called when receiving a log from stdout
* Called when receiving a log from stdout/stderr.
*
* Does not guarantee that this method is thread safe.
*
* @param log the log
*/

View File

@ -15,8 +15,10 @@
* 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.util
package org.jackhuang.hmcl.launch
import org.jackhuang.hmcl.util.LOG
import org.jackhuang.hmcl.util.SYSTEM_CHARSET
import java.io.IOException
import java.io.InputStream
import java.util.logging.Level

View File

@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.util
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue
class JavaProcess(
@ -24,8 +25,7 @@ class JavaProcess(
val commands: List<String>
) {
val properties = mutableMapOf<String, Any>()
val stdOutLines: MutableCollection<String> = ConcurrentLinkedQueue<String>()
val stdErrLines: MutableCollection<String> = ConcurrentLinkedQueue<String>()
val lines: Queue<String> = ConcurrentLinkedQueue<String>()
val relatedThreads = mutableListOf<Thread>()
val isRunning: Boolean = try {
process.exitValue()

View File

@ -18,6 +18,8 @@
package org.jackhuang.hmcl.util
import com.google.gson.annotations.SerializedName
import javafx.scene.input.Clipboard
import javafx.scene.input.ClipboardContent
import java.io.File
import java.lang.management.ManagementFactory
import java.nio.charset.Charset
@ -65,5 +67,12 @@ enum class OS {
val SYSTEM_VERSION: String by lazy { System.getProperty("os.version") }
val SYSTEM_ARCH: String by lazy { System.getProperty("os.arch") }
fun setClipboard(string: String) {
val clipboard = Clipboard.getSystemClipboard()
clipboard.setContent(ClipboardContent().apply {
putString(string)
})
}
}
}