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,22 +32,27 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
val writeQueue = playhead.pcmQueue val writeQueue = playhead.pcmQueue
if (playhead.isPlaying && writeQueue.notEmpty()) { if (playhead.isPlaying) {
// 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()
if (samples != null) {
printdbg("Taking samples from queue (queue size: ${writeQueue.size}/${playhead.getPcmQueueCapacity()})") printdbg("Taking samples from queue (queue size: ${writeQueue.size}/${playhead.getPcmQueueCapacity()})")
val samples = writeQueue.removeFirst()
playhead.position = writeQueue.size playhead.position = writeQueue.size
playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size) playhead.audioDevice.writeSamplesUI8(samples, 0, samples.size)
Thread.sleep(6) Thread.sleep(6)
} }
else if (playhead.isPlaying && writeQueue.isEmpty) { else {
printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ") printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ")
Thread.sleep(6) Thread.sleep(6)
} }
}
} else { } else {
// Tracker mode // Tracker mode
if (playhead.isPlaying) { if (playhead.isPlaying) {
@@ -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