-- Copyright (C) 2018-2021 by KittenOS NEO contributors -- -- Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. -- -- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF -- THIS SOFTWARE. -- app-metamachine.lua : Virtual Machine -- Authors: 20kdc local loaderPkg, loaderPid, vmName = ... local icecap = neo.requireAccess("x.neo.pub.base", "fs") local libVGPU = require("metamachine-vgpu") local vmBaseCoroutineWrap local vmComponent, vmComputer, vmOs local vmEnvironment local vmSelfdestruct = false local vmSuperVM = true local signalStack = {} local postVMRList = {} -- File structure: -- vm-* : Virtual machine configuration -- vm- local vmConfiguration = { -- true : Physical -- {type, ...} : Virtual -- NOTE : The following rules are set. -- k-computer always exists -- k-gpu always exists -- k-log always exists -- k-tmpfs always exists in non-Super VMs ["world"] = {"filesystem", "/", false}, ["eeprom"] = {"eeprom", "/confboot.lua", "/confdata.bin", "Configurator", true}, ["screen"] = {"screen", "configurator", 50, 15, 8} } if vmName then neo.ensurePathComponent("vm-" .. vmName) vmSuperVM = false local f = icecap.open("/vm-" .. vmName, false) vmConfiguration = require("serial").deserialize(f.read("*a")) f.close() if not vmConfiguration then error("The VM configuration was unloadable.") end vmConfiguration["k-tmpfs"] = {"filesystem", "/vt-" .. vmName .. "/", false} end local function clone(t) if type(t) == "table" then local b = {} for k, v in pairs(t) do b[k] = v end return b end return t end -- by window ID = {address, internal} local screensInt = { } -- by component address = callback local screensAll = { } local tmpAddress = "k-tmpfs" local passthroughs = {} local fakeArch = _VERSION local components = { ["k-computer"] = { type = "computer", beep = function () end, start = function () return false end, stop = function () vmSelfdestruct = true coroutine.yield(0.05) end, isRunning = function () return true end, getProgramLocations = function () -- Entries of {"file", "lootdisk"} return {} end, getDeviceInfo = function () return { ["k-computer"] = { ["class"] = "system", ["description"] = "Computer", ["product"] = "Freeziflow Liquid-Cooling Unit", ["vendor"] = "KDC Subsystems", ["capacity"] = "10", ["width"] = "", ["clock"] = "" }, ["k-processor"] = { ["class"] = "processor", ["description"] = "CPU", ["product"] = "Celesti4 Quantum Computing System", ["vendor"] = "KDC Subsystems", ["capacity"] = "", ["width"] = "", ["clock"] = "9000" }, ["k-memory"] = { ["class"] = "memory", ["description"] = "Memory bank", ["product"] = "Lun4 Paging Subsystem", ["vendor"] = "KDC Subsystems", ["capacity"] = "", ["width"] = "", ["clock"] = "9000" }, ["k-gpu"] = { ["class"] = "display", ["description"] = "Graphics controller", ["product"] = "Tw1-l GPU Multiplexer", ["vendor"] = "KDC Subsystems", ["capacity"] = "8000", ["width"] = "8", ["clock"] = "9000" } } end, getArchitectures = function () return {fakeArch} end, setArchitecture = function (a) fakeArch = a end, getArchitecture = function () return fakeArch end }, ["k-gpu"] = libVGPU.newGPU(screensAll), ["k-log"] = { type = "ocemu", log = neo.emergency } } -- Clones of components made on-demand. local proxies = {} setmetatable(proxies, {__mode = "v"}) vmComponent = { list = function (filter, exact) -- This is an iterator :( local t = {} local tk = {} for k, v in pairs(components) do local ok = false if filter then if v.type == filter or ((not exact) and v.type:match(filter, 1, true)) then ok = true end else ok = true end if ok then table.insert(t, {k, v.type}) tk[k] = v.type end end setmetatable(tk, { __call = function () local tr1 = table.remove(t, 1) if not tr1 then return end return table.unpack(tr1) end }) return tk end, invoke = function (com, me, ...) if not components[com] then error("no such component " .. com) end if not components[com][me] then error("no such method " .. com .. "." .. me) end return components[com][me](...) end, proxy = function (com) if not components[com] then return nil, "no such component" end local p = proxies[com] if p then return p end p = clone(components[com]) p.address = com p.fields = {} p.slot = 0 proxies[com] = p return p end, type = function (com) if not components[com] then return nil, "no such component" end return components[com].type end, methods = function (com) if not components[com] then return nil, "no such component" end local mt = {} for k, v in pairs(components[com]) do if type(v) == "function" then mt[k] = true end end return mt end, fields = function (com) -- This isn't actually supported, -- because fields are bad-sec nonsense. -- Luckily, everybody knows this, so nobody uses them. return {} end, doc = function (address, method) if not components[address] then error("no such component") end if not components[address][method] then return end return tostring(components[address][method]) end } -- Prepare configured components local insertionCallbacks = { ["screen"] = function (address, title, w, h, d) local activeLines = {} local scrW = neo.requireAccess("x.neo.pub.window", "primary window")(w, h, title) local gpuC, scrI, scrC gpuC, scrI, scrC = libVGPU.newBuffer(scrW, {address .. "-kb"}, w, h, function (nw, nh) table.insert(signalStack, {"screen_resized", address, nw, nh}) end, function (l) if activeLines[l] then return end activeLines[l] = true table.insert(postVMRList, function () scrI.line(l) activeLines = {} end) end) components[address] = scrC components[address .. "-kb"] = {type = "keyboard"} screensInt[scrW.id] = {address, scrI} screensAll[address] = gpuC end, ["eeprom"] = function (address, boot, data, name, ro) local codeSize = 4096 local dataSize = 256 local function getCore(fd) local f = icecap.open(fd, false) if not f then return "" end local contents = f.read("*a") f.close() return contents end local function setCore(fd, size, contents, important) checkArg(1, contents, "string") if #contents > size then return nil, "too large" end if ro and important then return nil, "storage is readonly" end local f = icecap.open(fd, true) if not f then return nil, "storage is readonly" end f.write(contents) f.close() return true end components[address] = { type = "eeprom", get = function () return getCore(boot) end, set = function (contents) return setCore(boot, codeSize, contents, true) end, makeReadonly = function () ro = true return true end, getChecksum = function () return "00000000" end, getSize = function () return codeSize end, getDataSize = function () return dataSize end, getData = function () return getCore(data) end, setData = function (contents) return setCore(data, dataSize, contents, false) end } end, ["filesystem"] = function (address, path, ro) components[address] = require("metamachine-vfs")(icecap, address, path, ro) end } for k, v in pairs(vmConfiguration) do if type(v) == "string" then local root = neo.requireAccess("k.root", "component passthrough") local ty = root.component.type(k) if ty then passthroughs[k] = true components[k] = root.component.proxy(k) if ty == "screen" then -- Need to ensure the screen in question is for the taking local div = neo.requireAccess("x.neo.sys.session", "ability to divorce screens") div.disclaimMonitor(k) local div2 = neo.requireAccess("x.neo.sys.screens", "ability to claim screens") screensAll[k] = div2.claim(k) assert(screensAll[k], "Hardware screen " .. k .. " unavailable.") end end else assert(insertionCallbacks[v[1]], "Cannot insert virtual " .. v[1]) insertionCallbacks[v[1]](k, table.unpack(v, 2)) end end vmOs = clone(os) vmComputer = {} vmComputer.shutdown = function (...) vmSelfdestruct = true coroutine.yield(0.05) end vmComputer.pushSignal = function (...) table.insert(signalStack, {...}) end vmComputer.pullSignal = function (time) if not signalStack[1] then if type(time) == "number" then time = time + os.uptime() coroutine.yield(time) if not signalStack[1] then return end else while not signalStack[1] do coroutine.yield(math.huge) end end end return table.unpack(table.remove(signalStack, 1)) end vmComputer.totalMemory = os.totalMemory vmOs.totalMemory = nil vmComputer.freeMemory = os.freeMemory vmOs.freeMemory = nil vmComputer.energy = os.energy vmOs.energy = nil vmComputer.maxEnergy = os.maxEnergy vmOs.maxEnergy = nil local startupUptime = os.uptime() vmComputer.uptime = function () return os.uptime() - startupUptime end vmOs.uptime = nil vmComputer.address = os.address vmOs.address = nil vmComputer.isRobot = function () return false end vmComputer.address = function () return "k-computer" end vmComputer.tmpAddress = function () return tmpAddress end local eepromAddress = "k-eeprom" vmComputer.getBootAddress = function () return eepromAddress end vmComputer.setBootAddress = function (a) eepromAddress = a end vmComputer.users = function () return {} end vmComputer.addUser = function () return false, "user support not available" end vmComputer.removeUser = function () return false, "user support not available" end vmComputer.beep = function (...) return vmComponent.invoke("k-computer", "beep", ...) end vmComputer.getDeviceInfo = function (...) return vmComponent.invoke("k-computer", "getDeviceInfo", ...) end vmComputer.getProgramLocations = function (...) return vmComponent.invoke("k-computer", "getProgramLocations", ...) end vmComputer.getArchitectures = function (...) return vmComponent.invoke("k-computer", "getArchitectures", ...) end vmComputer.getArchitecture = function (...) return vmComponent.invoke("k-computer", "getArchitecture", ...) end vmComputer.setArchitecture = function (...) return vmComponent.invoke("k-computer", "setArchitecture", ...) end vmUnicode = clone(unicode) vmUnicode.safeTextSupport = nil vmUnicode.undoSafeTextSupport = nil vmEnvironment = { _VERSION = _VERSION, component = vmComponent, computer = vmComputer, table = clone(table), math = clone(math), string = clone(string), unicode = vmUnicode, -- Scheme here: -- A yield's first argument is nil for an actual yield, -- or the time to add a timer at (math.huge if no timeout) for a pullSignal. -- This is not exactly the same, but is very similar, to that of machine.lua, -- differing mainly in how pullSignal timeout scheduling occurs. coroutine = { -- NOTE: I can't give you a definitive list from OC because OC (or OpenOS?) screws up a metatable! yield = function (...) return coroutine.yield(nil, ...) end, -- The way this is defined by machine.lua makes it true even when it arguably shouldn't be. Oh well. isyieldable = coroutine.isyieldable, status = coroutine.status, create = function (f) return coroutine.create(function (...) return nil, f(...) end) end, running = coroutine.running, wrap = function (f) local pf = coroutine.wrap(function (...) return nil, f(...) end) return function (...) local last = {...} while true do local tabpack = {pf(table.unpack(last))} if not tabpack[1] then return table.unpack(tabpack, 2) end last = {coroutine.yield(tabpack[1])} end end end, resume = function (co, ...) local last = {...} while true do local tabpack = {coroutine.resume(co, table.unpack(last))} if not tabpack[1] then neo.emergency(co, table.unpack(tabpack)) return table.unpack(tabpack) elseif not tabpack[2] then return tabpack[1], table.unpack(tabpack, 3) end last = {coroutine.yield(tabpack[2])} end end }, os = vmOs, debug = clone(debug), bit32 = clone(bit32), utf8 = clone(utf8), assert = assert, ipairs = ipairs, next = next, load = function (a, b, c, d) if rawequal(d, nil) then d = vmEnvironment end return load(a, b, c, d) end, pairs = pairs, pcall = function (...) local r = {pcall(...)} if not r[1] then neo.emergency("pcall error:", table.unpack(r, 2)) end return table.unpack(r) end, xpcall = xpcall, select = select, type = type, error = error, tonumber = tonumber, tostring = tostring, setmetatable = setmetatable, getmetatable = getmetatable, rawset = rawset, rawget = rawget, rawlen = rawlen, rawequal = rawequal, checkArg = checkArg } vmEnvironment._G = vmEnvironment if vmSuperVM then vmEnvironment._MMstartVM = function (vmName) neo.executeAsync("app-metamachine", vmName) end vmEnvironment._MMserial = function (...) return require("serial").serialize(...) end vmEnvironment._MMdeserial = function (...) return require("serial").deserialize(...) end vmEnvironment.print = neo.emergency local root = neo.requestAccess("k.root") if root then vmEnvironment._MMcomList = root.component.list else vmEnvironment._MMcomList = function () return function () end end end end -- bootstrap vmBaseCoroutineWrap = coroutine.wrap(function () vmBaseCoroutine = coroutine.running() eepromAddress = vmComponent.list("eeprom")() if not eepromAddress then error("No EEPROM") end local code = vmComponent.invoke(eepromAddress, "get") local res, f = load(code, "=eeprom", "t", vmEnvironment) if not res then error(f) else res() end end) while ((not vmBaseCoroutine) or (coroutine.status(vmBaseCoroutine) ~= "dead")) and not vmSelfdestruct do local details = {vmBaseCoroutineWrap()} while postVMRList[1] do table.remove(postVMRList, 1)() end if details[1] then local checkTimer = nil if details[1] ~= math.huge then checkTimer = neo.scheduleTimer(details[1]) --neo.emergency("metamachine timer " .. details[1]) else --neo.emergency("metamachine HANG") end while true do local ev = {coroutine.yield()} if ev[1] == "k.timer" then if ev[2] == checkTimer then break end elseif ev[1]:sub(1, 2) == "h." then if passthroughs[ev[2]] then ev[1] = ev[1]:sub(3) table.insert(signalStack, ev) break end elseif ev[1] == "x.neo.pub.window" then local id = ev[2] if ev[3] == "key" then if ev[6] then table.insert(signalStack, {"key_down", screensInt[id][1] .. "-kb", ev[4], ev[5], "neo"}) else table.insert(signalStack, {"key_up", screensInt[id][1] .. "-kb", ev[4], ev[5], "neo"}) end break elseif ev[3] == "line" then screensInt[id][2].line(ev[4]) elseif ev[3] == "clipboard" then table.insert(signalStack, {ev[3], screensInt[id][1] .. "-kb", ev[4], "neo"}) break elseif ev[3] == "touch" or ev[3] == "drag" or ev[3] == "drop" or ev[3] == "scroll" then local x = ev[4] local y = ev[5] if screensInt[id][2].precise then x = (x - 1) + ev[6] y = (y - 1) + ev[7] end table.insert(signalStack, {ev[3], screensInt[id][1], x, y, ev[8], "neo"}) break elseif ev[3] == "close" then return end end end else error("Yield in root coroutine") end end