OC-KittenOS/repository/apps/app-metamachine.lua

611 lines
16 KiB
Lua

-- 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