mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-03-07 11:51:49 +09:00
tevd disk drive update and fixes
This commit is contained in:
@@ -29,7 +29,8 @@ public class TerranBASIC {
|
|||||||
appConfig.setWindowedMode(WIDTH, HEIGHT);
|
appConfig.setWindowedMode(WIDTH, HEIGHT);
|
||||||
|
|
||||||
HashMap<String, VMWatchdog> watchdogs = new HashMap<>();
|
HashMap<String, VMWatchdog> watchdogs = new HashMap<>();
|
||||||
watchdogs.put("TEVD_SYNC", TevdSyncWatchdog.INSTANCE);
|
watchdogs.put("TEVD_COMMIT", TevdPartialDomCommitWatchdog.INSTANCE);
|
||||||
|
watchdogs.put("TEVD_SYNC", TevdPartialDomSyncWatchdog.INSTANCE);
|
||||||
|
|
||||||
VM tbasvm = new VM("./assets", 64 << 10, new TheRealWorld(), new VMProgramRom[]{TBASRelBios.INSTANCE}, 2, watchdogs);
|
VM tbasvm = new VM("./assets", 64 << 10, new TheRealWorld(), new VMProgramRom[]{TBASRelBios.INSTANCE}, 2, watchdogs);
|
||||||
EmulInstance tbasrunner = new EmulInstance(tbasvm, "net.torvald.tsvm.peripheral.ReferenceGraphicsAdapter", "assets/disk0", 560, 448);
|
EmulInstance tbasrunner = new EmulInstance(tbasvm, "net.torvald.tsvm.peripheral.ReferenceGraphicsAdapter", "assets/disk0", 560, 448);
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
package net.torvald.tsvm
|
package net.torvald.tsvm
|
||||||
|
|
||||||
import com.badlogic.gdx.utils.Queue
|
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.PartialDOM
|
||||||
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VDUtil
|
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VDUtil
|
||||||
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VirtualDisk
|
import net.torvald.terrarum.modulecomputers.virtualcomputer.tvd.VirtualDisk
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -29,15 +29,15 @@ abstract class VMWatchdog(val interval: Float) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
object TevdSyncWatchdog : VMWatchdog(5f) {
|
object TevdPartialDomCommitWatchdog : VMWatchdog(5f) {
|
||||||
|
|
||||||
private val messageQueue = ArrayList<Pair<File, VirtualDisk>>()
|
private val messageQueue = ArrayList<Pair<File, PartialDOM>>()
|
||||||
|
|
||||||
override fun consumeMessages() {
|
override fun consumeMessages() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
messageQueue.forEach { (outfile, dom) ->
|
messageQueue.forEach { (outfile, dom) ->
|
||||||
VDUtil.dumpToRealMachine(dom, outfile)
|
dom.commit()
|
||||||
// println("[${this.javaClass.simpleName}] dump ${outfile.path}")
|
println("[${this.javaClass.simpleName}] commit ${outfile.path}")
|
||||||
}
|
}
|
||||||
messageQueue.clear()
|
messageQueue.clear()
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,33 @@ object TevdSyncWatchdog : VMWatchdog(5f) {
|
|||||||
|
|
||||||
override fun addMessage(message: Array<Any?>) {
|
override fun addMessage(message: Array<Any?>) {
|
||||||
val file = message[0] as File
|
val file = message[0] as File
|
||||||
val dom = message[1] as VirtualDisk
|
val dom = message[1] as PartialDOM
|
||||||
|
|
||||||
|
val hasDup = messageQueue.fold(false) { acc, pair -> acc or (pair.first.path == file.path) }
|
||||||
|
if (!hasDup) {
|
||||||
|
messageQueue.add(file to dom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
object TevdPartialDomSyncWatchdog : VMWatchdog(120f) {
|
||||||
|
|
||||||
|
private val messageQueue = ArrayList<Pair<File, PartialDOM>>()
|
||||||
|
|
||||||
|
override fun consumeMessages() {
|
||||||
|
synchronized(this) {
|
||||||
|
messageQueue.forEach { (outfile, dom) ->
|
||||||
|
dom.sync()
|
||||||
|
println("[${this.javaClass.simpleName}] sync ${outfile.path}")
|
||||||
|
}
|
||||||
|
messageQueue.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addMessage(message: Array<Any?>) {
|
||||||
|
val file = message[0] as File
|
||||||
|
val dom = message[1] as PartialDOM
|
||||||
|
|
||||||
val hasDup = messageQueue.fold(false) { acc, pair -> acc or (pair.first.path == file.path) }
|
val hasDup = messageQueue.fold(false) { acc, pair -> acc or (pair.first.path == file.path) }
|
||||||
if (!hasDup) {
|
if (!hasDup) {
|
||||||
|
|||||||
@@ -47,9 +47,15 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var tevdSyncFilteringCounter = 0
|
||||||
fun notifyDiskCommit() {
|
fun notifyDiskCommit() {
|
||||||
vm.watchdogs["TEVD_SYNC"]?.addMessage(arrayOf(tevdPath, DOM))
|
vm.watchdogs["TEVD_COMMIT"]?.addMessage(arrayOf(tevdPath, DOM))
|
||||||
|
|
||||||
|
if (tevdSyncFilteringCounter >= 1) {
|
||||||
|
vm.watchdogs["TEVD_SYNC"]?.addMessage(arrayOf(tevdPath, DOM))
|
||||||
|
tevdSyncFilteringCounter = 0
|
||||||
|
}
|
||||||
|
tevdSyncFilteringCounter += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -98,12 +104,24 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
override fun writeoutImpl(inputData: ByteArray) {
|
override fun writeoutImpl(inputData: ByteArray) {
|
||||||
printdbg("inputString=${inputData.trimNull().toString(VM.CHARSET)}")
|
printdbg("inputString=${inputData.trimNull().toString(VM.CHARSET)}")
|
||||||
|
|
||||||
if (writeMode || appendMode) {
|
if ((writeMode || appendMode) && writeModeLength >= 0) {
|
||||||
|
printdbg("writeout with inputdata length of ${inputData.size}")
|
||||||
//println("[DiskDrive] writeout with inputdata length of ${inputData.size}")
|
//println("[DiskDrive] writeout with inputdata length of ${inputData.size}")
|
||||||
//println("[DiskDriveMsg] ${inputData.toString(Charsets.UTF_8)}")
|
//println("[DiskDriveMsg] ${inputData.toString(Charsets.UTF_8)}")
|
||||||
|
|
||||||
if (!fileOpen) throw InternalError("File is not open but the drive is in write mode")
|
if (!fileOpen) throw InternalError("File is not open but the drive is in write mode")
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
printdbg("File '${file.path}' not exists, creating new...")
|
||||||
|
val (result, failReason) = file.createNewFile()
|
||||||
|
if (failReason != null) {
|
||||||
|
throw failReason
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
printdbg("Operation successful")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
System.arraycopy(inputData, 0, writeBuffer, writeBufferUsage, minOf(writeModeLength - writeBufferUsage, inputData.size, BLOCK_SIZE))
|
System.arraycopy(inputData, 0, writeBuffer, writeBufferUsage, minOf(writeModeLength - writeBufferUsage, inputData.size, BLOCK_SIZE))
|
||||||
writeBufferUsage += inputData.size
|
writeBufferUsage += inputData.size
|
||||||
|
|
||||||
@@ -121,6 +139,8 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (fileOpenMode == 17) {
|
else if (fileOpenMode == 17) {
|
||||||
|
printdbg("File open mode 17")
|
||||||
|
|
||||||
if (!fileOpen) throw InternalError("Bootloader file is not open but the drive is in boot write mode")
|
if (!fileOpen) throw InternalError("Bootloader file is not open but the drive is in boot write mode")
|
||||||
|
|
||||||
val inputData = if (inputData.size != BLOCK_SIZE) ByteArray(BLOCK_SIZE) { if (it < inputData.size) inputData[it] else 0 }
|
val inputData = if (inputData.size != BLOCK_SIZE) ByteArray(BLOCK_SIZE) { if (it < inputData.size) inputData[it] else 0 }
|
||||||
@@ -140,6 +160,8 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
notifyDiskCommit()
|
notifyDiskCommit()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
printdbg("(cmd mode)")
|
||||||
|
|
||||||
val inputString = inputData.trimNull().toString(VM.CHARSET)
|
val inputString = inputData.trimNull().toString(VM.CHARSET)
|
||||||
|
|
||||||
if (inputString.startsWith("DEVRST\u0017")) {
|
if (inputString.startsWith("DEVRST\u0017")) {
|
||||||
@@ -222,7 +244,7 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val successful = file.delete()
|
val (successful, whyFailed) = file.delete()
|
||||||
if (!successful) {
|
if (!successful) {
|
||||||
statusCode.set(TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
statusCode.set(TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
||||||
return
|
return
|
||||||
@@ -381,7 +403,7 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val status = file.mkdir()
|
val (status, whyFailed) = file.mkdir()
|
||||||
statusCode.set(if (status) 0 else 1)
|
statusCode.set(if (status) 0 else 1)
|
||||||
if (status) {
|
if (status) {
|
||||||
printdbg("Notifying disk commit (mkdir)")
|
printdbg("Notifying disk commit (mkdir)")
|
||||||
@@ -402,7 +424,7 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val f1 = file.createNewFile()
|
val (f1, whyFailed) = file.createNewFile()
|
||||||
statusCode.set(if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
statusCode.set(if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
||||||
if (f1) {
|
if (f1) {
|
||||||
printdbg("Notifying disk commit (mkfile)")
|
printdbg("Notifying disk commit (mkfile)")
|
||||||
@@ -427,7 +449,7 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val f1 = file.setLastModified(vm.worldInterface.currentTimeInMills())
|
val (f1, whyFailed) = file.setLastModified(vm.worldInterface.currentTimeInMills())
|
||||||
statusCode.set(if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
statusCode.set(if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
||||||
if (f1) {
|
if (f1) {
|
||||||
printdbg("Notifying disk commit (touch)")
|
printdbg("Notifying disk commit (touch)")
|
||||||
@@ -452,11 +474,16 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
val f1 = file.createNewFile()
|
val (f1, whyFailed) = file.createNewFile()
|
||||||
statusCode.set(if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
statusCode.set(if (f1) TestDiskDrive.STATE_CODE_STANDBY else TestDiskDrive.STATE_CODE_OPERATION_FAILED)
|
||||||
if (!f1) { return }
|
if (!f1) { return }
|
||||||
}
|
}
|
||||||
|
// if (fileOpenMode == 1) { writeMode = true; appendMode = false }
|
||||||
|
// else if (fileOpenMode == 2) { writeMode = false; appendMode = true }
|
||||||
writeModeLength = inputString.substring(5, inputString.length).toInt()
|
writeModeLength = inputString.substring(5, inputString.length).toInt()
|
||||||
|
|
||||||
|
printdbg("WRITE issued with len $writeModeLength")
|
||||||
|
|
||||||
writeBuffer = ByteArray(writeModeLength)
|
writeBuffer = ByteArray(writeModeLength)
|
||||||
writeBufferUsage = 0
|
writeBufferUsage = 0
|
||||||
statusCode.set(TestDiskDrive.STATE_CODE_STANDBY)
|
statusCode.set(TestDiskDrive.STATE_CODE_STANDBY)
|
||||||
@@ -493,8 +520,10 @@ class TevdDiskDrive(private val vm: VM, private val driveNum: Int, private val t
|
|||||||
// DOM.entries.clear()
|
// DOM.entries.clear()
|
||||||
// DOM.diskName = newName.toByteArray(VM.CHARSET)
|
// DOM.diskName = newName.toByteArray(VM.CHARSET)
|
||||||
}
|
}
|
||||||
else
|
else {
|
||||||
|
printdbg("Illegal command: ${inputString}")
|
||||||
statusCode.set(TestDiskDrive.STATE_CODE_ILLEGAL_COMMAND)
|
statusCode.set(TestDiskDrive.STATE_CODE_ILLEGAL_COMMAND)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ import java.io.IOException
|
|||||||
*/
|
*/
|
||||||
class TevdFileDescriptor(val DOM: PartialDOM, _pathstr: String) {
|
class TevdFileDescriptor(val DOM: PartialDOM, _pathstr: String) {
|
||||||
|
|
||||||
val path = _pathstr.replace('\\', '/')
|
val path = _pathstr.replace('\\', '/').let {
|
||||||
|
var s = it.substring(0)
|
||||||
|
while (s.startsWith("/"))
|
||||||
|
s = s.substring(1)
|
||||||
|
s
|
||||||
|
}
|
||||||
val vdPath = VDUtil.VDPath(path, VM.CHARSET)
|
val vdPath = VDUtil.VDPath(path, VM.CHARSET)
|
||||||
|
|
||||||
val entryID: EntryID?
|
val entryID: EntryID?
|
||||||
@@ -84,16 +89,19 @@ class TevdFileDescriptor(val DOM: PartialDOM, _pathstr: String) {
|
|||||||
return (DOM.requestFile(path) != null)
|
return (DOM.requestFile(path) != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(): Boolean {
|
fun delete(): Pair<Boolean, Throwable?> {
|
||||||
return try {
|
return try {
|
||||||
val parentDir = vdPath.getParent().toString()
|
val parentDir = vdPath.getParent().toString()
|
||||||
DOM.removeFile(path)
|
DOM.removeFile(path)
|
||||||
DOM.requestFile(parentDir)!!.modificationDate = VDUtil.currentUnixtime
|
DOM.requestFile(parentDir)!!.let {
|
||||||
|
it.modificationDate = VDUtil.currentUnixtime
|
||||||
|
DOM.touchFile(it)
|
||||||
|
}
|
||||||
|
|
||||||
true
|
true to null
|
||||||
}
|
}
|
||||||
catch (e: KotlinNullPointerException) {
|
catch (e: KotlinNullPointerException) {
|
||||||
false
|
false to e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +127,7 @@ class TevdFileDescriptor(val DOM: PartialDOM, _pathstr: String) {
|
|||||||
return fileContent.getContent().toByteArray()
|
return fileContent.getContent().toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun mkdir(): Boolean {
|
fun mkdir(): Pair<Boolean, Throwable?> {
|
||||||
return try {
|
return try {
|
||||||
val parentDir = vdPath.getParent().toString()
|
val parentDir = vdPath.getParent().toString()
|
||||||
|
|
||||||
@@ -131,18 +139,19 @@ class TevdFileDescriptor(val DOM: PartialDOM, _pathstr: String) {
|
|||||||
val newDir = DiskEntry(newID, dir.entryID, nameBytes, newTime, newTime, EntryDirectory())
|
val newDir = DiskEntry(newID, dir.entryID, nameBytes, newTime, newTime, EntryDirectory())
|
||||||
|
|
||||||
DOM.addNewFile(newDir)
|
DOM.addNewFile(newDir)
|
||||||
|
DOM.touchFile(dir)
|
||||||
dirContent.add(newID)
|
dirContent.add(newID)
|
||||||
|
|
||||||
dir.modificationDate = newTime
|
dir.modificationDate = newTime
|
||||||
|
|
||||||
true
|
true to null
|
||||||
}
|
}
|
||||||
catch (e: KotlinNullPointerException) {
|
catch (e: KotlinNullPointerException) {
|
||||||
false
|
false to e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createNewFile(): Boolean {
|
fun createNewFile(): Pair<Boolean, Throwable?> {
|
||||||
val fileContent = EntryFile(ByteArray64())
|
val fileContent = EntryFile(ByteArray64())
|
||||||
val time_t = System.currentTimeMillis() / 1000
|
val time_t = System.currentTimeMillis() / 1000
|
||||||
val newFile = DiskEntry(-1, -1, nameBytes, time_t, time_t, fileContent)
|
val newFile = DiskEntry(-1, -1, nameBytes, time_t, time_t, fileContent)
|
||||||
@@ -159,24 +168,28 @@ class TevdFileDescriptor(val DOM: PartialDOM, _pathstr: String) {
|
|||||||
newFile.parentEntryID = dir.entryID
|
newFile.parentEntryID = dir.entryID
|
||||||
|
|
||||||
DOM.addNewFile(newFile)
|
DOM.addNewFile(newFile)
|
||||||
|
DOM.touchFile(dir)
|
||||||
dirContent.add(newID)
|
dirContent.add(newID)
|
||||||
|
|
||||||
dir.modificationDate = newTime
|
dir.modificationDate = newTime
|
||||||
|
|
||||||
true
|
true to null
|
||||||
}
|
}
|
||||||
catch (e: KotlinNullPointerException) {
|
catch (e: KotlinNullPointerException) {
|
||||||
false
|
false to e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setLastModified(newTime_t: Long): Boolean {
|
fun setLastModified(newTime_t: Long): Pair<Boolean, Throwable?> {
|
||||||
return try {
|
return try {
|
||||||
DOM.requestFile(path)!!.modificationDate = newTime_t
|
DOM.requestFile(path)!!.let {
|
||||||
true
|
it.modificationDate = newTime_t
|
||||||
|
DOM.touchFile(it)
|
||||||
|
}
|
||||||
|
true to null
|
||||||
}
|
}
|
||||||
catch (e: KotlinNullPointerException) {
|
catch (e: KotlinNullPointerException) {
|
||||||
false
|
false to e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,8 @@ package net.torvald.tsvm;
|
|||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3Application;
|
||||||
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
|
import com.badlogic.gdx.backends.lwjgl3.Lwjgl3ApplicationConfiguration;
|
||||||
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
|
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
|
||||||
import kotlin.Pair;
|
|
||||||
import kotlin.collections.CollectionsKt;
|
|
||||||
import net.torvald.tsvm.peripheral.*;
|
import net.torvald.tsvm.peripheral.*;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
|
||||||
public class AppLoader {
|
public class AppLoader {
|
||||||
@@ -32,7 +29,8 @@ public class AppLoader {
|
|||||||
appConfig.setWindowedMode(WIDTH, HEIGHT);
|
appConfig.setWindowedMode(WIDTH, HEIGHT);
|
||||||
|
|
||||||
HashMap<String, VMWatchdog> watchdogs = new HashMap<>();
|
HashMap<String, VMWatchdog> watchdogs = new HashMap<>();
|
||||||
watchdogs.put("TEVD_SYNC", TevdSyncWatchdog.INSTANCE);
|
watchdogs.put("TEVD_COMMIT", TevdPartialDomCommitWatchdog.INSTANCE);
|
||||||
|
watchdogs.put("TEVD_SYNC", TevdPartialDomSyncWatchdog.INSTANCE);
|
||||||
|
|
||||||
|
|
||||||
String diskPath = "assets/disk0";
|
String diskPath = "assets/disk0";
|
||||||
|
|||||||
@@ -65,9 +65,13 @@ class VMEmuExecutableWrapper(val windowWidth: Int, val windowHeight: Int, var pa
|
|||||||
*/
|
*/
|
||||||
class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: Int, var panelsY: Int, val diskPathRoot: String) : ApplicationAdapter() {
|
class VMEmuExecutable(val windowWidth: Int, val windowHeight: Int, var panelsX: Int, var panelsY: Int, val diskPathRoot: String) : ApplicationAdapter() {
|
||||||
|
|
||||||
val TEVD_SYNC = TevdSyncWatchdog
|
val TEVD_COMMIT = TevdPartialDomCommitWatchdog
|
||||||
|
val TEVD_SYNC = TevdPartialDomCommitWatchdog
|
||||||
|
|
||||||
val watchdogs = hashMapOf<String, VMWatchdog>("TEVD_SYNC" to TEVD_SYNC)
|
val watchdogs = hashMapOf<String, VMWatchdog>(
|
||||||
|
"TEVD_COMMIT" to TEVD_COMMIT,
|
||||||
|
"TEVD_SYNC" to TEVD_SYNC
|
||||||
|
)
|
||||||
|
|
||||||
data class VMRunnerInfo(val vm: VM, val profileName: String)
|
data class VMRunnerInfo(val vm: VM, val profileName: String)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user