Skip to content

Commit

Permalink
Improved top level error handling
Browse files Browse the repository at this point in the history
  • Loading branch information
Martomate committed Dec 5, 2023
1 parent c041080 commit 0be2d46
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 20 deletions.
22 changes: 7 additions & 15 deletions game/src/main/scala/hexacraft/main/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import hexacraft.infra.os.OSUtils

import org.lwjgl.system.Configuration

import java.io.{File, FileOutputStream, PrintStream}
import java.time.OffsetDateTime
import java.io.File

object Main:
def main(args: Array[String]): Unit =
Expand All @@ -14,24 +13,17 @@ object Main:
val isDebugStr = System.getProperty("hexacraft.debug")
val isDebug = isDebugStr != null && isDebugStr == "true"

val window = new MainWindow(isDebug)
val saveFolder: File = new File(OSUtils.appdataPath, ".hexacraft")

val errorHandler = MainErrorLogger.create(!isDebug, saveFolder)

val window = new MainWindow(isDebug, saveFolder)
try window.run()
catch
case t: Throwable =>
if isDebug
then t.printStackTrace()
else logThrowable(t, window.saveFolder)
errorHandler.log(t)
System.exit(1)

private def logThrowable(e: Throwable, saveFolder: File): Unit =
val now = OffsetDateTime.now()
val logFile = new File(saveFolder, s"logs/error_$now.log")
logFile.getParentFile.mkdirs()
e.printStackTrace(new PrintStream(new FileOutputStream(logFile)))
System.err.println(
s"The program has crashed. The crash report can be found in: ${logFile.getAbsolutePath}"
)

private def setNativesFolder(): Unit =
var file = new File("lib/natives")

Expand Down
31 changes: 31 additions & 0 deletions game/src/main/scala/hexacraft/main/MainErrorLogger.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package hexacraft.main

import hexacraft.infra.fs.FileSystem

import java.io.{ByteArrayOutputStream, File, PrintStream}
import java.time.OffsetDateTime

class MainErrorLogger(saveToFile: Boolean, saveFolder: File, fs: FileSystem) {
def log(e: Throwable): Unit = if saveToFile then logToFile(e) else logToConsole(e)

private def logToConsole(e: Throwable): Unit =
e.printStackTrace()

private def logToFile(e: Throwable): Unit =
val byteStream = new ByteArrayOutputStream()
e.printStackTrace(new PrintStream(byteStream))

val now = OffsetDateTime.now()
val logFile = new File(saveFolder, s"logs/error_$now.log".replace(':', '.'))

fs.writeBytes(logFile.toPath, byteStream.toByteArray)

System.err.println(
s"The program has crashed. The crash report can be found in: ${logFile.getAbsolutePath}"
)
}

object MainErrorLogger {
def create(saveToFile: Boolean, saveFolder: File): MainErrorLogger =
MainErrorLogger(saveToFile, saveFolder, FileSystem.create())
}
7 changes: 2 additions & 5 deletions game/src/main/scala/hexacraft/main/MainWindow.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import hexacraft.gui.*
import hexacraft.gui.comp.GUITransformation
import hexacraft.infra.fs.FileSystem
import hexacraft.infra.gpu.OpenGL
import hexacraft.infra.os.OSUtils
import hexacraft.infra.window.*
import hexacraft.renderer.VAO
import hexacraft.util.{Resource, Result}
Expand All @@ -16,9 +15,7 @@ import org.joml.Vector2i
import java.io.File
import scala.collection.mutable

class MainWindow(isDebug: Boolean) extends GameWindow:
val saveFolder: File = new File(OSUtils.appdataPath, ".hexacraft")

class MainWindow(isDebug: Boolean, saveFolder: File) extends GameWindow:
private val fs = FileSystem.create()

private val multiplayerEnabled = isDebug
Expand Down Expand Up @@ -264,6 +261,6 @@ class MainWindow(isDebug: Boolean) extends GameWindow:
private def tryQuit(): Unit = window.requestClose()

private def destroy(): Unit =
scene.unload()
if scene != null then scene.unload()

Resource.freeAllResources()
99 changes: 99 additions & 0 deletions game/src/test/scala/hexacraft/main/MainErrorLoggerTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package hexacraft.main

import hexacraft.infra.fs.FileSystem

import munit.FunSuite

import java.io.{ByteArrayOutputStream, File, PrintStream}
import java.nio.file.Path

class MainErrorLoggerTest extends FunSuite {
test("does not write log files in debug mode") {
val fs = FileSystem.createNull()
val tracker = fs.trackWrites()

val logger = MainErrorLogger(false, null, fs)
captureStdErr(logger.log(new Exception("something happened")))

assertEquals(tracker.events, Seq())
}

test("writes stacktrace to stderr in debug mode") {
val fs = FileSystem.createNull()
val logger = MainErrorLogger(false, null, fs)

val output = captureStdErr(logger.log(new Exception("something happened")))

val lines = output.lines().toList
assert(lines.size() > 1)
assertEquals(lines.get(0), "java.lang.Exception: something happened")
}

test("write a log file in release mode") {
val fs = FileSystem.createNull()
val tracker = fs.trackWrites()

val saveFolder = File("some/path")
val logger = MainErrorLogger(true, saveFolder, fs)
captureStdErr(logger.log(new Exception("something happened")))

assertEquals(tracker.events.size, 1)
val writeEvent = tracker.events(0)

val logsFolder = writeEvent.path.getParent
assertEquals(logsFolder, Path.of("some/path/logs"))

val logFileName = writeEvent.path.getFileName.toString
assert(logFileName.startsWith("error_"))
assert(logFileName.endsWith(".log"))

val logLines = String(writeEvent.bytes.toArray).lines().toList
assert(logLines.size() > 1)
assertEquals(logLines.get(0), "java.lang.Exception: something happened")
}

// TODO: the check below should probably live in FileSystem instead
// On Windows colons may not be used in file names
test("does not use colon in the log file name") {
val fs = FileSystem.createNull()
val tracker = fs.trackWrites()

val saveFolder = File("some/path")
val logger = MainErrorLogger(true, saveFolder, fs)
captureStdErr(logger.log(new Exception("something happened")))

assertEquals(tracker.events.size, 1)

val logFile = tracker.events(0).path
val logFileName = logFile.getFileName.toString
assert(!logFileName.contains(':'), s"'$logFileName' contains a colon")
}

test("writes to stderr to mention the log file in release mode") {
val fs = FileSystem.createNull()
val tracker = fs.trackWrites()

val saveFolder = File("some/path")
val logger = MainErrorLogger(true, saveFolder, fs)

val output = captureStdErr(logger.log(new Exception("something happened")))

assertEquals(tracker.events.size, 1)
val writeEvent = tracker.events(0)
val logFileAbsolutePath = writeEvent.path.toAbsolutePath.toString

assert(output.contains(logFileAbsolutePath))
}

def captureStdErr(code: => Unit): String = {
val prevErr = System.err
try
val err = ByteArrayOutputStream()
System.setErr(PrintStream(err))

code

String(err.toByteArray)
finally System.setErr(prevErr)
}
}

0 comments on commit 0be2d46

Please sign in to comment.