-- This is released into the public domain. -- No warranty is provided, implied or otherwise. -- s-donkonit : config, shutdown, screens local donkonitSPProvider = neo.requireAccess("r.neo.sys.manage", "creating NEO core APIs") -- Restrict to s- -- Doesn't matter what calls this service, because there's a mutex here. local donkonitRDProvider = neo.requireAccess("r.neo.sys.screens", "creating NEO core APIs") local glacierDCProvider = neo.requireAccess("r.neo.pub.globals", "creating NEO core APIs") local computer = neo.requireAccess("k.computer", "shutting down") local fs = neo.requireAccess("c.filesystem", "settings I/O") local gpus = neo.requireAccess("c.gpu", "screen control") local screens = neo.requireAccess("c.screen", "screen control") neo.requireAccess("s.h.component_added", "HW management") neo.requireAccess("s.h.component_removed", "HW management") neo.requireAccess("s.h.key_down", "Keymap guesswork") local function shutdownFin(reboot) -- any final actions donkonit needs to take here computer.shutdown(reboot) end -- keys are pids local targs = {} -- settings notify targs local targsDC = {} -- displaycontrol settings notify targs local targsSD = {} -- shutdown notify targs local targsST = {} -- saving throws local targsRD = {} -- pid->{sendSig,dead} -- screen address -> {gpu, monitor} local monitorMap = {} local shuttingDown = false local shutdownMode = false -- needs improvements local settings = { -- The list of settings is here: -- password password = "", ["pub.clipboard"] = "", ["sys-init.shell"] = "sys-everest", ["run.sys-icecap"] = "yes", -- scr.w/h/d. } local function loadSettings() pcall(function () local fw = require("sys-filewrap") local se = require("serial") local st = fw(fs.primary, "data/sys-glacier/sysconf.lua", false) local cfg = st.read("*a") st.close() st = nil fw = nil cfg = se.deserialize(cfg) for k, v in pairs(cfg) do if type(k) == "string" then if type(v) == "string" then settings[k] = v end end end end) end local function saveSettings() local fw = require("sys-filewrap") local se = require("serial") fs.primary.makeDirectory("data/sys-glacier") local st = fw(fs.primary, "data/sys-glacier/sysconf.lua", true) st.write(se.serialize(settings)) st.close() end -- [i] = screenProxy local monitorPool = {} -- [screenAddr] = {gpu, claimedLoseCallback} local monitorClaims = {} -- [gpuAddr] = monitorAddr local currentGPUBinding = {} -- [gpuAddr] = userCount local currentGPUUsers = {} local function announceFreeMonitor(address, except) for k, v in pairs(targsRD) do if k ~= except then v[1]("available", address) end end end local function getGPU(monitor) local bestG local bestD = 0 for v in gpus.list() do v.bind(monitor.address, false) currentGPUBinding[v.address] = monitor.address local d = v.maxDepth() if d > bestD then bestG = v bestD = d bestU = currentGPUUsers[v.address] or 0 elseif d == bestD then if (currentGPUUsers[v.address] or 0) < bestU then bestG = v bestD = d bestU = currentGPUUsers[v.address] or 0 end end end return bestG end local function sRattle(name, val) for _, v in pairs(targs) do v("set_setting", name, val) end if name:sub(1, 4) == "scr." or name:sub(1, 4) == "pub." then for k, v in pairs(targsDC) do if not targs[k] then v("set_setting", name, val) end end end end -- Settings integration w/ monitors local function getMonitorSettings(a) local w = tonumber(settings["scr.w." .. a]) or 80 local h = tonumber(settings["scr.h." .. a]) or 25 local d = tonumber(settings["scr.d." .. a]) or 8 w, h, d = math.floor(w), math.floor(h), math.floor(d) return w, h, d end local function setupMonitor(gpu, monitor) gpu.bind(monitor.address, false) currentGPUBinding[gpu.address] = monitor.address local maxW, maxH = gpu.maxResolution() local maxD = gpu.maxDepth() local w, h, d = getMonitorSettings(monitor.address) w, h, d = math.min(w, maxW), math.min(h, maxH), math.min(d, maxD) settings["scr.w." .. monitor.address] = tostring(w) settings["scr.h." .. monitor.address] = tostring(h) settings["scr.d." .. monitor.address] = tostring(d) sRattle("scr.w." .. monitor.address, tostring(w)) sRattle("scr.h." .. monitor.address, tostring(h)) sRattle("scr.d." .. monitor.address, tostring(d)) gpu.setResolution(w, h) gpu.setDepth(d) pcall(saveSettings) end donkonitSPProvider(function (pkg, pid, sendSig) targs[pid] = sendSig return { listSettings = function () local s = {} for k, v in pairs(settings) do table.insert(s, k) end return s end, -- NOTE: REPLICATED IN GB getSetting = function (name) if type(name) ~= "string" then error("Setting name must be string") end return settings[name] end, delSetting = function (name) if type(name) ~= "string" then error("Setting name must be string") end local val = nil if name == "password" or name == "pub.clipboard" then val = "" end settings[name] = val sRattle(name, val) pcall(saveSettings) end, setSetting = function (name, val) if type(name) ~= "string" then error("Setting name must be string") end if type(val) ~= "string" then error("Setting value must be string") end settings[name] = val -- NOTE: Either a monitor is under application control, -- or it's not under any control. -- Monitor settings are applied on the transition to control. sRattle(name, val) pcall(saveSettings) end, -- registerForShutdownEvent = function () targsSD[pid] = sendSig end, registerSavingThrow = function (st) if type(st) ~= "function" then error("Saving throw function must be a function") end targsST[pid] = st end, shutdown = function (reboot) if type(reboot) ~= "boolean" then error("Shutdown parameter must be a boolean (reboot)") end if shuttingDown then return end shuttingDown = true shutdownMode = reboot local counter = 0 neo.scheduleTimer(os.uptime() + 5) -- in case the upcoming code fails in some way for f, v in pairs(targsSD) do counter = counter + 1 v("shutdown", reboot, function () counter = counter - 1 if counter == 0 then shutdownFin(reboot) end end) end if counter == 0 then shutdownFin(reboot) end -- donkonit will shutdown when the timer is hit. end } end) donkonitRDProvider(function (pkg, pid, sendSig) local claimed = {} targsRD[pid] = {sendSig, function () for k, v in pairs(claimed) do -- Nothing to really do here v(false) end end} return { getClaimable = function () local c = {} -- do we have gpu? if not gpus.list()() then return c end for _, v in ipairs(monitorPool) do table.insert(c, v.address) end return c end, claim = function (address) if type(address) ~= "string" then error("Address must be string.") end for k, v in ipairs(monitorPool) do if v.address == address then local gpu = getGPU(v) if gpu then setupMonitor(gpu, v) gpu = gpu.address currentGPUBinding[gpu] = address currentGPUUsers[gpu] = (currentGPUUsers[gpu] or 0) + 1 local disclaimer = function (wasDevLoss) -- we lost it monitorClaims[address] = nil claimed[address] = nil if not wasDevLoss then currentGPUUsers[gpu] = currentGPUUsers[gpu] - 1 table.insert(monitorPool, v) announceFreeMonitor(address, pid) else sendSig("lost", address) end end claimed[address] = disclaimer monitorClaims[address] = {gpu, disclaimer} table.remove(monitorPool, k) return function () for v in gpus.list() do if v.address == gpu then if currentGPUBinding[gpu] ~= address then v.bind(address, false) end currentGPUBinding[gpu] = address return v end end end end end end end, disclaim = function (address) if not address then error("Cannot disclaim nothing.") end if claimed[address] then claimed[address](false) end end } end) -- -- The actual initialization loadSettings() local function rescanDevs() monitorPool = {} currentGPUBinding = {} currentGPUUsers = {} local hasGPU = gpus.list()() for k, v in pairs(monitorClaims) do v[2](true) end monitorClaims = {} for m in screens.list() do table.insert(monitorPool, m) if hasGPU then announceFreeMonitor(m.address) end end end rescanDevs() -- Save any settings made during the above (or just the language) saveSettings() -- -- glacierDCProvider(function (pkg, pid, sendSig) targsDC[pid] = sendSig return { getKnownMonitors = function () local tbl = {} -- yes, this should work fine so long as GMS is the *last* one #luaquirks for k, v in ipairs(monitorPool) do tbl[k] = {v.address, false, getMonitorSettings(v.address)} end for k, v in pairs(monitorClaims) do table.insert(tbl, {k, true, getMonitorSettings(v.address)}) end end, changeMonitorSetup = function (ma, w, h, d) neo.ensureType(ma, "string") neo.ensureType(w, "number") neo.ensureType(h, "number") neo.ensureType(d, "number") w = math.floor(w) h = math.floor(h) d = math.floor(d) if w < 1 then error("Invalid width") end if h < 1 then error("Invalid height") end if d < 1 then error("Invalid depth") end w, h, d = tostring(w), tostring(h), tostring(d) settings["scr.w." .. ma] = w settings["scr.h." .. ma] = h settings["scr.d." .. ma] = d sRattle("scr.w." .. ma, w) sRattle("scr.h." .. ma, h) sRattle("scr.d." .. ma, d) pcall(saveSettings) end, forceRescan = rescanDevs, -- NOTE: "pub." prefixed version of functions in sys.manage getSetting = function (name) if type(name) ~= "string" then error("Setting name must be string") end return settings["pub." .. name] end, delSetting = function (name) if type(name) ~= "string" then error("Setting name must be string") end local val = nil if name == "clipboard" then val = "" end settings["pub." .. name] = val sRattle("pub." .. name, val) pcall(saveSettings) end, setSetting = function (name, val) if type(name) ~= "string" then error("Setting name must be string") end if type(val) ~= "string" then error("Setting value must be string") end settings["pub." .. name] = val sRattle("pub." .. name, val) pcall(saveSettings) end } end) -- main loop while true do local s = {coroutine.yield()} if s[1] == "k.timer" then -- always shutdown shutdownFin(shutdownMode) end if s[1] == "h.component_added" then -- Before doing anything, is it worth it? if s[3] == "gpu" or s[3] == "screen" then rescanDevs() end end if s[1] == "h.component_removed" then if s[3] == "gpu" or s[3] == "screen" then rescanDevs() end end if s[1] == "k.procdie" then targs[s[3]] = nil targsDC[s[3]] = nil targsSD[s[3]] = nil if targsST[s[3]] then if s[4] then pcall(targsST[s[3]]) end end targsST[s[3]] = nil if targsRD[s[3]] then targsRD[s[3]][2]() end end end