mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-06 05:28:31 +09:00
attempting to fix VM reboot bug
This commit is contained in:
@@ -122,8 +122,39 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
|
|||||||
private var rebootRequested = false
|
private var rebootRequested = false
|
||||||
|
|
||||||
private fun reboot() {
|
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()
|
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()
|
vm.init()
|
||||||
init()
|
init()
|
||||||
|
|||||||
@@ -325,7 +325,13 @@ class VM(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun killAllContexts() {
|
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()
|
contexts.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
* @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) {
|
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()
|
vm.init()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -61,9 +71,38 @@ object VMSetupBroker {
|
|||||||
*/
|
*/
|
||||||
fun killVMenv(vm: VM, vmRunners: HashMap<VmId, VMRunner>, coroutineJobs: HashMap<VmId, Thread>) {
|
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.park()
|
||||||
vm.poke(-90L, -128)
|
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) {
|
for (i in 1 until vm.peripheralTable.size) {
|
||||||
try {
|
try {
|
||||||
vm.peripheralTable[i].peripheral?.dispose()
|
vm.peripheralTable[i].peripheral?.dispose()
|
||||||
@@ -71,8 +110,9 @@ object VMSetupBroker {
|
|||||||
catch (_: Throwable) {}
|
catch (_: Throwable) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
coroutineJobs[vm.id]?.interrupt()
|
// 5. Drop runner / job handles so a subsequent initVMenv won't see stale entries.
|
||||||
vmRunners[vm.id]?.close()
|
vmRunners.remove(vm.id)
|
||||||
|
coroutineJobs.remove(vm.id)
|
||||||
|
|
||||||
vm.getPrintStream = { TODO() }
|
vm.getPrintStream = { TODO() }
|
||||||
vm.getErrorStream = { TODO() }
|
vm.getErrorStream = { TODO() }
|
||||||
|
|||||||
@@ -1362,8 +1362,12 @@ open class GraphicsAdapter(private val assetsRoot: String, val vm: VM, val confi
|
|||||||
textCursorIsOn = !textCursorIsOn
|
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) })
|
textCursorIsOn = textCursorIsOn || ((1..254).any { Gdx.input.isKeyPressed(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,9 +285,17 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
private var rtc = 0L
|
private var rtc = 0L
|
||||||
|
|
||||||
fun update(delta: Float) {
|
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) {
|
if (rawInputFunctionLatched) {
|
||||||
rawInputFunctionLatched = false
|
rawInputFunctionLatched = false
|
||||||
|
|
||||||
|
keyEventBuffers.fill(0)
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
// store mouse info
|
// store mouse info
|
||||||
mouseX = (Gdx.input.x + guiPosX).toShort()
|
mouseX = (Gdx.input.x + guiPosX).toShort()
|
||||||
mouseY = (Gdx.input.y + guiPosY).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
|
// strobe keys to fill the key read buffer
|
||||||
var keysPushed = 0
|
var keysPushed = 0
|
||||||
keyEventBuffers.fill(0)
|
|
||||||
for (k in 1..254) {
|
for (k in 1..254) {
|
||||||
if (Gdx.input.isKeyPressed(k)) {
|
if (Gdx.input.isKeyPressed(k)) {
|
||||||
keyEventBuffers[keysPushed] = k.toByte()
|
keyEventBuffers[keysPushed] = k.toByte()
|
||||||
@@ -305,6 +312,10 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
if (keysPushed >= 8) break
|
if (keysPushed >= 8) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
mouseDown = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (uptimeCounterLatched) {
|
if (uptimeCounterLatched) {
|
||||||
uptimeCounterLatched = false
|
uptimeCounterLatched = false
|
||||||
@@ -316,6 +327,7 @@ class IOSpace(val vm: VM) : PeriBase("io"), InputProcessor {
|
|||||||
rtc = vm.worldInterface.currentTimeInMills()
|
rtc = vm.worldInterface.currentTimeInMills()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFocused) {
|
||||||
// SIGTERM key combination: Ctrl+Shift+T+R
|
// SIGTERM key combination: Ctrl+Shift+T+R
|
||||||
vm.stopDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) &&
|
vm.stopDown = (Gdx.input.isKeyPressed(Input.Keys.SHIFT_LEFT) &&
|
||||||
Gdx.input.isKeyPressed(Input.Keys.CONTROL_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)
|
Gdx.input.isKeyPressed(Input.Keys.S)) || Gdx.input.isKeyPressed(Input.Keys.PRINT_SCREEN)
|
||||||
if (vm.sysrqDown) println("[VM-${vm.id}] SYSRQ requested")
|
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 {
|
override fun touchUp(p0: Int, p1: Int, p2: Int, p3: Int): Boolean {
|
||||||
return false
|
return false
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.6 KiB |
@@ -90,6 +90,11 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
var vmRunners = HashMap<VmId, VMRunner>() // <VM's identifier, VMRunner>
|
var vmRunners = HashMap<VmId, VMRunner>() // <VM's identifier, VMRunner>
|
||||||
var coroutineJobs = HashMap<VmId, Thread>() // <VM's identifier, Job>
|
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>()
|
internal val whatToDoOnVmExceptionQueue = ArrayList<() -> Unit>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -288,15 +293,20 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun reboot(profileName: String) {
|
private fun reboot(profileName: String) {
|
||||||
val vm = currentlyLoadedProfiles[profileName]!!
|
val vm = currentlyLoadedProfiles[profileName] ?: return
|
||||||
|
|
||||||
/*vmRunners[vm.id]!!.close()
|
// Tear down the old session (joins the runner thread, then disposes
|
||||||
coroutineJobs[vm.id]!!.interrupt()
|
// 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()
|
// The old IOSpace was kept (peripheralTable[0] survives init/kill), so
|
||||||
initVMenv(vm, profileName)*/
|
// the InputProcessor reference is still valid; just make sure the
|
||||||
|
// currently focused viewport is still wired to it.
|
||||||
// hypervisor will take over by monitoring MMIO addr 48
|
if (currentVMselection != null && vms[currentVMselection!!]?.vm?.id == vm.id) {
|
||||||
|
Gdx.input.inputProcessor = vm.getIO()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGame(delta: Float) {
|
private fun updateGame(delta: Float) {
|
||||||
@@ -312,8 +322,27 @@ class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX:
|
|||||||
}
|
}
|
||||||
|
|
||||||
vms.forEachIndexed { index, it ->
|
vms.forEachIndexed { index, it ->
|
||||||
if (it?.vm?.resetDown == true && index == currentVMselection) { reboot(it.profileName) }
|
if (it == null) return@forEachIndexed
|
||||||
if (it?.vm?.isRunning == true) it?.vm?.update(delta)
|
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()
|
updateMenu()
|
||||||
|
|||||||
@@ -172,8 +172,34 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
|
|||||||
private fun killVMenv() {
|
private fun killVMenv() {
|
||||||
if (vmKilled.compareAndSet(0, System.currentTimeMillis())) {
|
if (vmKilled.compareAndSet(0, System.currentTimeMillis())) {
|
||||||
System.err.println("VMGUI is killing VM environment...")
|
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.park()
|
||||||
vm.poke(-90L, -128)
|
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) {
|
for (i in 1 until vm.peripheralTable.size) {
|
||||||
try {
|
try {
|
||||||
vm.peripheralTable[i].peripheral?.dispose()
|
vm.peripheralTable[i].peripheral?.dispose()
|
||||||
@@ -181,8 +207,7 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
|
|||||||
catch (_: Throwable) {
|
catch (_: Throwable) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
coroutineJob.interrupt()
|
|
||||||
vmRunner.close()
|
|
||||||
vm.getPrintStream = { TODO() }
|
vm.getPrintStream = { TODO() }
|
||||||
vm.getErrorStream = { TODO() }
|
vm.getErrorStream = { TODO() }
|
||||||
vm.getInputStream = { TODO() }
|
vm.getInputStream = { TODO() }
|
||||||
@@ -195,12 +220,12 @@ class VMGUI(val loaderInfo: EmulInstance, val viewportWidth: Int, val viewportHe
|
|||||||
private var rebootRequested = false
|
private var rebootRequested = false
|
||||||
|
|
||||||
private fun reboot() {
|
private fun reboot() {
|
||||||
/*vmRunner.close()
|
// Tear down the old session (joins the runner thread, then disposes
|
||||||
coroutineJob.interrupt()
|
// peripherals) before re-initialising. Without the join, the old JS
|
||||||
|
// thread races the new one on shared VM memory / IO state and can
|
||||||
init()*/
|
// SIGSEGV on disposed peripherals.
|
||||||
|
killVMenv()
|
||||||
// hypervisor will take over by monitoring MMIO addr 48
|
init()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var updateAkku = 0.0
|
private var updateAkku = 0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user