mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-10 20:37:30 -04:00
Log window category and show lines
This commit is contained in:
parent
be202a6e1a
commit
a67bfabea4
@ -31,12 +31,13 @@ import org.jackhuang.hmcl.task.*
|
|||||||
import org.jackhuang.hmcl.ui.*
|
import org.jackhuang.hmcl.ui.*
|
||||||
import org.jackhuang.hmcl.util.JavaProcess
|
import org.jackhuang.hmcl.util.JavaProcess
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel
|
import org.jackhuang.hmcl.util.Log4jLevel
|
||||||
import java.util.concurrent.ConcurrentSkipListSet
|
import java.util.*
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
|
|
||||||
object LauncherHelper {
|
object LauncherHelper {
|
||||||
private val launchingStepsPane = LaunchingStepsPane()
|
private val launchingStepsPane = LaunchingStepsPane()
|
||||||
val PROCESS = ConcurrentSkipListSet<JavaProcess>()
|
val PROCESS = ConcurrentLinkedQueue<JavaProcess>()
|
||||||
|
|
||||||
fun launch() {
|
fun launch() {
|
||||||
val profile = Settings.selectedProfile
|
val profile = Settings.selectedProfile
|
||||||
@ -76,6 +77,7 @@ object LauncherHelper {
|
|||||||
})
|
})
|
||||||
.then { it.get<DefaultLauncher>("launcher").launchAsync() }
|
.then { it.get<DefaultLauncher>("launcher").launchAsync() }
|
||||||
.then(task {
|
.then(task {
|
||||||
|
PROCESS.add(it[DefaultLauncher.LAUNCH_ASYNC_ID])
|
||||||
if (setting.launcherVisibility == LauncherVisibility.CLOSE)
|
if (setting.launcherVisibility == LauncherVisibility.CLOSE)
|
||||||
Main.stop()
|
Main.stop()
|
||||||
})
|
})
|
||||||
@ -120,6 +122,7 @@ object LauncherHelper {
|
|||||||
private lateinit var process: JavaProcess
|
private lateinit var process: JavaProcess
|
||||||
private var lwjgl = false
|
private var lwjgl = false
|
||||||
private var logWindow: LogWindow? = null
|
private var logWindow: LogWindow? = null
|
||||||
|
private val logs = LinkedList<Pair<String, Log4jLevel>>()
|
||||||
override fun setProcess(process: JavaProcess) {
|
override fun setProcess(process: JavaProcess) {
|
||||||
this.process = process
|
this.process = process
|
||||||
|
|
||||||
@ -129,12 +132,21 @@ object LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLog(log: String, level: Log4jLevel) {
|
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))
|
if (level.lessOrEqual(Log4jLevel.ERROR))
|
||||||
System.err.print(log)
|
System.err.print(log)
|
||||||
else
|
else
|
||||||
System.out.print(log)
|
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: ")) {
|
if (!lwjgl && log.contains("LWJGL Version: ")) {
|
||||||
lwjgl = true
|
lwjgl = true
|
||||||
@ -162,6 +174,16 @@ object LauncherHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onExit(exitCode: Int, exitType: ProcessListener.ExitType) {
|
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)
|
checkExit(launcherVisibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,8 +203,11 @@ object LauncherHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopManagedProcess() {
|
fun stopManagedProcesses() {
|
||||||
PROCESS.forEach(JavaProcess::stop)
|
synchronized(PROCESS) {
|
||||||
|
while (PROCESS.isNotEmpty())
|
||||||
|
PROCESS.poll()?.stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class LoadingState {
|
enum class LoadingState {
|
||||||
|
@ -200,6 +200,12 @@ object Settings {
|
|||||||
SETTINGS.fontSize = value.size
|
SETTINGS.fontSize = value.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var logLines: Int
|
||||||
|
get() = maxOf(SETTINGS.logLines, 100)
|
||||||
|
set(value) {
|
||||||
|
SETTINGS.logLines = value
|
||||||
|
}
|
||||||
|
|
||||||
var downloadProvider: DownloadProvider
|
var downloadProvider: DownloadProvider
|
||||||
get() = when (SETTINGS.downloadtype) {
|
get() = when (SETTINGS.downloadtype) {
|
||||||
0 -> MojangDownloadProvider
|
0 -> MojangDownloadProvider
|
||||||
|
@ -17,49 +17,133 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui
|
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.concurrent.Worker
|
||||||
|
import javafx.fxml.FXML
|
||||||
import javafx.scene.Scene
|
import javafx.scene.Scene
|
||||||
|
import javafx.scene.control.ToggleButton
|
||||||
|
import javafx.scene.image.Image
|
||||||
import javafx.scene.layout.StackPane
|
import javafx.scene.layout.StackPane
|
||||||
import javafx.scene.web.WebEngine
|
import javafx.scene.web.WebEngine
|
||||||
import javafx.scene.web.WebView
|
import javafx.scene.web.WebView
|
||||||
import javafx.stage.Stage
|
import javafx.stage.Stage
|
||||||
|
import org.jackhuang.hmcl.game.LauncherHelper
|
||||||
import org.jackhuang.hmcl.i18n
|
import org.jackhuang.hmcl.i18n
|
||||||
import org.jackhuang.hmcl.setting.Settings
|
import org.jackhuang.hmcl.setting.Settings
|
||||||
import org.jackhuang.hmcl.util.Log4jLevel
|
import org.jackhuang.hmcl.util.Log4jLevel
|
||||||
|
import org.jackhuang.hmcl.util.inc
|
||||||
import org.jackhuang.hmcl.util.readFullyAsString
|
import org.jackhuang.hmcl.util.readFullyAsString
|
||||||
import org.w3c.dom.Document
|
import org.w3c.dom.Document
|
||||||
import org.w3c.dom.Node
|
import org.w3c.dom.Node
|
||||||
|
import java.util.concurrent.Callable
|
||||||
|
|
||||||
class LogWindow : Stage() {
|
class LogWindow : Stage() {
|
||||||
val contentPane = WebView()
|
val fatalProperty = SimpleIntegerProperty(0)
|
||||||
val rootPane = StackPane().apply {
|
val errorProperty = SimpleIntegerProperty(0)
|
||||||
children.setAll(contentPane)
|
val warnProperty = SimpleIntegerProperty(0)
|
||||||
}
|
val infoProperty = SimpleIntegerProperty(0)
|
||||||
val engine: WebEngine
|
val debugProperty = SimpleIntegerProperty(0)
|
||||||
lateinit var body: Node
|
|
||||||
lateinit var document: Document
|
val impl = LogWindowImpl()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
scene = Scene(rootPane, 800.0, 480.0)
|
scene = Scene(impl, 800.0, 480.0)
|
||||||
|
scene.stylesheets.addAll(*stylesheets)
|
||||||
title = i18n("logwindow.title")
|
title = i18n("logwindow.title")
|
||||||
|
icons += Image("/assets/img/icon.png")
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun logLine(line: String, level: Log4jLevel) {
|
fun logLine(line: String, level: Log4jLevel) {
|
||||||
body.appendChild(contentPane.engine.document.createElement("div").apply {
|
impl.body.appendChild(impl.engine.document.createElement("div").apply {
|
||||||
setAttribute("style", "background-color: #${level.color.toString().substring(2)};")
|
|
||||||
textContent = line
|
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()")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -794,6 +794,20 @@
|
|||||||
-fx-background-color: null;
|
-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 *
|
* JFX Spinner *
|
||||||
|
61
HMCL/src/main/resources/assets/fxml/log.fxml
Normal file
61
HMCL/src/main/resources/assets/fxml/log.fxml
Normal 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>
|
@ -8,6 +8,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
font: ${FONT};
|
font: ${FONT};
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
@ -16,10 +17,55 @@
|
|||||||
border-width: 0 0 1px 0;
|
border-width: 0 0 1px 0;
|
||||||
border-color: #dddddd;
|
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>
|
</style>
|
||||||
<script>
|
<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() {
|
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>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
@ -286,11 +286,11 @@ open class DefaultLauncher(repository: GameRepository, versionId: String, accoun
|
|||||||
|
|
||||||
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
|
private fun startMonitors(javaProcess: JavaProcess, processListener: ProcessListener, isDaemon: Boolean = true) {
|
||||||
processListener.setProcess(javaProcess)
|
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
|
javaProcess.relatedThreads += logHandler
|
||||||
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, { logHandler.newLine(it) } )::run)
|
val stdout = thread(name = "stdout-pump", isDaemon = isDaemon, block = StreamPump(javaProcess.process.inputStream, { logHandler.newLine(it) } )::run)
|
||||||
javaProcess.relatedThreads += stdout
|
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 += stderr
|
||||||
javaProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
|
javaProcess.relatedThreads += thread(name = "exit-waiter", isDaemon = isDaemon, block = ExitWaiter(javaProcess, listOf(stdout, stderr), { exitCode, exitType -> logHandler.onStopped(); processListener.onExit(exitCode, exitType) })::run)
|
||||||
}
|
}
|
||||||
|
@ -15,13 +15,15 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
* 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.EVENT_BUS
|
||||||
import org.jackhuang.hmcl.event.JVMLaunchFailedEvent
|
import org.jackhuang.hmcl.event.JVMLaunchFailedEvent
|
||||||
import org.jackhuang.hmcl.event.JavaProcessExitedAbnormallyEvent
|
import org.jackhuang.hmcl.event.JavaProcessExitedAbnormallyEvent
|
||||||
import org.jackhuang.hmcl.event.JavaProcessStoppedEvent
|
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.*
|
import java.util.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,10 +38,7 @@ internal class ExitWaiter(val process: JavaProcess, val joins: Collection<Thread
|
|||||||
joins.forEach { it.join() }
|
joins.forEach { it.join() }
|
||||||
|
|
||||||
val exitCode = process.exitCode
|
val exitCode = process.exitCode
|
||||||
val lines = LinkedList<String>()
|
val errorLines = process.lines.filter(::guessLogLineError)
|
||||||
lines.addAll(process.stdErrLines)
|
|
||||||
lines.addAll(process.stdOutLines)
|
|
||||||
val errorLines = lines.filter(::guessLogLineError)
|
|
||||||
val exitType: ProcessListener.ExitType
|
val exitType: ProcessListener.ExitType
|
||||||
// LaunchWrapper will catch the exception logged and will exit normally.
|
// LaunchWrapper will catch the exception logged and will exit normally.
|
||||||
if (exitCode != 0 || errorLines.containsOne("Unable to launch")) {
|
if (exitCode != 0 || errorLines.containsOne("Unable to launch")) {
|
@ -24,10 +24,12 @@ import org.xml.sax.Attributes
|
|||||||
import org.xml.sax.InputSource
|
import org.xml.sax.InputSource
|
||||||
import org.xml.sax.helpers.DefaultHandler
|
import org.xml.sax.helpers.DefaultHandler
|
||||||
import org.xml.sax.helpers.XMLReaderFactory
|
import org.xml.sax.helpers.XMLReaderFactory
|
||||||
|
import java.io.InterruptedIOException
|
||||||
import java.io.PipedInputStream
|
import java.io.PipedInputStream
|
||||||
import java.io.PipedOutputStream
|
import java.io.PipedOutputStream
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
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.
|
* 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 outputStream = PipedOutputStream()
|
||||||
private val inputStream = PipedInputStream(outputStream)
|
private val inputStream = PipedInputStream(outputStream)
|
||||||
|
private val interrupted = AtomicBoolean(false)
|
||||||
|
|
||||||
override fun run() {
|
override fun run() {
|
||||||
name = "log4j-handler"
|
name = "log4j-handler"
|
||||||
newLine("<output>")
|
newLine("<output>")
|
||||||
reader.parse(InputSource(inputStream))
|
try {
|
||||||
|
reader.parse(InputSource(inputStream))
|
||||||
|
} catch (e: InterruptedIOException) {
|
||||||
|
// Game has been interrupted.
|
||||||
|
interrupted.set(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onStopped() {
|
fun onStopped() {
|
||||||
|
if (interrupted.get())
|
||||||
|
return
|
||||||
Scheduler.NEW_THREAD.schedule {
|
Scheduler.NEW_THREAD.schedule {
|
||||||
newLine("</output>")?.get()
|
if (!interrupted.get()) {
|
||||||
outputStream.close()
|
newLine("</output>")?.get()
|
||||||
join()
|
outputStream.close()
|
||||||
|
join()
|
||||||
|
}
|
||||||
}!!.get()
|
}!!.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,9 @@ interface ProcessListener {
|
|||||||
fun setProcess(process: JavaProcess) {}
|
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
|
* @param log the log
|
||||||
*/
|
*/
|
||||||
|
@ -15,8 +15,10 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see {http://www.gnu.org/licenses/}.
|
* 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.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
@ -17,6 +17,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.util
|
package org.jackhuang.hmcl.util
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
|
|
||||||
class JavaProcess(
|
class JavaProcess(
|
||||||
@ -24,8 +25,7 @@ class JavaProcess(
|
|||||||
val commands: List<String>
|
val commands: List<String>
|
||||||
) {
|
) {
|
||||||
val properties = mutableMapOf<String, Any>()
|
val properties = mutableMapOf<String, Any>()
|
||||||
val stdOutLines: MutableCollection<String> = ConcurrentLinkedQueue<String>()
|
val lines: Queue<String> = ConcurrentLinkedQueue<String>()
|
||||||
val stdErrLines: MutableCollection<String> = ConcurrentLinkedQueue<String>()
|
|
||||||
val relatedThreads = mutableListOf<Thread>()
|
val relatedThreads = mutableListOf<Thread>()
|
||||||
val isRunning: Boolean = try {
|
val isRunning: Boolean = try {
|
||||||
process.exitValue()
|
process.exitValue()
|
||||||
|
@ -18,6 +18,8 @@
|
|||||||
package org.jackhuang.hmcl.util
|
package org.jackhuang.hmcl.util
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import javafx.scene.input.Clipboard
|
||||||
|
import javafx.scene.input.ClipboardContent
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.management.ManagementFactory
|
import java.lang.management.ManagementFactory
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
@ -65,5 +67,12 @@ enum class OS {
|
|||||||
|
|
||||||
val SYSTEM_VERSION: String by lazy { System.getProperty("os.version") }
|
val SYSTEM_VERSION: String by lazy { System.getProperty("os.version") }
|
||||||
val SYSTEM_ARCH: String by lazy { System.getProperty("os.arch") }
|
val SYSTEM_ARCH: String by lazy { System.getProperty("os.arch") }
|
||||||
|
|
||||||
|
fun setClipboard(string: String) {
|
||||||
|
val clipboard = Clipboard.getSystemClipboard()
|
||||||
|
clipboard.setContent(ClipboardContent().apply {
|
||||||
|
putString(string)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user