added seperate input and working on http / sockets

This commit is contained in:
2026-05-27 17:01:26 -04:00
parent 59e09a5995
commit f7b64c11b7
43 changed files with 5035 additions and 533 deletions

View File

@@ -0,0 +1,201 @@
--:Minify:--
local http = {}
local syscall = syscall
local function parseUrl(url)
local proto, host, path =
url:match(
"^(https?://)([^/]+)(/.*)$"
)
if not proto then
proto, host =
url:match(
"^(https?://)([^/]+)$"
)
path = "/"
end
if not proto then
return nil, "EINVAL"
end
return host, path
end
local function buildRequest(req)
local host, path =
parseUrl(req.url)
if not host then
return nil, path
end
local method =
req.method or "GET"
local body =
req.body or ""
local headers =
table.deepcopy(
req.headers or {}
)
headers.Host =
headers.Host or host
headers.Connection =
headers.Connection or "close"
if body ~= "" then
headers["Content-Length"] =
tostring(#body)
end
local out = {
method ..
" " ..
path ..
" HTTP/1.1"
}
for k, v in pairs(headers) do
out[#out + 1] =
tostring(k) ..
": " ..
tostring(v)
end
out[#out + 1] = ""
out[#out + 1] = body
return table.concat(out, "\r\n")
end
local function parseResponse(raw)
local headerEnd =
raw:find(
"\r\n\r\n",
1,
true
)
if not headerEnd then
return nil, "EBADMSG"
end
local headerPart =
raw:sub(1, headerEnd - 1)
local body =
raw:sub(headerEnd + 4)
local lines = {}
for line in headerPart:gmatch("[^\r\n]+") do
lines[#lines + 1] = line
end
local _, code, msg =
lines[1]:match(
"^(HTTP/%S+)%s+(%d+)%s*(.*)$"
)
local headers = {}
for i = 2, #lines do
local k, v =
lines[i]:match(
"^([^:]+):%s*(.*)$"
)
if k then
headers[k:lower()] = v
end
end
return {
code = tonumber(code),
message = msg,
headers = headers,
body = body
}
end
local function readAll(fd)
local out = ""
while true do
local chunk =
syscall.read(fd, 4096)
if not chunk or chunk == "" then
break
end
out = out .. chunk
end
return out
end
function http.request(req)
local raw, err =
buildRequest(req)
if not raw then
return nil, err
end
local fd =
syscall.socket(0, 0)
if not fd then
return nil, "ESOCKET"
end
local ok, err =
syscall.connect(fd, req.url)
if not ok then
syscall.close(fd)
return nil, err
end
local ok2, err2 =
syscall.write(fd, raw)
if not ok2 then
syscall.close(fd)
return nil, err2
end
local resp =
readAll(fd)
syscall.close(fd)
return parseResponse(resp)
end
function http.get(url, headers)
return http.request({
url = url,
method = "GET",
headers = headers
})
end
function http.post(url, body, headers)
return http.request({
url = url,
method = "POST",
body = body,
headers = headers
})
end
return http

View File

@@ -258,7 +258,7 @@ local function refresh()
end
end
end
local function iter()
refresh()
local combined = {}

View File

@@ -1 +1,306 @@
--:Minify:--
local kernel = ...
local handler = {}
local http = kernel.apis.http
kernel.cct.httpqueue = kernel.cct.httpqueue or {}
kernel.cct.httpresponse = kernel.cct.httpresponse or {}
kernel.cct.httperror = kernel.cct.httperror or {}
local function parseRawRequest(raw)
local sepLen = 4
local headerEnd =
raw:find("\r\n\r\n", 1, true)
if not headerEnd then
headerEnd =
raw:find("\n\n", 1, true)
sepLen = 2
end
local headerPart
local body = ""
if headerEnd then
headerPart = raw:sub(1, headerEnd - 1)
body = raw:sub(headerEnd + sepLen)
else
headerPart = raw
end
local lines = {}
for line in headerPart:gmatch("[^\r\n]+") do
lines[#lines + 1] = line
end
if #lines == 0 then
return nil, "EINVAL"
end
local method, path =
lines[1]:match("^(%S+)%s+(%S+)")
if not method or not path then
return nil, "EBADMSG"
end
local headers = {}
for i = 2, #lines do
local k, v =
lines[i]:match("^([^:]+):%s*(.*)$")
if k then
headers[k] = v
end
end
local host = headers.Host or headers.host
if not host and not path:match("^https?://") then
return nil, "EHOSTUNREACH"
end
local url
if path:match("^https?://") then
url = path
else
url = "http://" .. host .. path
end
local req = {
url = url,
method = method,
headers = headers
}
if body ~= "" then
req.body = body
end
return req
end
local function buildResponse(resp)
if not resp then
return nil, "EINVAL"
end
local code, msg = resp.getResponseCode()
local headers =
resp:getResponseHeaders()
local body =
resp:readAll() or ""
local out = {
"HTTP/1.1 " ..
tostring(code) ..
" " ..
tostring(msg)
}
local hasLength = false
for k, v in pairs(headers or {}) do
if k:lower() == "content-length" then
hasLength = true
end
out[#out + 1] =
tostring(k) ..
": " ..
tostring(v)
end
if not hasLength then
out[#out + 1] =
"Content-Length: " ..
tostring(#body)
end
out[#out + 1] = ""
out[#out + 1] = body
return table.concat(out, "\r\n")
end
function handler.connect(fd, address)
local fdo = kernel.currentTask.fd[fd]
if not fdo then
return nil, "EBADF"
end
fdo.socket.rbuf = ""
fdo.socket.closed = false
fdo.socket.httpid = nil
fdo.handle.write = function(raw)
local req, err =
parseRawRequest(raw)
if not req then
return nil, err
end
local id =
tostring(kernel.uuid())
fdo.socket.httpid = id
kernel.cct.httpqueue[id] = true
local ok, err =
http.request(req, id)
if not ok then
kernel.cct.httpqueue[id] = nil
return nil, err
end
return true
end
fdo.handle.read = function(count)
count = count or 4096
local sock = fdo.socket
if #sock.rbuf > 0 then
local out =
sock.rbuf:sub(1, count)
sock.rbuf =
sock.rbuf:sub(count + 1)
return out
end
local id = sock.httpid
if not id then
return ""
end
local function finish(resp)
sock.rbuf =
buildResponse(resp) or ""
local out =
sock.rbuf:sub(1, count)
sock.rbuf =
sock.rbuf:sub(count + 1)
return out
end
if kernel.cct.httpresponse[id] then
local resp =
kernel.cct.httpresponse[id]
kernel.cct.httpresponse[id] = nil
kernel.cct.httpqueue[id] = nil
return finish(resp)
end
if kernel.cct.httperror[id] then
local err =
kernel.cct.httperror[id]
kernel.cct.httperror[id] = nil
kernel.cct.httpqueue[id] = nil
return nil, err
end
kernel.currentTask.status = "D"
local coro
coro = function()
if kernel.cct.httpresponse[id] then
local resp =
kernel.cct.httpresponse[id]
kernel.cct.httpresponse[id] = nil
kernel.cct.httpqueue[id] = nil
kernel.asyncReturn(
finish(resp)
)
return
end
if kernel.cct.httperror[id] then
local err =
kernel.cct.httperror[id]
kernel.cct.httperror[id] = nil
kernel.cct.httpqueue[id] = nil
kernel.asyncReturn(
nil,
err
)
return
end
coroutine.yield()
end
kernel.currentTask.ksh =
coroutine.create(function()
local ok, err =
xpcall(
coro,
debug.traceback
)
if not ok then
kernel.asyncReturn(
nil,
err
)
end
end)
end
fdo.handle.close = function()
fdo.socket.closed = true
local id =
fdo.socket.httpid
if id then
kernel.cct.httpqueue[id] = nil
kernel.cct.httpresponse[id] = nil
kernel.cct.httperror[id] = nil
end
return true
end
return true
end
kernel.socket.registerProtocal(
"http://",
handler
)
kernel.socket.registerProtocal(
"https://",
handler
)

View File

@@ -223,6 +223,9 @@ local function newtty(obj, id, ev)
gctrl=function()
return serializeBool(kernel.cct.ctrl)..";"..serializeBool(kernel.cct.alt)
end,
isvirt=function()
return false
end,
gplt=function()
return plt
end
@@ -243,8 +246,39 @@ end
local fifo = kernel.newFifo()
kernel.cct.fifo=fifo
newtty(kernel.apis.term, "1", fifo.pop)
newtty(kernel.apis.term, "1", function() end)
for i,v in ipairs({peripheral.find("monitor")}) do
newtty(v,tostring(i+1),function () end)
end
kernel.devfs.data["input"]["keyboard1"] = function(op, mode)
if op=="type" then
return "Keyboard"
elseif op=="open" then
local h = {
read=function(amount)
local rv=""
for i=1, amount or 1 do
local event = {fifo.pop()}
if event[1] then
rv=rv..event[1]
end
end
if rv=="" then rv=nil end
return rv
end,
write=function(content)
end
}
if mode=="rw" then
return h
elseif mode=="r" then
h["write"]=nil
return h
elseif mode=="w" then
h["read"]=nil
return h
end
end
end

View File

@@ -4,6 +4,7 @@ local keys=kernel.apis.keys
kernel.processes.cctdeamon = function()
local timeout = false
kernel.log("CCT deamon started")
while true do
local event = {kernel.EFI:getMachineEvent()}
@@ -66,6 +67,12 @@ kernel.processes.cctdeamon = function()
elseif eventType == "keyTyped" then
if charOrKey then kernel.cct.fifo.push(charOrKey) end
elseif eventType == "mouse_scroll" then
if event[2] == 1 then
kernel.cct.fifo.push("S")
else
kernel.cct.fifo.push("T")
end
elseif eventType == "http_success" then
kernel.cct.httpqueue[event[2]]=nil
kernel.cct.httpresponse[event[2]]=event[3]
@@ -83,4 +90,6 @@ kernel.processes.cctdeamon = function()
sleep(0.05)
end
end
end
end
kernel.log("CCT deamon queued for execution")

View File

@@ -1,4 +1,4 @@
U $;/
U devfs0000;/dev/
U tmpfs0000;/tmp/
U procfs0000;/proc/
U procfs0000;/proc/

View File

@@ -19,26 +19,34 @@ kernel.status = "start"
kernel.key = {}
kernel.cache = {}
kernel.cache.preload = {}
kernel.unixSockets={}
kernel._G=_G
kernel.sleep=sleep
_G.sleep=nil
local windowsExp = true
local windowsExp = false
function kernel.log(msg, level, c)
c=c or 0x6D6D6D
kernel.LOG_Text = kernel.LOG_Text..tostring(EFI:date()).." "..kernel.users[kernel.uid].." "..kernel.process.."["..tostring(level or "INFO").."]: "..msg.."\n"
if kernel.status == "start" then
screen:setTextColor(c)
screen:print(tostring(EFI:date()).." "..kernel.users[kernel.uid].." "..kernel.process.."["..tostring(level or "INFO").."]: "..msg)
elseif kernel.status == "term" then
kernel.standbyTask=kernel.currentTask
kernel.currentTask=kernel.kernelTask
local file=kernel.vfs.open("/dev/console", "w")
kernel.vfs.devctl(file,"sfgc",c)
kernel.vfs.write(file,tostring(EFI:date()).." "..kernel.users[kernel.uid].." "..kernel.process.."["..tostring(level or "INFO").."]: "..msg.."\n")
kernel.vfs.close(file)
kernel.currentTask=kernel.standbyTask
function kernel.log(msg, level, c, nopanic)
local f=function()
c=c or 0x6D6D6D
kernel.LOG_Text = kernel.LOG_Text..tostring(EFI:date()).." "..kernel.users[kernel.uid].." "..kernel.process.."["..tostring(level or "INFO").."]: "..msg.."\n"
if kernel.status == "start" then
screen:setTextColor(c)
screen:print(tostring(EFI:date()).." "..kernel.users[kernel.uid].." "..kernel.process.."["..tostring(level or "INFO").."]: "..msg)
elseif kernel.status == "term" then
kernel.standbyTask=kernel.currentTask
kernel.currentTask=kernel.kernelTask
local file=kernel.vfs.open("/dev/console", "w")
kernel.vfs.devctl(file,"sfgc",c)
kernel.vfs.write(file,tostring(EFI:date()).." "..kernel.users[kernel.uid].." "..kernel.process.."["..tostring(level or "INFO").."]: "..msg.."\n")
kernel.vfs.close(file)
kernel.currentTask=kernel.standbyTask
end
end
local ok,err = xpcall(f,debug.traceback)
if not ok and not nopanic then
kernel.panic(err)
end
end
@@ -246,6 +254,15 @@ function kernel.reboot()
kernel.status="reboot"
end
function kernel.halt()
kernel.exitMain=true
kernel.status="halt"
end
function kernel.asyncReturn(...)
kernel.currentTask.syscallReturn = {...}
end
kernel.syscalls["time"]=function() return kernel.EFI:getEpochMs() end
kernel.syscalls["date"]=function() return kernel.EFI:date() end
kernel.syscalls["log"]=kernel.log
@@ -264,8 +281,21 @@ kernel.syscalls["sysdump"]=function()
end
return rv
end
kernel.syscalls["reboot"]=kernel.reboot
kernel.syscalls["shutdown"]=kernel.shutdown
kernel.syscalls["reboot"]=function()
if kernel.uid==0 or kernel.groups.wheel then
kernel.reboot()
end
end
kernel.syscalls["shutdown"]=function()
if kernel.uid==0 or kernel.groups.wheel then
kernel.shutdown()
end
end
kernel.syscalls["halt"]=function()
if kernel.uid==0 or kernel.groups.wheel then
kernel.halt()
end
end
kernel.log("Running modules")
for _,p in ipairs(modules) do
@@ -287,11 +317,18 @@ kernel.log("Kernel initialized successfully.")
kernel.saveLog()
kernel.status="running"
screen:disable()
kernel.main()
local ok,err = xpcall(kernel.main, debug.traceback)
if not ok then
kernel.panic(err)
end
if kernel.status=="panic" then
kernel.panic(kernel.reason)
end
if kernel.status=="reboot" then
EFI.reboot=true
return true
elseif kernel.status=="halt" then
kernel.log("System halted.")
kernel.saveLog()
while true do end
end

View File

@@ -7,5 +7,5 @@ return {
initPath = "/sbin/init",
maxOpenFiles = 128,
maxFilesPerTask = 16,
preempt=true
preempt = true,
}

View File

@@ -229,6 +229,23 @@ function toHex(num)
return string.format("%X", num)
end
local st={
push=function(self,...) table.insert(self.contents,{...}) end,
pop=function(self) return table.unpack(table.remove(self.contents)) end,
}
local q={
push=function(self,...) table.insert(self.contents,{...}) end,
pop=function(self) return table.unpack(table.remove(self.contents,1)) end,
}
function stack()
return setmetatable({contents={}}, {__index=st})
end
function queue()
return setmetatable({contents={}}, {__index=q})
end
local function makeSyscallProxy()
local backing = {}
return setmetatable(backing, {

View File

@@ -542,6 +542,7 @@ local function allocFD(task)
if fd >= kernel.config.maxFilesPerTask then error("ENFILE") end
return fd
end
local function checkSystemLimit()
if total >= kernel.config.maxOpenFiles - 16 then error("ENFILE") end
end
@@ -555,27 +556,88 @@ function vfs.newfd(fdobj)
return fd
end
function vfs.mount(target, diskOrId)
function vfs.mount(target, diskOrId, bind)
local _euid = (kernel.currentTask and (kernel.currentTask.euid or kernel.currentTask.uid)) or kernel.uid
if _euid ~= 0 then error("EPERM") end
if not target then error("EINVAL") end
target = normalizeMountPoint(target)
local drive, path = resolvePath(target)
if not drive:directoryExists(path) then drive:makeDirectory(path) end
if drive:type(target) ~= "directory" then error("EINVAL") end
if not drive:directoryExists(path) then
drive:makeDirectory(path)
end
if drive:type(path) ~= "directory" then
error("EINVAL")
end
local disk, id
if type(diskOrId) == "string" then
disk = kernel.disks[diskOrId]
if not disk then error("ENODEV") end
checkDisk(disk); id = diskOrId
elseif type(diskOrId) == "table" then
checkDisk(diskOrId); disk = diskOrId
id = disk.address; vfs.disks[id] = disk
else error("EINVAL") end
-- bind mount
if bind then
local src = normalizeMountPoint(diskOrId)
local srcDrive = resolvePath(src)
if not srcDrive then error("ENOENT") end
id = "bind:" .. src
disk = {
address = id,
isBind = true,
source = srcDrive,
exists = function(_, p)
return srcDrive:exists(p)
end,
type = function(_, p)
return srcDrive:type(p)
end,
list = function(_, p)
return srcDrive:list(p)
end,
open = function(_, ...)
return srcDrive:open(...)
end,
remove = function(_, p)
return srcDrive:remove(p)
end,
makeDirectory = function(_, p)
return srcDrive:makeDirectory(p)
end,
rename = function(_, a, b)
return srcDrive:rename(a, b)
end,
size = function(_, p)
return srcDrive:size(p)
end,
lastModified = function(_, p)
return srcDrive:lastModified(p)
end,
isReadOnly = function()
return srcDrive:isReadOnly()
end,
spaceTotal = function()
return srcDrive:spaceTotal()
end,
spaceUsed = function()
return srcDrive:spaceUsed()
end
}
vfs.disks[id] = disk
else
if type(diskOrId) == "string" then
disk = vfs.disks[diskOrId]
if not disk then error("ENODEV") end
checkDisk(disk)
id = diskOrId
elseif type(diskOrId) == "table" then
checkDisk(diskOrId)
disk = diskOrId
id = disk.address
vfs.disks[id] = disk
else
error("EINVAL")
end
end
if vfs.mounts[id] then error("EBUSY") end
for _, mp in pairs(vfs.mounts) do if mp == target then error("EBUSY") end end
for _, mp in pairs(vfs.mounts) do
if mp == target then
error("EBUSY")
end
end
vfs.mounts[id] = target
return true
end
@@ -588,7 +650,11 @@ function vfs.umount(target)
for id, mp in pairs(vfs.mounts) do
if mp == target then
if id == "$" then error("EBUSY") end
vfs.mounts[id] = nil; return true
vfs.mounts[id] = nil
if vfs.disks[id] and vfs.disks[id].isBind then
vfs.disks[id] = nil
end
return true
end
end
error("EINVAL")

View File

@@ -0,0 +1,63 @@
--:Minify:--
local kernel = ...
local cache = {}
kernel.searchpaths = {
"?", "?.lua", "/lib/?", "/lib/?.lua", "/usr/lib/?", "/usr/lib/?.lua",
"/usr/local/lib/?", "/usr/local/lib/?.lua"
}
kernel.reqcache = cache
local function require(module, ...)
if cache[module] then return cache[module].ret end
kernel.currentTask.status = "D"
local args = {...}
kernel.currentTask.ksh = coroutine.create(function()
local coro = function()
for _, path in ipairs(kernel.searchpaths) do
local filepath = path:gsub("?", module)
if kernel.vfs.exists(filepath) then
if kernel.vfs.type(filepath) == "directory" then
filepath = filepath .. "/init.lua"
if kernel.vfs.type(filepath) == "directory" then
kernel.asyncReturn(false, "Module not found: "..module)
return
end
end
local fd = kernel.vfs.open(filepath, "r")
local chunks = {}
while true do
local chunk = kernel.vfs.read(fd, 4096)
if not chunk or chunk == "" then break end
chunks[#chunks + 1] = chunk
coroutine.yield()
end
kernel.vfs.close(fd)
local data = table.concat(chunks)
local func, err = load(data, "@"..module, "t", kernel._U)
if not func then
kernel.asyncReturn(false, "Error loading module "..module..": "..tostring(err))
return
end
local ok, ret = xpcall(func, debug.traceback, table.unpack(args))
if not ok then
kernel.asyncReturn(false, "Error running module "..module..": "..tostring(ret))
return
end
cache[module] = {ret = ret, expires = kernel.EFI:getEpochMs() + 120000}
kernel.asyncReturn(true, ret)
return
end
coroutine.yield()
end
kernel.asyncReturn(false, "Module not found: "..module)
end
local status, err = xpcall(coro, debug.traceback)
if not status then
kernel.asyncReturn(false, "Error in require coroutine for module "..module..": "..tostring(err))
end
end)
end
kernel.syscalls["require"] = require
_G.require = function(...) return syscall.require(...) end

View File

@@ -1,40 +0,0 @@
--:Minify:--
local kernel = ...
local cache = {}
kernel.searchpaths = {
"/lib/?.lua", "/lib/?", "/usr/lib/?.lua", "/usr/lib/?",
"/usr/local/lib/?.lua", "/usr/local/lib/?", "?.lua", "?"
}
function require(module, ...)
if cache[module] then return cache[module] end
local modpath = module:gsub("%.", "/")
local failed = {}
for _, path in ipairs(kernel.searchpaths) do
local full_path = string.replace(path, "?", modpath)
if full_path:sub(1, 1) ~= "/" then
full_path = kernel.currentTask.cwd .. full_path
end
if kernel.vfs.exists(full_path) then
if kernel.vfs.type(full_path) == "directory" then
full_path = full_path .. "/init"
end
if kernel.vfs.exists(full_path) then
local handle = kernel.vfs.open(full_path, "r")
local file_content = kernel.vfs.read(handle, 1024 * 1024 * 4)
kernel.vfs.close(handle)
return
assert(load(file_content, full_path, "t", kernel._U))(...)
else
table.insert(failed, full_path)
end
else
table.insert(failed, full_path)
end
end
error("Module not found: " .. module .. " (searched paths: " .. table.concat(failed, ", ") .. ")")
end

View File

@@ -140,6 +140,83 @@ function data.zero(op, mode)
end
end
local function buildMeta(entries, opts)
opts = opts or {}
local uid = opts.uid or 0
local gid = opts.gid or 0
local perms = opts.perms or 0x3F -- default read/write for owner/group/world
local chunks = {}
table.insert(chunks, string.char(0x02)) -- version header
for path, target in pairs(entries) do
local name = path
local nameLen = #name
if nameLen > 255 then
error("Filename too long (>255 bytes): "..name)
end
-- Determine entry type: 0x01 = symlink if target ~= nil and target ~= ""
local entryType = 0x00
local cmeta = ""
if target and target ~= "" then
entryType = 0x01
cmeta = target
end
local cmetaLen = #cmeta
if cmetaLen > 255 then
error("cmeta too long (>255 bytes) for "..name)
end
-- Build entry as bytes
table.insert(chunks, string.char(nameLen)) -- name length
table.insert(chunks, name) -- name
table.insert(chunks, string.char(entryType)) -- entry type
table.insert(chunks, string.char(uid % 256, math.floor(uid/256) % 256)) -- uid
table.insert(chunks, string.char(gid % 256, math.floor(gid/256) % 256)) -- gid
table.insert(chunks, string.char(perms % 256, math.floor(perms/256) % 256)) -- perms
table.insert(chunks, string.char(cmetaLen)) -- cmeta length
if cmetaLen > 0 then
table.insert(chunks, cmeta)
end
end
return table.concat(chunks)
end
local function simpleFile(r,w)
return function(op, mode)
if op=="type" then
return "file"
elseif op=="open" then
if mode=="r" then
return {
read=r
}
elseif mode=="w" then
return {
write=w
}
end
end
end
end
local function strFile(str)
local dat=tostring(str)
local pos=1
return simpleFile(function(amount)
pos=pos+amount
return dat:sub(pos-amount, pos)
end,function() error("EACCES") end)
end
data[".meta"]=strFile(buildMeta({
stdin="/proc/self/fd/0",
stdout="/proc/self/fd/1",
log="/var/log/syslog.log"
}))
if kernel.EFI:getEEPROM() then
function data.eeprom(op, mode)
if op=="type" then
@@ -216,7 +293,8 @@ if kernel.EFI:getNvram() then
end
end
data["disk"]={}
data["disk"]={["by-id"]={}, ["by-path"]={}, ["by-label"]={}}
data["input"]={}
kernel.devfs={}
kernel.devfs.data=data
kernel.devfs.proxy=proxy

View File

@@ -114,7 +114,8 @@ local function newtaskproxy(task)
},
children={
[".meta"]=strFile(buildMeta(children))
}
},
pid=strFile(task.pid)
}
end

View File

@@ -1,134 +1,141 @@
--:Minify:--
local kernel = ...
local proxy = {}
local data = {}
kernel.vfs.remove("/tmp")
kernel.vfs.mkdir("/tmp")
if not kernel.config.tmpToDisk then
local proxy = {}
local data = {}
proxy.address = "tmpfs0000"
proxy.isvirt = true
proxy.isReadOnly = function() return false end
proxy.address = "tmpfs0000"
proxy.isvirt = true
proxy.isReadOnly = function() return false end
proxy.spaceUsed = function() return 0 end
proxy.spaceTotal = function() return 0 end
proxy.spaceUsed = function() return 0 end
proxy.spaceTotal = function() return 0 end
proxy.makeDirectory = function(_, path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps do
if not step[steps[i]] then
step[steps[i]] = {}
elseif type(step[steps[i]]) ~= "table" then
error("ENOTDIR")
proxy.makeDirectory = function(_, path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps do
if not step[steps[i]] then
step[steps[i]] = {}
elseif type(step[steps[i]]) ~= "table" then
error("ENOTDIR")
end
step = step[steps[i]]
end
step = step[steps[i]]
end
end
proxy.remove = function(_, path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps-1 do
step = step[steps[i]]
if not step then error("ENOENT") end
end
step[steps[#steps]] = nil
end
proxy.setLabel = function(_, label) end
proxy.getLabel = function() return "tmpfs" end
proxy.attributes = function(_, path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps do
step = step[steps[i]]
if not step then error("ENOENT") end
end
return {
size = type(step) == "string" and #step or 0,
modified = 0,
created = 0,
}
end
function proxy:open(path, mode)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps-1 do
if not step[steps[i]] then
if mode == "w" then step[steps[i]] = {} else error("ENOENT") end
elseif type(step[steps[i]]) ~= "table" then
error("ENOTDIR")
proxy.remove = function(_, path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps-1 do
step = step[steps[i]]
if not step then error("ENOENT") end
end
step = step[steps[i]]
step[steps[#steps]] = nil
end
local filename = steps[#steps]
if mode == "r" then
if type(step[filename]) ~= "string" then error("ENOENT") end
local content = step[filename]
local pos = 1
proxy.setLabel = function(_, label) end
proxy.getLabel = function() return "tmpfs" end
proxy.attributes = function(_, path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps do
step = step[steps[i]]
if not step then error("ENOENT") end
end
return {
read = function(amount)
amount = amount or #content
local chunk = content:sub(pos, pos+amount-1)
pos = pos + #chunk
return chunk
end,
close = function() end,
size = type(step) == "string" and #step or 0,
modified = 0,
created = 0,
}
elseif mode == "w" then
step[filename] = ""
local buf = {}
return {
write = function(str)
buf[#buf + 1] = str
end,
close = function()
step[filename] = table.concat(buf)
end,
}
elseif mode == "a" then
if type(step[filename]) ~= "string" then step[filename] = "" end
return {
write = function(str)
step[filename] = step[filename] .. str
end,
close = function() end,
}
else
error("EACCES")
end
end
function proxy:type(path)
local steps = kernel.vfs.splitPath(path)
local step = data
if #steps == 0 then return "directory" end
for i=1,#steps do
step = step[steps[i]]
if not step then return false end
function proxy:open(path, mode)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps-1 do
if not step[steps[i]] then
if mode == "w" then step[steps[i]] = {} else error("ENOENT") end
elseif type(step[steps[i]]) ~= "table" then
error("ENOTDIR")
end
step = step[steps[i]]
end
local filename = steps[#steps]
if mode == "r" then
if type(step[filename]) ~= "string" then error("ENOENT") end
local content = step[filename]
local pos = 1
return {
read = function(amount)
amount = amount or #content
local chunk = content:sub(pos, pos+amount-1)
pos = pos + #chunk
return chunk
end,
close = function() end,
}
elseif mode == "w" then
step[filename] = ""
local buf = {}
return {
write = function(str)
buf[#buf + 1] = str
end,
close = function()
step[filename] = table.concat(buf)
end,
}
elseif mode == "a" then
if type(step[filename]) ~= "string" then step[filename] = "" end
return {
write = function(str)
step[filename] = step[filename] .. str
end,
close = function() end,
}
else
error("EACCES")
end
end
if type(step) == "table" then return "directory" end
if type(step) == "string" then return "file" end
end
function proxy:list(path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps do
step = step[steps[i]]
if not step then error("ENOENT") end
function proxy:type(path)
local steps = kernel.vfs.splitPath(path)
local step = data
if #steps == 0 then return "directory" end
for i=1,#steps do
step = step[steps[i]]
if not step then return false end
end
if type(step) == "table" then return "directory" end
if type(step) == "string" then return "file" end
end
if type(step) ~= "table" then error("ENOTDIR") end
local keys = {}
for k,_ in pairs(step) do table.insert(keys, k) end
return keys
end
function proxy:fileExists(path)
local t = self:type(path)
return t == "file" or t == "directory"
end
function proxy:list(path)
local steps = kernel.vfs.splitPath(path)
local step = data
for i=1,#steps do
step = step[steps[i]]
if not step then error("ENOENT") end
end
if type(step) ~= "table" then error("ENOTDIR") end
local keys = {}
for k,_ in pairs(step) do table.insert(keys, k) end
return keys
end
kernel.disks["tmpfs0000"] = proxy
function proxy:fileExists(path)
local t = self:type(path)
return t == "file" or t == "directory"
end
kernel.disks["tmpfs0000"] = proxy
else
kernel.log("tmpToDisk enabled, skipping tmpfs module")
kernel.log("tmpfs0000 will passthrough to /tmp on the disk")
end

View File

@@ -25,10 +25,11 @@ for _, line in ipairs(string.split(kernel.fstab, "\n")) do
else
local id = line:sub(3, semicolon_pos - 1)
local path = trim(line:sub(semicolon_pos + 1))
kernel.log("Mounted "..id.." to "..path)
kernel.log("Mounting '"..id.."' to '"..path.."'")
if id ~= "$" then
kernel.vfs.mount(path, id)
end
kernel.log("Mounted "..id.." to "..path)
end
end
end

View File

@@ -1,67 +1,194 @@
--:Minify:--
-- Supports:
-- AF_UNIX - local IPC via /var/run/*.sock paths
-- AF_INET - network sockets with three backends:
-- Implemented by drivers but expect http:// and https://
--
-- Socket lifecycle:
-- fd = syscall.socket(domain, socktype) -- "unix"/"inet", "stream"/"dgram"
-- syscall.bind(fd, address) -- server: claim address
-- syscall.listen(fd, backlog) -- server: mark as listening
-- cfd = syscall.accept(fd) -- server: get connected client fd (blocking poll)
-- syscall.connect(fd, address) -- client: connect to server
-- syscall.send(fd, data) -- send bytes
-- syscall.recv(fd, len) -- receive bytes (blocking poll, returns "" on nothing)
-- syscall.sockshutdown(fd) -- half-close send side
-- -- normal vfs.close(fd) closes the socket
local kernel = ...
local kernel=...
local socket={}
socket.handlers={}
kernel.socket=socket
local socket = {}
function socket.registerProtocal(protocal, handler)
socket.handlers[protocal] = handler
socket.handlers = {}
kernel.socket = socket
local P = kernel.vfs.P
local sys = kernel.syscalls
function socket.registerProtocal(proto, handler)
socket.handlers[proto] = handler
end
function socket.socket()
local P=kernel.vfs.P
local data=kernel.newFifo()
local isClosed=false
return kernel.vfs.newfd({
handle={
read=function() if isClosed then error("ECCON") end return data.pop() end,
write=function(data) if isClosed then error("ECCON") end return data.push(data) end,
close=function() isClosed = true end
local function getHandler(address)
for proto, handler in pairs(socket.handlers) do
if string.hasPrefix(address, proto) then
return handler
end
end
end
function socket.socket(domain, socktype)
local fd = kernel.vfs.newfd({
handle = {},
type = "socket",
refcount = 1,
socket = {
connected = false,
protocol = nil,
address = nil
},
type="socket",
refcount=1,
socket={},
meta={
owner=kernel.currentTask.uid,
group=kernel.currentTask.uid,
etype=2,
perms=P.OWNER_R+P.OWNER_W+P.GROUP_R+P.GROUP_W
meta = {
owner = kernel.currentTask.uid,
group = kernel.currentTask.uid,
etype = 2,
perms =
P.OWNER_R +
P.OWNER_W +
P.GROUP_R +
P.GROUP_W
},
isvirt=true
isvirt = true
})
local fdo = kernel.currentTask.fd[fd]
fdo.handle.read = function()
return ""
end
fdo.handle.write = function()
return nil, "ENOTCONN"
end
fdo.handle.close = function()
fdo.socket.connected = false
return true
end
return fd
end
function socket.connect(fd, address)
local handler
for k, v in pairs(socket.handlers) do
if string.hasPrefix(address, k) then
handler=v
end
local fdo = kernel.currentTask.fd[fd]
if not fdo then
return nil, "EBADF"
end
handler.connect(kernel.currentTask.fd[fd], address)
if fdo.type ~= "socket" then
return nil, "ENOTSOCK"
end
local handler =
getHandler(address)
if not handler then
return nil, "EPROTONOSUPPORT"
end
fdo.socket.protocol = handler
fdo.socket.address = address
local ok, err =
handler.connect(fd, address)
if not ok then
return nil, err
end
fdo.socket.connected = true
return true
end
function socket.listen(fd, backlog)
error("Not Implemented")
local fdo =
kernel.currentTask.fd[fd]
if not fdo then
return nil, "EBADF"
end
if not fdo.handle.listen then
return nil, "EOPNOTSUPP"
end
return fdo.handle.listen(backlog)
end
local sys=kernel.syscalls
sys["socket"]=socket.socket
function socket.send(fd, data)
local fdo =
kernel.currentTask.fd[fd]
if not fdo then
return nil, "EBADF"
end
if fdo.type ~= "socket" then
return nil, "ENOTSOCK"
end
if not fdo.socket.connected then
return nil, "ENOTCONN"
end
if not fdo.handle.write then
return nil, "EOPNOTSUPP"
end
return fdo.handle.write(data)
end
function socket.recv(fd, amount)
local fdo =
kernel.currentTask.fd[fd]
if not fdo then
return nil, "EBADF"
end
if fdo.type ~= "socket" then
return nil, "ENOTSOCK"
end
if not fdo.socket.connected then
return nil, "ENOTCONN"
end
if not fdo.handle.read then
return nil, "EOPNOTSUPP"
end
return fdo.handle.read(amount)
end
function socket.sockshutdown(fd)
local fdo =
kernel.currentTask.fd[fd]
if not fdo then
return nil, "EBADF"
end
if fdo.type ~= "socket" then
return nil, "ENOTSOCK"
end
if fdo.handle.close then
return fdo.handle.close()
end
fdo.socket.connected = false
return true
end
sys.socket = socket.socket
sys.connect = socket.connect
sys.listen = socket.listen
sys.send = socket.send
sys.recv = socket.recv
sys.sockshutdown = socket.sockshutdown
kernel.log("Loaded socket module")

View File

@@ -0,0 +1,129 @@
--:Minify:--
local kernel = ...
local vterms = {}
local function createVt(id, width, height)
local vt = {
id = id,
width = width,
height = height,
buffer = {},
cursorX = 1,
cursorY = 1,
fgColor = 0xFFFFFF,
bgColor = 0x000000,
obj = {}
}
for y = 1, height do
vt.buffer[y] = {}
for x = 1, width do
vt.buffer[y][x] = {char = " ", fgColor = vt.fgColor, bgColor = vt.bgColor}
end
end
local function scroll(lines)
for _ = 1, lines do
table.remove(vt.buffer, 1)
vt.buffer[vt.height] = {}
for x = 1, vt.width do
vt.buffer[vt.height][x] = {char = " ", fgColor = vt.fgColor, bgColor = vt.bgColor}
end
end
end
function vt.obj:write(content)
local x, y = vt.cursorX, vt.cursorY
if x>vt.width then return end
if y>vt.height then return end
for i = 1, #content do
local c = content:sub(i, i)
if c == "\n" then
y = y + 1
x = 1
elseif c == "\t" then
local tabSize = 4
local spaces = tabSize - ((x - 1) % tabSize)
for _ = 1, spaces do
vt.buffer[y][x] = {char = " ", fgColor = vt.fgColor, bgColor = vt.bgColor}
x = x + 1
if x > vt.width then
x = 1
y = y + 1
end
end
elseif c == "\b" then
if x > 1 then
x = x - 1
vt.buffer[y][x] = {char = " ", fgColor = vt.fgColor, bgColor = vt.bgColor}
end
else
if x <= vt.width and y <= vt.height then
vt.buffer[y][x] = {char = c, fgColor = vt.fgColor, bgColor = vt.bgColor}
x = x + 1
end
end
if x > vt.width then
x = 1
y = y + 1
end
if y > vt.height then
scroll(1)
y = vt.height
end
end
vt.cursorX, vt.cursorY = x, y
end
function vt.obj:spos(x, y)
vt.cursorX = tonumber(x)
vt.cursorY = tonumber(y)
end
function vt.obj:gpos()
return tostring(vt.cursorX)..";"..tostring(vt.cursorY)
end
function vt.obj:sfgc(color)
vt.fgColor = tonumber(color)
end
function vt.obj:sbgc(color)
vt.bgColor = tonumber(color)
end
function vt.obj:gfgc()
return vt.fgColor
end
function vt.obj:gbgc()
return vt.bgColor
end
function vt.obj:gplt()
return 24
end
function vt.obj:clear()
for y = 1, vt.height do
for x = 1, vt.width do
vt.buffer[y][x] = {char = " ", fgColor = vt.fgColor, bgColor = vt.bgColor}
end
end
vt.cursorX, vt.cursorY = 1, 1
end
function vt.obj:isvirt()
return true
end
function vt.obj:gctrl()
return serializeBool(vt.ctrl)..";"..serializeBool(vt.alt)
end
return vt
end

View File

@@ -21,7 +21,7 @@ local function loadExecutable(path)
local env = kernel.freshUserEnv()
local func, err = load(data, "@" .. path, "t", env)
if not func then error("ENOEXEC: " .. tostring(err)) end
if not func then kernel.log("Failed to load executable: " .. tostring(err).. " : ".. tostring(path)); error("ENOEXEC: " .. tostring(err)) end
local meta = kernel.vfs.lstat(path)
local suid_set = bit_is_set(meta.perms, 6)
@@ -211,6 +211,8 @@ function sys.kill(pid, force)
return false, "Task is already dead"
elseif task.status == "D" and not force then
return false, "Cannot kill task waiting for IO"
elseif task.euid ~= kernel.uid and kernel.uid ~= 0 then
return false, "Different user"
end
local caller = kernel.currentTask
local ceuid = caller and (caller.euid or caller.uid) or kernel.uid
@@ -227,6 +229,8 @@ function sys.stop(pid)
return false, "Task does not exist"
elseif task.status ~= "R" and task.status ~= "S" then
return false, "Cannot stop non-running task"
elseif task.euid ~= kernel.uid and kernel.uid ~= 0 then
return false, "Different user"
else
if task.status == "S" then
task.status = "ST"
@@ -243,6 +247,8 @@ function sys.continue(pid)
return false, "Task does not exist"
elseif task.status ~= "T" and task.status ~= "ST" then
return false, "Task is not stopped"
elseif task.euid ~= kernel.uid and kernel.uid ~= 0 then
return false, "Different user"
else
if task.status == "ST" then
task.status = "S"
@@ -299,20 +305,21 @@ function sys.getuid() return kernel.currentTask.uid end
local function reapDeadTasks()
for pid, task in pairs(tasks) do
if task.status == "Z" and not task.reapTime then
task.coro = nil
task.ivs = nil
task.vs = nil
task.args = nil
task.envars = nil
task.cwd = nil
task.numRuns = nil
task.totalTime = nil
task.lastTime = nil
task.timeSlice = nil
if task.pid == 1 then kernel.panic("Attempted to gc init!") end
task.coro = nil
task.ivs = nil
task.vs = nil
task.args = nil
task.envars = nil
task.cwd = nil
task.numRuns = nil
task.totalTime = nil
task.lastTime = nil
task.timeSlice = nil
task.syscallReturn = nil
task.sleep = nil
task.fd = nil
task.reapTime = kernel.EFI:getEpochMs() + 30000
task.sleep = nil
task.fd = nil
task.reapTime = kernel.EFI:getEpochMs() + 30000
elseif task.reapTime and kernel.EFI:getEpochMs() > task.reapTime
and task.status == "Z" then
@@ -344,7 +351,23 @@ local k_max = 0.5
local B = 0.01
function kernel.main()
kernel.log("Starting main loop...")
kernel.saveLog()
local stopLog=5
local logTasks=100
while not kernel.exitMain do
if kernel.config.logTasks then
if logTasks<0 then
kernel.log("Active Tasks:")
for i,v in pairs(tasks) do
kernel.log(v.name.." : "..v.status.." : "..tostring(v.pid).." : "..tostring(v.exit))
end
kernel.log("[END BLOCK]")
logTasks=100
end
logTasks=logTasks-1
end
local N = 0
local Tmin_hit = 0
local Tmax_hit = 0
@@ -352,6 +375,7 @@ function kernel.main()
local taskTimes = {}
for pid, task in pairs(tasks) do
if kernel.exitMain then break end
kernel.currentTask = task
kernel.uid = task.euid or task.uid
kernel.process = task.name
@@ -366,9 +390,29 @@ function kernel.main()
task.sleep = 0
end
if task.status == "DS" and kernel.EFI:getEpochMs() >= task.sleep then
task.status = "D"
task.sleep = 0
end
if task.status == "D" then
if task.ksh then
coroutine.resume(task.ksh)
if coroutine.status(task.ksh) == "dead" then
task.ksh = nil
if task.status == "D" then
task.status = "R"
end
if kernel.config.debugSyscalls then
kernel.log("Task " .. task.pid .. " IO wait completed", "DBUG", 0x00FFFF)
local sysret = task.syscallReturn
for i = 2, #sysret do
local v = type(sysret[i]) == "table" and table.serialize(sysret[i]) or tostring(sysret[i])
kernel.log(" retval[" .. (i-1) .. "] = " .. v, "DBUG", 0x00FFFF)
end
end
end
end
end
@@ -405,7 +449,9 @@ function kernel.main()
if task.status == "R" then
local startTime = kernel.EFI:getEpochMs()
local ret
if stopLog > 0 then
kernel.log("Running task " .. tostring(task.pid) .. " (" .. task.name .. ") with time slice " .. string.format("%.3f", task.timeSlice) .. "s", "DBUG", 0x00FFFF)
end
if kernel.config.preempt then
if not task.debugger then
ret = { resumeWithTimeout(task.coro, task.timeSlice, table.unpack(task.syscallReturn)) }
@@ -491,6 +537,11 @@ function kernel.main()
end
reapDeadTasks()
if stopLog > 0 then
kernel.log("Executed " .. N .. " tasks, avg time: " .. string.format("%.2f", T_prev_avg) .. "ms, var: " .. string.format("%.2f", T_prev_var) .. ", B: " .. string.format("%.5f", B))
stopLog = stopLog - 1
kernel.saveLog()
end
end
end

View File

@@ -0,0 +1,11 @@
local kernel = ...
kernel.processes.kgc = function()
while true do
for i,v in pairs(kernel.reqcache) do
if v.expires and kernel.EFI:getEpochMs() > v.expires then
kernel.reqcache[i] = nil
end
end
kernel.sleep(5000)
end
end

View File

@@ -1,5 +1,5 @@
--:Minify:--
syscall.open("/dev/tty/1", "r") --stdin (fd 0)
syscall.open("/dev/input/keyboard1", "r") --stdin (fd 0)
syscall.open("/dev/tty/1", "w") --stdout (fd 1)
syscall.open("/dev/null", "w") --stderr (fd 2)

View File

@@ -0,0 +1,3 @@
{
"/bin/sh": "/bin/hysh"
}

View File

@@ -107,6 +107,7 @@ local COMMANDS = {
{ name="whoami", usage="whoami", desc="Print the current username.", flags={} },
{ name="id", usage="id [username]", desc="Print user identity (uid, gid).", flags={} },
{ name="ps", usage="ps", desc="List running tasks with pid, user, name, and status.", flags={} },
{ name="hyperctl", usage="hyperctl <command> [service]", desc="Control system services (start, stop, restart, status, list, enable, disable).", flags={} },
{ name="hostname", usage="hostname [NAME]", desc="Print or set the system hostname.", flags={} },
{ name="uname", usage="uname [-asnrm]", desc="Print system information (OS name, hostname, release, machine).", flags={
{"-a","Print all fields"},

View File

@@ -1,10 +1,10 @@
--:Minify:--
syscall.open("/dev/tty/1","r") --stdin (Device 0)
local success, errorMsg = xpcall(function()
syscall.open("/dev/input/keyboard1","r") --stdin (Device 0)
syscall.open("/dev/tty/1","w") --stdout (Device 1)
syscall.open("/dev/null","w") --stderr (device 2)
local success, errorMsg = xpcall(function()
local fs = require("fs")
syscall.devctl(1,"clear")
@@ -833,6 +833,14 @@ builtinCmds.ops = function()
end
end
builtinCmds.reboot = function ()
syscall.reboot()
end
builtinCmds.shutdown = function ()
syscall.shutdown()
end
local function listDir(dir, prefix)
local ok, entries = pcall(syscall.listdir, dir)
if not ok or not entries then return {} end
@@ -1203,9 +1211,9 @@ local function runCommand(command)
local proc = syscall.spawn(function()
-- Open standard fds so programs that don't do it themselves work correctly.
syscall.open("/dev/tty/1", "r") -- fd 0 stdin
syscall.open("/dev/input/keyboard1", "r") -- fd 0 stdin
syscall.open("/dev/tty/1", "w") -- fd 1 stdout
syscall.open("/dev/null", "w") -- fd 2 stderr
syscall.open("/dev/null", "w") -- fd 2 stderr
-- exec replaces this coroutine's code with a fresh isolated environment
-- compiled from disk by the kernel (via loadExecutable -> freshUserEnv),
-- so the child cannot share any upvalue or syscall table state with hysh.

113
Src/hysh/data/bin/tree Normal file
View File

@@ -0,0 +1,113 @@
--:Minify:--
local cloptions = {
a = false,
h = false,
l = false,
help = false,
}
local inpArgs = { ... }
local args = {}
local name = syscall.getTask(syscall.getpid()).name
for _, v in pairs(inpArgs) do
if v:sub(1, 2) == "--" then
local opt = v:sub(3)
if cloptions[opt] == nil then
print(name .. ": unrecognized option '" .. v .. "'.")
print("try '" .. name .. " --help' for more information.")
return
end
cloptions[opt] = true
elseif v:sub(1, 1) == "-" then
for i = 2, #v do
local opt = v:sub(i, i)
if cloptions[opt] == nil then
print(name .. ": invalid option '-" .. opt .. "'.")
print("try '" .. name .. " --help' for more information.")
return
end
cloptions[opt] = true
end
else
table.insert(args, v)
end
end
if cloptions.help then
print("Usage: " .. name .. " [OPTION]... [DIR]")
print("List all entries in the specified DIRectory, or cwd if not specified.")
print("Display directory tree structure.")
print("")
print("Options:")
print(" -a do not ignore entries starting with .")
print(" -h with -l, print sizes in human readable format (e.g., 1K 234M 2G)")
print(" -l use a long listing format (show file sizes)")
print(" --help display this help and exit")
return
end
local fs = require("fs")
local dir = args[1] or ""
if dir:sub(1, 1) ~= "/" then
dir = syscall.getcwd() .. "/" .. dir
end
if dir:sub(-1) ~= "/" then dir = dir .. "/" end
if not fs.isDir(dir) then
print(name .. ": cannot access '" .. (args[1] or dir) .. "': no such directory")
return
end
local function format_size(size)
if not cloptions.h then return tostring(size) end
if size < 1024 then return tostring(size) .. "B"
elseif size < 1024*1024 then return string.format("%.1fK", size/1024)
elseif size < 1024*1024*1024 then return string.format("%.1fM", size/1024/1024)
else return string.format("%.1fG", size/1024/1024/1024) end
end
local function pdir(current_dir, level, prefix)
prefix = prefix or ""
local list = fs.list(current_dir)
if not cloptions.a then
for i = #list, 1, -1 do
if list[i]:sub(1, 1) == "." then table.remove(list, i) end
end
end
table.sort(list)
if current_dir:sub(-1) ~= "/" then current_dir = current_dir .. "/" end
for i, entry in ipairs(list) do
local full_path = current_dir .. entry
local is_last = (i == #list)
local branch = is_last and "`--" or "|--"
local indent = prefix .. branch
local suffix = ""
local info = ""
if fs.isDir(full_path) then
suffix = "/"
end
if cloptions.l then
local stat = fs.stat and fs.stat(full_path)
if stat then
local size = stat.size or 0
info = " " .. format_size(size)
end
end
print(indent .. entry .. suffix .. info)
if fs.isDir(full_path) then
local new_prefix = prefix .. (is_last and " " or "| ")
pcall(pdir, full_path, level + 1, new_prefix)
end
end
end
-- Print root directory
local root_name = dir:sub(1, -2) -- remove trailing /
if root_name == "" then root_name = "/" end
print(root_name)
pdir(dir, 0, "")

View File

View File

View File

@@ -0,0 +1,29 @@
local function usageError()
print(
"\nusage: minify <file> or unminify <file>\n" ..
" The modified code will be printed to the stdout, pipe it to a file,\n" ..
" or something else as desired EG:\n\n" ..
" minify minify input.lua > output.lua\n\n" ..
" * minify will minify the code in the file.\n" ..
" * unminify will beautify the code and replace the variable names with easily\n" ..
" find-replacable ones to aide in reverse engineering minified code.\n")
syscall.exit(0)
end
local args = {...}
if #args ~= 2 then
usageError()
end
local minify = require("minify")
local fs = require("fs")
if args[1] == 'minify' then
print(minify.minify(fs.readAllText(args[2])))
elseif args[1] == 'unminify' then
print(minify.beautify(fs.readAllText(args[2])))
else
usageError()
end

File diff suppressed because it is too large Load Diff

136
Src/pid/data/lib/pid.lua Normal file
View File

@@ -0,0 +1,136 @@
--:Minify:--
local pid = {}
pid.__index = pid
local function clamp(value, minimum, maximum)
if minimum and value < minimum then
return minimum
end
if maximum and value > maximum then
return maximum
end
return value
end
local function assertNumber(value, name)
if type(value) ~= "number" then
error("pid: " .. name .. " must be a number")
end
return value
end
function pid.new(kp, ki, kd, options)
assertNumber(kp, "kp")
assertNumber(ki, "ki")
assertNumber(kd, "kd")
options = options or {}
local controller = setmetatable({
kp = kp,
ki = ki,
kd = kd,
setpoint = options.setpoint or 0,
outputMin = options.outputMin,
outputMax = options.outputMax,
integralMin = options.integralMin,
integralMax = options.integralMax,
lastError = 0,
integral = 0,
lastMeasurement = nil,
lastOutput = 0,
lastTime = nil,
}, pid)
return controller
end
function pid:reset()
self.lastError = 0
self.integral = 0
self.lastMeasurement = nil
self.lastOutput = 0
self.lastTime = nil
end
function pid:setSetpoint(setpoint)
assertNumber(setpoint, "setpoint")
self.setpoint = setpoint
end
function pid:setCoefficients(kp, ki, kd)
assertNumber(kp, "kp")
assertNumber(ki, "ki")
assertNumber(kd, "kd")
self.kp = kp
self.ki = ki
self.kd = kd
end
function pid:setOutputLimits(minimum, maximum)
if minimum ~= nil then assertNumber(minimum, "outputMin") end
if maximum ~= nil then assertNumber(maximum, "outputMax") end
if minimum and maximum and minimum > maximum then
error("pid: outputMin must be <= outputMax")
end
self.outputMin = minimum
self.outputMax = maximum
end
function pid:setIntegralLimits(minimum, maximum)
if minimum ~= nil then assertNumber(minimum, "integralMin") end
if maximum ~= nil then assertNumber(maximum, "integralMax") end
if minimum and maximum and minimum > maximum then
error("pid: integralMin must be <= integralMax")
end
self.integralMin = minimum
self.integralMax = maximum
end
function pid:update(measurement, dt)
assertNumber(measurement, "measurement")
assertNumber(dt, "dt")
if dt <= 0 then
error("pid: dt must be greater than zero")
end
local errorValue = self.setpoint - measurement
self.integral = clamp(self.integral + errorValue * dt, self.integralMin, self.integralMax)
local derivative = 0
if self.lastTime ~= nil then
derivative = (errorValue - self.lastError) / dt
end
local output = self.kp * errorValue + self.ki * self.integral + self.kd * derivative
output = clamp(output, self.outputMin, self.outputMax)
self.lastError = errorValue
self.lastMeasurement = measurement
self.lastOutput = output
self.lastTime = (self.lastTime or 0) + dt
return output
end
function pid:compute(measurement, dt)
return self:update(measurement, dt)
end
function pid:getState()
return {
setpoint = self.setpoint,
kp = self.kp,
ki = self.ki,
kd = self.kd,
integral = self.integral,
lastError = self.lastError,
lastOutput = self.lastOutput,
}
end
function pid:resetIntegral()
self.integral = 0
end
return pid

View File

@@ -0,0 +1,3 @@
{
"/sbin/init": "/usr/lib/sysinit/sysinit"
}

View File

@@ -2,6 +2,7 @@
local kernel=...
local fs=require("fs")
kernel.log("Sysinit started...")
kernel.saveLog()
for i,v in pairs(kernel.processes) do
kernel.log("Spawning kernel task "..i)
@@ -41,6 +42,7 @@ for i,v in ipairs(files) do
end
end
end
kernel.saveLog()
while true do
sleep(5)

541
build.py
View File

@@ -1,271 +1,272 @@
#!/usr/bin/env python3
"""
Usage:
python build.py <target> [--arch cct|oc] [--release|--dev]
Targets:
build
build-mini
build-micro
build-test
build-mini-test
build-micro-test
clean
prod
prod-mini
Arch flags:
--arch cct
--arch oc
Release flags:
--release
--dev
"""
import sys
import shutil
import argparse
import subprocess
import json
from pathlib import Path
from typing import Union
PROJECT_ROOT = Path(__file__).resolve().parent
SRC_ROOT = PROJECT_ROOT / "Src"
TEST_ROOT = PROJECT_ROOT / "Test"
BUILD_ROOT = PROJECT_ROOT / "Build"
PROD_ROOT = PROJECT_ROOT / "prod"
ARCH_BOOT_DIR = {
"cct": Path("boot") / "cct",
"oc": Path("boot") / "oc",
}
def clean():
if BUILD_ROOT.exists():
print(f"Removing {BUILD_ROOT} ...")
shutil.rmtree(BUILD_ROOT)
else:
print("Nothing to clean.")
def cleanprod():
if PROD_ROOT.exists():
print(f"Removing {PROD_ROOT} ...")
shutil.rmtree(PROD_ROOT)
else:
print("Nothing to clean.")
def has_minify_header(path: Path) -> bool:
try:
with path.open("r", encoding="utf-8", errors="ignore") as f:
for _ in range(3):
if "--:Minify:--" in f.readline():
return True
except OSError:
pass
return False
def minify_file(src: Path) -> str:
result = subprocess.run(
["luamin.cmd", "-f", str(src)],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f" ! luamin failed: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
return result.stdout
def compress_lz4(data: bytes) -> bytes:
return lz4.frame.compress(data)
def process_root(src_root: Path, out_root: Path, minify: bool, micro: bool, arch:Union[str, None], prod:bool):
print(f"Building from {src_root}")
print(f"Output to {out_root}")
print()
for pkg_dir in sorted(src_root.iterdir()):
if not pkg_dir.is_dir():
continue
if pkg_dir.name[:18] == "Hyperion-firmware-":
if pkg_dir.name != f"Hyperion-firmware-{arch}":
if prod != True:
continue
data_dir=pkg_dir / "data"
if not data_dir.is_dir():
continue
print(f"== Package: {pkg_dir.name} ==")
for src in sorted(data_dir.rglob("*")):
if not src.is_file():
continue
rel = src.relative_to(data_dir)
dst = out_root / rel
if prod:
dst = out_root / pkg_dir.name / rel
dst.parent.mkdir(parents=True, exist_ok=True)
print(f" Processing: {src.relative_to(src_root)}")
if has_minify_header(src):
if minify:
print(" > Minifying")
content = minify_file(src)
if micro:
print(" > LZ4 compressing")
compressed = compress_lz4(content.encode("utf-8"))
dst.write_bytes(compressed)
else:
dst.write_text(content, encoding="utf-8")
else:
print(" > Copying")
shutil.copy2(src, dst)
else:
print(" > Copying")
shutil.copy2(src, dst)
print()
def install_bootloader(arch: str, release: bool):
boot_dir = BUILD_ROOT / "$" / ARCH_BOOT_DIR[arch]
eeprom = boot_dir / "eeprom"
eeprom_dst_name = "startup.lua" if release else "eeprom"
print(f" Installing: eeprom -> Build/{eeprom_dst_name}")
shutil.copy2(eeprom, BUILD_ROOT / eeprom_dst_name)
def run_build(minify: bool, micro: bool, include_test: bool, arch: Union[str, None], release: bool, prod: bool):
clean()
BUILD_ROOT.mkdir()
if prod:
cleanprod()
PROD_ROOT.mkdir()
out_root = BUILD_ROOT / "$" if arch else BUILD_ROOT
process_root(SRC_ROOT, out_root, minify, micro, arch, False)
if prod:
process_root(SRC_ROOT, PROD_ROOT, minify, micro, arch, True)
if include_test:
process_root(TEST_ROOT, out_root, minify, micro)
if arch:
print("Installing bootloader files ...")
install_bootloader(arch, release)
print()
def _make_firstboot_kmod(users):
lines = []
lines.append("local kernel = ...")
lines.append("local auth = kernel.auth")
lines.append("")
for username, password in users:
u = username.replace("\\", "\\\\").replace("'", "\\'")
p = password.replace("\\", "\\\\").replace("'", "\\'")
if username == "root":
lines.append("do")
lines.append(" local ok, err = auth.setPassword(0, '" + p + "')")
lines.append(" if ok then")
lines.append(" kernel.log('FIRSTBOOT: root password set')")
lines.append(" else")
lines.append(" kernel.log('FIRSTBOOT: root password error: ' .. tostring(err), 'ERROR')")
lines.append(" end")
lines.append("end")
else:
lines.append("do")
lines.append(" local uid, err = auth.newUser('" + u + "', '" + p + "')")
lines.append(" if uid then")
lines.append(" kernel.log('FIRSTBOOT: created user " + u + " uid=' .. tostring(uid))")
lines.append(" else")
lines.append(" kernel.log('FIRSTBOOT: failed to create user " + u + ": ' .. tostring(err), 'ERROR')")
lines.append(" end")
lines.append("end")
lines.append("")
lines.append("do")
lines.append(" local ok, err = pcall(function()")
lines.append(" kernel.vfs.remove('/lib/modules/Hyperion/50_firstboot_users.kmod')")
lines.append(" end)")
lines.append(" if not ok then")
lines.append(" kernel.log('FIRSTBOOT: could not self-delete: ' .. tostring(err), 'WARN')")
lines.append(" end")
lines.append("end")
return "\n".join(lines) + "\n"
def inject_makeusers(users, arch):
base = BUILD_ROOT / "$" if arch else BUILD_ROOT
kmod_path = base / "lib" / "modules" / "Hyperion" / "50_firstboot_users.kmod"
kmod_path.parent.mkdir(parents=True, exist_ok=True)
kmod_path.write_text(_make_firstboot_kmod(users), encoding="utf-8")
print(" Wrote first-boot user setup -> " + str(kmod_path.relative_to(BUILD_ROOT)))
def main():
parser = argparse.ArgumentParser(description="HyperionOS build script")
parser.add_argument("target", choices=["build", "build-mini", "build-micro", "build-test", "build-mini-test", "build-micro-test", "clean", "prod", "prod-mini"])
parser.add_argument("--arch", choices=["cct", "oc"], default=None,
help="Target architecture")
parser.add_argument("--release", dest="release", action="store_true", default=True,
help="Release build: eeprom placed as startup.lua (default)")
parser.add_argument("--dev", dest="release", action="store_false",
help="Dev build: boot.lua and eeprom copied unchanged")
parser.add_argument(
"--makeuser", metavar=("USERNAME", "PASSWORD"), nargs=2, action="append",
default=[],
help=(
"Pre-create a user on first boot (dev builds only). "
"May be specified multiple times. "
"Example: --makeuser root secretpass --makeuser alice alicepass"
),
)
args = parser.parse_args()
if args.makeuser and args.release:
parser.error("--makeuser is only allowed with --dev builds")
if args.target == "clean":
clean()
return
prod = "prod" in args.target
minify = "mini" in args.target or "micro" in args.target
micro = "micro" in args.target
include_test = "test" in args.target
if micro:
import lz4.block
run_build(minify=minify, micro=micro, include_test=include_test, arch=args.arch, release=args.release, prod=prod)
if args.makeuser:
print("Injecting first-boot user setup ...")
inject_makeusers(args.makeuser, args.arch)
print()
print("Build complete.")
if __name__ == "__main__":
#!/usr/bin/env python3
"""
Usage:
python build.py <target> [--arch cct|oc] [--release|--dev]
Targets:
build
build-mini
build-micro
build-test
build-mini-test
build-micro-test
clean
prod
prod-mini
Arch flags:
--arch cct
--arch oc
Release flags:
--release
--dev
"""
import sys
import shutil
import argparse
import subprocess
import json
import os
from pathlib import Path
from typing import Union
PROJECT_ROOT = Path(__file__).resolve().parent
SRC_ROOT = PROJECT_ROOT / "Src"
TEST_ROOT = PROJECT_ROOT / "Test"
BUILD_ROOT = PROJECT_ROOT / "Build"
PROD_ROOT = PROJECT_ROOT / "prod"
ARCH_BOOT_DIR = {
"cct": Path("boot") / "cct",
"oc": Path("boot") / "oc",
}
def clean():
if BUILD_ROOT.exists():
print(f"Removing {BUILD_ROOT} ...")
shutil.rmtree(BUILD_ROOT)
else:
print("Nothing to clean.")
def cleanprod():
if PROD_ROOT.exists():
print(f"Removing {PROD_ROOT} ...")
shutil.rmtree(PROD_ROOT)
else:
print("Nothing to clean.")
def has_minify_header(path: Path) -> bool:
try:
with path.open("r", encoding="utf-8", errors="ignore") as f:
for _ in range(3):
if "--:Minify:--" in f.readline():
return True
except OSError:
pass
return False
def minify_file(src: Path) -> str:
result = subprocess.run(
["npm", "exec", "luamin", "--", "-f", str(src)],
capture_output=True,
text=True
)
if result.returncode != 0:
print(f" ! luamin failed: {result.stderr.strip()}", file=sys.stderr)
sys.exit(1)
return result.stdout
def compress_lz4(data: bytes) -> bytes:
return lz4.frame.compress(data)
def process_root(src_root: Path, out_root: Path, minify: bool, micro: bool, arch:Union[str, None], prod:bool):
print(f"Building from {src_root}")
print(f"Output to {out_root}")
print()
for pkg_dir in sorted(src_root.iterdir()):
if not pkg_dir.is_dir():
continue
if pkg_dir.name[:18] == "Hyperion-firmware-":
if pkg_dir.name != f"Hyperion-firmware-{arch}":
if prod != True:
continue
data_dir=pkg_dir / "data"
if not data_dir.is_dir():
continue
print(f"== Package: {pkg_dir.name} ==")
for src in sorted(data_dir.rglob("*")):
if not src.is_file():
continue
rel = src.relative_to(data_dir)
dst = out_root / rel
if prod:
dst = out_root / pkg_dir.name / rel
dst.parent.mkdir(parents=True, exist_ok=True)
print(f" Processing: {src.relative_to(src_root)}")
if has_minify_header(src):
if minify:
print(" > Minifying")
content = minify_file(src)
if micro:
print(" > LZ4 compressing")
compressed = compress_lz4(content.encode("utf-8"))
dst.write_bytes(compressed)
else:
dst.write_text(content, encoding="utf-8")
else:
print(" > Copying")
shutil.copy2(src, dst)
else:
print(" > Copying")
shutil.copy2(src, dst)
print()
def install_bootloader(arch: str, release: bool):
boot_dir = BUILD_ROOT / "$" / ARCH_BOOT_DIR[arch]
eeprom = boot_dir / "eeprom"
eeprom_dst_name = "startup.lua" if release else "eeprom"
print(f" Installing: eeprom -> Build/{eeprom_dst_name}")
shutil.copy2(eeprom, BUILD_ROOT / eeprom_dst_name)
def run_build(minify: bool, micro: bool, include_test: bool, arch: Union[str, None], release: bool, prod: bool):
clean()
BUILD_ROOT.mkdir()
if prod:
cleanprod()
PROD_ROOT.mkdir()
out_root = BUILD_ROOT / "$" if arch else BUILD_ROOT
process_root(SRC_ROOT, out_root, minify, micro, arch, False)
if prod:
process_root(SRC_ROOT, PROD_ROOT, minify, micro, arch, True)
if include_test:
process_root(TEST_ROOT, out_root, minify, micro)
if arch:
print("Installing bootloader files ...")
install_bootloader(arch, release)
print()
def _make_firstboot_kmod(users):
lines = []
lines.append("local kernel = ...")
lines.append("local auth = kernel.auth")
lines.append("")
for username, password in users:
u = username.replace("\\", "\\\\").replace("'", "\\'")
p = password.replace("\\", "\\\\").replace("'", "\\'")
if username == "root":
lines.append("do")
lines.append(" local ok, err = auth.setPassword(0, '" + p + "')")
lines.append(" if ok then")
lines.append(" kernel.log('FIRSTBOOT: root password set')")
lines.append(" else")
lines.append(" kernel.log('FIRSTBOOT: root password error: ' .. tostring(err), 'ERROR')")
lines.append(" end")
lines.append("end")
else:
lines.append("do")
lines.append(" local uid, err = auth.newUser('" + u + "', '" + p + "')")
lines.append(" if uid then")
lines.append(" kernel.log('FIRSTBOOT: created user " + u + " uid=' .. tostring(uid))")
lines.append(" else")
lines.append(" kernel.log('FIRSTBOOT: failed to create user " + u + ": ' .. tostring(err), 'ERROR')")
lines.append(" end")
lines.append("end")
lines.append("")
lines.append("do")
lines.append(" local ok, err = pcall(function()")
lines.append(" kernel.vfs.remove('/lib/modules/Hyperion/50_firstboot_users.kmod')")
lines.append(" end)")
lines.append(" if not ok then")
lines.append(" kernel.log('FIRSTBOOT: could not self-delete: ' .. tostring(err), 'WARN')")
lines.append(" end")
lines.append("end")
return "\n".join(lines) + "\n"
def inject_makeusers(users, arch):
base = BUILD_ROOT / "$" if arch else BUILD_ROOT
kmod_path = base / "lib" / "modules" / "Hyperion" / "50_firstboot_users.kmod"
kmod_path.parent.mkdir(parents=True, exist_ok=True)
kmod_path.write_text(_make_firstboot_kmod(users), encoding="utf-8")
print(" Wrote first-boot user setup -> " + str(kmod_path.relative_to(BUILD_ROOT)))
def main():
parser = argparse.ArgumentParser(description="HyperionOS build script")
parser.add_argument("target", choices=["build", "build-mini", "build-micro", "build-test", "build-mini-test", "build-micro-test", "clean", "prod", "prod-mini"])
parser.add_argument("--arch", choices=["cct", "oc"], default=None,
help="Target architecture")
parser.add_argument("--release", dest="release", action="store_true", default=True,
help="Release build: eeprom placed as startup.lua (default)")
parser.add_argument("--dev", dest="release", action="store_false",
help="Dev build: boot.lua and eeprom copied unchanged")
parser.add_argument(
"--makeuser", metavar=("USERNAME", "PASSWORD"), nargs=2, action="append",
default=[],
help=(
"Pre-create a user on first boot (dev builds only). "
"May be specified multiple times. "
"Example: --makeuser root secretpass --makeuser alice alicepass"
),
)
args = parser.parse_args()
if args.makeuser and args.release:
parser.error("--makeuser is only allowed with --dev builds")
if args.target == "clean":
clean()
return
prod = "prod" in args.target
minify = "mini" in args.target or "micro" in args.target
micro = "micro" in args.target
include_test = "test" in args.target
if micro:
import lz4.block
run_build(minify=minify, micro=micro, include_test=include_test, arch=args.arch, release=args.release, prod=prod)
if args.makeuser:
print("Injecting first-boot user setup ...")
inject_makeusers(args.makeuser, args.arch)
print()
print("Build complete.")
if __name__ == "__main__":
main()

View File

@@ -29,4 +29,7 @@ logTaskExit<bool>
showModLoad<bool>
log module loads
tmpToDisk<bool>
writes tmp to disk instead of ram
```