attempting to fix VM reboot bug

This commit is contained in:
minjaesong
2026-05-04 15:44:59 +09:00
parent 2dcdff83c8
commit 4ff48bba1c
8 changed files with 208 additions and 55 deletions

View File

@@ -122,8 +122,39 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
private var rebootRequested = false
private fun reboot() {
vmRunner.close()
// Order is critical: stop ALL execution first, then dispose peripherals
// before re-initialising. Without this, the old JS thread races the new
// one on shared VM memory / IO state and can SIGSEGV on disposed peripherals.
// 1. Stop parallel/child contexts. park() interrupts and joins them.
vm.park()
vm.poke(-90L, -128)
// 2. Interrupt the main runner thread and cancel the GraalVM context.
if (::coroutineJob.isInitialized) coroutineJob.interrupt()
try { if (::vmRunner.isInitialized) vmRunner.close() } catch (_: Throwable) {}
// 3. Wait for the main runner thread to actually finish.
if (::coroutineJob.isInitialized && coroutineJob !== Thread.currentThread()) {
try {
coroutineJob.join(2000L)
if (coroutineJob.isAlive) {
System.err.println("[VMGUI] runner ${vm.id} did not exit within 2s; proceeding anyway")
coroutineJob.interrupt()
}
}
catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
// 4. Now it's safe to release native resources held by peripherals.
for (i in 1 until vm.peripheralTable.size) {
try {
vm.peripheralTable[i].peripheral?.dispose()
}
catch (_: Throwable) {}
}
vm.init()
init()

View File

@@ -325,7 +325,13 @@ class VM(
}
fun killAllContexts() {
contexts.forEach { it.interrupt() }
// Snapshot first: interrupt() can race with the worker thread mutating `contexts`
// (see Parallel.kill / attachProgram) and we want to wait on every one of them.
val snapshot = contexts.toList()
snapshot.forEach { it.interrupt() }
snapshot.forEach {
try { it.join(500L) } catch (_: InterruptedException) { Thread.currentThread().interrupt() }
}
contexts.clear()
}

View File

@@ -22,6 +22,16 @@ object VMSetupBroker {
* @param coroutineJobs Hashmap on the host of VMs that holds the coroutine-job object for the currently running VM-instance. Key: Int(VM's identifier), value: [kotlin.coroutines.Job]
*/
fun initVMenv(vm: VM, profileJson: JsonValue, profileName: String, gpu: GraphicsAdapter, vmRunners: HashMap<VmId, VMRunner>, coroutineJobs: HashMap<VmId, Thread>, whatToDoOnVmException: (Throwable) -> Unit) {
// Refuse to start a new runner while the previous one is still alive:
// running both concurrently would race on the VM's memory / IO and lead
// to mixed text input, garbled rendering, and SIGSEGV on disposed peripherals.
coroutineJobs[vm.id]?.let { old ->
if (old.isAlive) {
System.err.println("[VMSetupBroker] previous runner for ${vm.id} is still alive; tearing it down before re-init")
killVMenv(vm, vmRunners, coroutineJobs)
}
}
vm.init()
try {
@@ -61,9 +71,38 @@ object VMSetupBroker {
*/
fun killVMenv(vm: VM, vmRunners: HashMap<VmId, VMRunner>, coroutineJobs: HashMap<VmId, Thread>) {
// Order is critical: stop ALL execution first, then dispose peripherals.
// If we disposed peripherals while the runner thread is still alive, the
// thread would touch destroyed UnsafePtrs and SIGSEGV.
// 1. Stop parallel/child contexts. park() interrupts and joins them.
vm.park()
vm.poke(-90L, -128)
// 2. Interrupt the main runner thread and cancel the GraalVM context.
// context.close(true) cancels in-flight script evaluation.
val runnerThread = coroutineJobs[vm.id]
runnerThread?.interrupt()
try { vmRunners[vm.id]?.close() } catch (_: Throwable) {}
// 3. Wait for the main runner thread to actually finish.
if (runnerThread != null && runnerThread !== Thread.currentThread()) {
try {
runnerThread.join(2000L)
if (runnerThread.isAlive) {
// Last resort: re-interrupt and accept that disposal will
// happen with the thread still alive. This is logged so
// diagnostics surface a stuck VM rather than failing silently.
System.err.println("[VMSetupBroker] runner ${vm.id} did not exit within 2s; proceeding anyway")
runnerThread.interrupt()
}
}
catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
// 4. Now it's safe to release native resources held by peripherals.
for (i in 1 until vm.peripheralTable.size) {
try {
vm.peripheralTable[i].peripheral?.dispose()
@@ -71,8 +110,9 @@ object VMSetupBroker {
catch (_: Throwable) {}
}
coroutineJobs[vm.id]?.interrupt()
vmRunners[vm.id]?.close()
// 5. Drop runner / job handles so a subsequent initVMenv won't see stale entries.
vmRunners.remove(vm.id)
coroutineJobs.remove(vm.id)
vm.getPrintStream = { TODO() }
vm.getErrorStream = { TODO() }

View File

@@ -1362,8 +1362,12 @@ open class GraphicsAdapter(private val assetsRoot: String, val vm: VM, val confi
textCursorIsOn = !textCursorIsOn
}
// force light cursor up while typing
// force light cursor up while typing -- only honour global key state when
// this VM is the focused viewport; otherwise hidden VMs would react to
// keypresses meant for the focused one.
if (Gdx.input.inputProcessor === vm.getIO()) {
textCursorIsOn = textCursorIsOn || ((1..254).any { Gdx.input.isKeyPressed(it) })
}
}

View File

@@ -285,9 +285,17 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
private var rtc = 0L
fun update(delta: Float) {
// Only the VM whose IOSpace is wired up as the active InputProcessor (i.e. the
// currently focused viewport) may observe global keyboard/mouse state. Otherwise
// hidden VMs would all see the same keypresses as the focused one.
val isFocused = Gdx.input.inputProcessor === this
if (rawInputFunctionLatched) {
rawInputFunctionLatched = false
keyEventBuffers.fill(0)
if (isFocused) {
// store mouse info
mouseX = (Gdx.input.x + guiPosX).toShort()
mouseY = (Gdx.input.y + guiPosY).toShort()
@@ -295,7 +303,6 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
// strobe keys to fill the key read buffer
var keysPushed = 0
keyEventBuffers.fill(0)
for (k in 1..254) {
if (Gdx.input.isKeyPressed(k)) {
keyEventBuffers[keysPushed] = k.toByte()
@@ -305,6 +312,10 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
if (keysPushed >= 8) break
}
}
else {
mouseDown = false
}
}
if (uptimeCounterLatched) {
uptimeCounterLatched = false
@@ -316,6 +327,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
rtc = vm.worldInterface.currentTimeInMills()
}
if (isFocused) {
// SIGTERM key combination: Ctrl+Shift+T+R
vm.stopDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) &&
Gdx.input.isKeyPressed(Input.Keys.CONTROL_LEFT) &&
@@ -337,6 +349,12 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
Gdx.input.isKeyPressed(Input.Keys.S)) || Gdx.input.isKeyPressed(Input.Keys.PRINT_SCREEN)
if (vm.sysrqDown) println("[VM-${vm.id}] SYSRQ requested")
}
else {
vm.stopDown = false
vm.resetDown = false
vm.sysrqDown = false
}
}
override fun touchUp(p0: Int, p1: Int, p2: Int, p3: Int): Boolean {
return false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -90,6 +90,11 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
var vmRunners = HashMap<VmId, VMRunner>() // <VM's identifier, VMRunner>
var coroutineJobs = HashMap<VmId, Thread>() // <VM's identifier, Job>
// Per-VM rising-edge latch for the RESET key combo (Ctrl+Shift+R+S). The reboot
// only fires when the user releases the keys, otherwise we'd restart-spam every
// frame while the combo is held.
private val rebootLatched = HashMap<VmId, Boolean>()
internal val whatToDoOnVmExceptionQueue = ArrayList<() -> Unit>()
companion object {
@@ -288,15 +293,20 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
}
private fun reboot(profileName: String) {
val vm = currentlyLoadedProfiles[profileName]!!
val vm = currentlyLoadedProfiles[profileName] ?: return
/*vmRunners[vm.id]!!.close()
coroutineJobs[vm.id]!!.interrupt()
// Tear down the old session (joins the runner thread, then disposes
// peripherals) before spinning up a new one. Without the join, the old
// JS thread races the new one on shared VM memory / IO state.
killVMenv(vm)
initVMenv(vm, profileName)
vm.init()
initVMenv(vm, profileName)*/
// hypervisor will take over by monitoring MMIO addr 48
// The old IOSpace was kept (peripheralTable[0] survives init/kill), so
// the InputProcessor reference is still valid; just make sure the
// currently focused viewport is still wired to it.
if (currentVMselection != null && vms[currentVMselection!!]?.vm?.id == vm.id) {
Gdx.input.inputProcessor = vm.getIO()
}
}
private fun updateGame(delta: Float) {
@@ -312,8 +322,27 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
}
vms.forEachIndexed { index, it ->
if (it?.vm?.resetDown == true && index == currentVMselection) { reboot(it.profileName) }
if (it?.vm?.isRunning == true) it?.vm?.update(delta)
if (it == null) return@forEachIndexed
val vmId = it.vm.id
// Trigger reboot on the *release* edge of the RESET key combo, and
// only for the focused viewport (resetDown for a hidden VM is
// already kept false by IOSpace.update; this guard is belt-and-braces).
if (index == currentVMselection) {
if (it.vm.resetDown) {
rebootLatched[vmId] = true
}
else if (rebootLatched[vmId] == true) {
rebootLatched[vmId] = false
reboot(it.profileName)
return@forEachIndexed // VM was just rebuilt; skip the update tick
}
}
else {
rebootLatched[vmId] = false
}
if (it.vm.isRunning) it.vm.update(delta)
}
updateMenu()

View File

@@ -172,8 +172,34 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
private fun killVMenv() {
if (vmKilled.compareAndSet(0, System.currentTimeMillis())) {
System.err.println("VMGUI is killing VM environment...")
// Order is critical: stop ALL execution first, then dispose peripherals.
// If we disposed peripherals while the runner thread is still alive, the
// thread would touch destroyed UnsafePtrs and SIGSEGV.
// 1. Stop parallel/child contexts. park() interrupts and joins them.
vm.park()
vm.poke(-90L, -128)
// 2. Interrupt the main runner thread and cancel the GraalVM context.
if (::coroutineJob.isInitialized) coroutineJob.interrupt()
try { if (::vmRunner.isInitialized) vmRunner.close() } catch (_: Throwable) {}
// 3. Wait for the main runner thread to actually finish.
if (::coroutineJob.isInitialized && coroutineJob !== Thread.currentThread()) {
try {
coroutineJob.join(2000L)
if (coroutineJob.isAlive) {
System.err.println("[VMGUI] runner ${vm.id} did not exit within 2s; proceeding anyway")
coroutineJob.interrupt()
}
}
catch (_: InterruptedException) {
Thread.currentThread().interrupt()
}
}
// 4. Now it's safe to release native resources held by peripherals.
for (i in 1 until vm.peripheralTable.size) {
try {
vm.peripheralTable[i].peripheral?.dispose()
@@ -181,8 +207,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
catch (_: Throwable) {
}
}
coroutineJob.interrupt()
vmRunner.close()
vm.getPrintStream = { TODO() }
vm.getErrorStream = { TODO() }
vm.getInputStream = { TODO() }
@@ -195,12 +220,12 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
private var rebootRequested = false
private fun reboot() {
/*vmRunner.close()
coroutineJob.interrupt()
init()*/
// hypervisor will take over by monitoring MMIO addr 48
// Tear down the old session (joins the runner thread, then disposes
// peripherals) before re-initialising. Without the join, the old JS
// thread races the new one on shared VM memory / IO state and can
// SIGSEGV on disposed peripherals.
killVMenv()
init()
}
private var updateAkku = 0.0