printing stacktrace on host exception

This commit is contained in:
minjaesong
2026-06-20 12:18:27 +09:00
parent 76e297a412
commit 04aa651ff1
5 changed files with 88 additions and 24 deletions

View File

@@ -895,8 +895,12 @@ shell.execute = function(line, nameOverride) {
catch (e) { catch (e) {
gotError = true gotError = true
serial.printerr(`[command.js] program quit with ${e}:\n${e.stack || '(stack trace unavailable)'}`) // A host (Java) exception has no JS `.stack`, so `e.stack` alone is
printerrln(`Program quit with ${e}:\n${e.stack || '(stack trace unavailable)'}`) // "(stack trace unavailable)". Recover the real host trace (to stderr + string).
let hostTrace = ""
try { hostTrace = sys.printStackTrace(e) } catch (_) {}
serial.printerr(`[command.js] program quit with ${e}:\n${e.stack || hostTrace || '(stack trace unavailable)'}`)
printerrln(`Program quit with ${e}:\n${e.stack || hostTrace || '(stack trace unavailable)'}`)
if (`${e}`.startsWith("InterruptedException")) if (`${e}`.startsWith("InterruptedException"))
errorlevel = SIGTERM.name errorlevel = SIGTERM.name

View File

@@ -585,7 +585,7 @@ class AudioJSR223Delegate(private val vm: VM) {
getFirstSnd()?.let { snd -> getFirstSnd()?.let { snd ->
val ba = ByteArray(2304) val ba = ByteArray(2304)
UnsafeHelper.memcpyRaw(null, snd.mediaDecodedBin.ptr, ba, UnsafeHelper.getArrayOffset(ba), 2304) UnsafeHelper.memcpyRaw(null, snd.mediaDecodedBin.ptr, ba, UnsafeHelper.getArrayOffset(ba), 2304)
snd.playheads[playhead].pcmQueue.addLast(ba) snd.playheads[playhead].pcmQueue.add(ba)
} }
} }
@@ -600,7 +600,7 @@ class AudioJSR223Delegate(private val vm: VM) {
getFirstSnd()?.let { snd -> getFirstSnd()?.let { snd ->
val ba = ByteArray(sampleLength * 2) // 32768 samples * 2 channels val ba = ByteArray(sampleLength * 2) // 32768 samples * 2 channels
UnsafeHelper.memcpyRaw(null, snd.tadDecodedBin.ptr, ba, UnsafeHelper.getArrayOffset(ba), sampleLength * 2L) UnsafeHelper.memcpyRaw(null, snd.tadDecodedBin.ptr, ba, UnsafeHelper.getArrayOffset(ba), sampleLength * 2L)
snd.playheads[playhead].pcmQueue.addLast(ba) snd.playheads[playhead].pcmQueue.add(ba)
} }
} }

View File

@@ -778,7 +778,7 @@ class VM(
// MMIO area // MMIO area
else if (from in -1048576..-1 && (from - len) in -1048577..-1) { else if (from in -1048576..-1 && (from - len) in -1048577..-1) {
val fromIndex = (-from-1) / 131072 val fromIndex = (-from-1) / 131072
val dev = peripheralTable[fromIndex.toInt()].peripheral ?: return null val dev = peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null
val fromRel = (-from-1) % 131072 val fromRel = (-from-1) % 131072
if (fromRel + len > 131072) return null if (fromRel + len > 131072) return null
@@ -807,7 +807,7 @@ class VM(
// memory area // memory area
else { else {
val fromIndex = (-from-1) / 1048576 val fromIndex = (-from-1) / 1048576
val dev = peripheralTable[fromIndex.toInt()].peripheral ?: return null val dev = peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null
val fromRel = (-from-1) % 1048576 val fromRel = (-from-1) % 1048576
if (fromRel + len > 1048576) return null if (fromRel + len > 1048576) return null

View File

@@ -25,7 +25,7 @@ class VMJSR223Delegate(private val vm: VM) {
// MMIO area // MMIO area
else if (from in -1048576..-1 && (from - len) in -1048577..-1) { else if (from in -1048576..-1 && (from - len) in -1048577..-1) {
val fromIndex = ((-from-1) / 131072).absoluteValue val fromIndex = ((-from-1) / 131072).absoluteValue
val dev = vm.peripheralTable[fromIndex.toInt()].peripheral ?: return null val dev = vm.peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null
val fromRel = (-from-1) % 131072 val fromRel = (-from-1) % 131072
if (fromRel + len > 131072) return null if (fromRel + len > 131072) return null
@@ -55,7 +55,7 @@ class VMJSR223Delegate(private val vm: VM) {
// memory area // memory area
else { else {
val fromIndex = (-from-1) / 1048576 val fromIndex = (-from-1) / 1048576
val dev = vm.peripheralTable[fromIndex.toInt()].peripheral ?: return null val dev = vm.peripheralTable.getOrNull(fromIndex.toInt())?.peripheral ?: return null
val fromRel = (-from-1) % 1048576 val fromRel = (-from-1) % 1048576
if (fromRel + len > 1048576) return null if (fromRel + len > 1048576) return null
@@ -88,6 +88,56 @@ class VMJSR223Delegate(private val vm: VM) {
fun getVmId() = vm.id.toString() fun getVmId() = vm.id.toString()
/**
* Recover the FULL host (Java) stack trace of an exception caught in JS, write it to the host
* stderr, and also RETURN it as a string so the caller can show it on the VM screen too.
*
* When a host (Kotlin) function throws, GraalVM surfaces it to JS as an exception object whose
* `toString()` is only the message — so `catch (e) { printerrln(e) }` shows the message on the
* VM screen while the Java stack trace (the part that says *where* it blew up) is lost and the
* host stdout/stderr gets nothing. Re-throwing the caught value back across the polyglot
* boundary recovers the original Throwable so its stack trace can be printed. Call from a JS
* catch block: `let tr = sys.printStackTrace(e)` (then optionally `printerrln(tr)`).
*/
fun printStackTrace(e: org.graalvm.polyglot.Value?): String {
val sb = StringBuilder("===== host stack trace =====\n")
if (e == null) {
sb.append("(the caught value was null/undefined)\n")
} else {
try {
if (e.isException) {
e.throwException() // throws a PolyglotException wrapping the original Throwable
} else if (e.isHostObject && e.asHostObject<Any?>() is Throwable) {
// Some configs surface a caught host exception as a host object, not an
// exception object — unwrap the Throwable directly.
val sw = java.io.StringWriter()
(e.asHostObject<Any?>() as Throwable).printStackTrace(java.io.PrintWriter(sw))
sb.append("host object Throwable:\n").append(sw)
} else {
sb.append("caught value is not an exception object: ").append(e).append('\n')
}
} catch (pe: org.graalvm.polyglot.PolyglotException) {
if (pe.isHostException) {
val sw = java.io.StringWriter()
pe.asHostException().printStackTrace(java.io.PrintWriter(sw))
sb.append("host (Java) exception:\n").append(sw)
} else {
sb.append("guest exception: ").append(pe.message).append('\n')
}
sb.append("guest stack frames:\n")
for (frame in pe.polyglotStackTrace) sb.append(" at ").append(frame).append('\n')
} catch (t: Throwable) {
val sw = java.io.StringWriter()
t.printStackTrace(java.io.PrintWriter(sw))
sb.append("could not recover the trace; got:\n").append(sw)
}
}
val s = sb.toString()
System.err.println(s)
System.err.flush()
return s
}
fun poke(addr: Int, value: Int) = vm.poke(addr.toLong(), value.toByte()) fun poke(addr: Int, value: Int) = vm.poke(addr.toLong(), value.toByte())
fun peek(addr: Int) = vm.peek(addr.toLong())!!.toInt().and(255) fun peek(addr: Int) = vm.peek(addr.toLong())!!.toInt().and(255)

View File

@@ -3,7 +3,7 @@ package net.torvald.tsvm.peripheral
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio import com.badlogic.gdx.backends.lwjgl3.audio.OpenALLwjgl3Audio
import com.badlogic.gdx.utils.GdxRuntimeException import com.badlogic.gdx.utils.GdxRuntimeException
import com.badlogic.gdx.utils.Queue import java.util.concurrent.ConcurrentLinkedQueue
import io.airlift.compress.zstd.ZstdInputStream import io.airlift.compress.zstd.ZstdInputStream
import net.torvald.UnsafeHelper import net.torvald.UnsafeHelper
import net.torvald.UnsafePtr import net.torvald.UnsafePtr
@@ -32,21 +32,26 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
val writeQueue = playhead.pcmQueue val writeQueue = playhead.pcmQueue
if (playhead.isPlaying && writeQueue.notEmpty()) { if (playhead.isPlaying) {
printdbg("Taking samples from queue (queue size: ${writeQueue.size}/${playhead.getPcmQueueCapacity()})") // Single atomic poll — `pcmQueue` has concurrent producers (queueing thread /
// JS thread), so a separate notEmpty()/removeFirst() would race and corrupt it.
val samples = writeQueue.poll()
val samples = writeQueue.removeFirst() if (samples != null) {
playhead.position = writeQueue.size printdbg("Taking samples from queue (queue size: ${writeQueue.size}/${playhead.getPcmQueueCapacity()})")
playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size) playhead.position = writeQueue.size
Thread.sleep(6) playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size)
}
else if (playhead.isPlaying && writeQueue.isEmpty) {
printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ")
Thread.sleep(6) Thread.sleep(6)
}
else {
printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ")
Thread.sleep(6)
}
} }
} else { } else {
// Tracker mode // Tracker mode
@@ -92,7 +97,7 @@ private class WriteQueueingRunnable(val playhead: AudioAdapter.Playhead, val pcm
UnsafeHelper.getArrayOffset(samples), UnsafeHelper.getArrayOffset(samples),
it.pcmUploadLength.toLong() it.pcmUploadLength.toLong()
) )
it.pcmQueue.addLast(samples) it.pcmQueue.add(samples)
it.pcmUploadLength = 0 it.pcmUploadLength = 0
it.position = it.pcmQueue.size it.position = it.pcmQueue.size
@@ -442,11 +447,11 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
val writeQueue = playhead.pcmQueue val writeQueue = playhead.pcmQueue
if (playhead.isPlaying && writeQueue.notEmpty()) { val samples = if (playhead.isPlaying) writeQueue.poll() else null
if (samples != null) {
printdbg("Taking samples from queue (queue size: ${writeQueue.size})") printdbg("Taking samples from queue (queue size: ${writeQueue.size})")
val samples = writeQueue.removeFirst()
playhead.position = writeQueue.size playhead.position = writeQueue.size
// printdbg("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...") // printdbg("P${playhead.index+1} Vol ${playhead.masterVolume}; LpP ${playhead.pcmUploadLength}; start playback...")
@@ -4793,7 +4798,12 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
var globalVolume: Int = 0x80, // 8-bit, default $80 (spec §5). Mutated by V $xx00. var globalVolume: Int = 0x80, // 8-bit, default $80 (spec §5). Mutated by V $xx00.
var mixingVolume: Int = 0x80, // 8-bit, default $80 (spec §5). Final-mix scaler, set once per song. var mixingVolume: Int = 0x80, // 8-bit, default $80 (spec §5). Final-mix scaler, set once per song.
var pcmQueue: Queue<ByteArray> = Queue<ByteArray>(), // PCM playback queue. ConcurrentLinkedQueue (not the libGDX Queue) because it is written
// by the queueing thread (WAV/raw PCM via WriteQueueingRunnable) and the JS thread
// (mp2UploadDecoded / tadUploadDecoded), drained by the render thread, and purged by the JS
// thread — concurrent addLast/removeFirst on a non-thread-safe queue corrupted head/tail and
// threw a sporadic ArrayIndexOutOfBoundsException during PCM playback.
var pcmQueue: ConcurrentLinkedQueue<ByteArray> = ConcurrentLinkedQueue<ByteArray>(),
var pcmQueueSizeIndex: Int = 0, var pcmQueueSizeIndex: Int = 0,
val audioDevice: OpenALBufferedAudioDevice, val audioDevice: OpenALBufferedAudioDevice,
) { ) {
@@ -4825,7 +4835,7 @@ class AudioAdapter(val vm: VM) : PeriBase(VM.PERITYPE_SOUND) {
if (field && !value) { if (field && !value) {
// println("!! inserting dummy bytes") // println("!! inserting dummy bytes")
if (isPcmMode) { if (isPcmMode) {
pcmQueue.addLast(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount)) pcmQueue.add(ByteArray(audioDevice.bufferSize * audioDevice.bufferCount))
} }
} }
field = value field = value