diff --git a/build.lua b/build.lua index 5dea716..af4623f 100644 --- a/build.lua +++ b/build.lua @@ -161,13 +161,20 @@ local function exportFile(srcpath, dstpath) end local function buildKernel(dst) - local sources = fs.list('src/kernel/') + local srcdir = 'src/kernel/' + local sources = fs.list(srcdir) table.sort(sources) + for i, path in ipairs(sources) do + path = srcdir .. path + sources[i] = path + assert(loadfile(path)) --validate syntax + end + function sources:lines() return coroutine.wrap(function() for i, path in ipairs(sources) do - local f = assert(io.open('src/kernel/' .. path)) + local f = assert(io.open(path)) for l in f:lines() do coroutine.yield(l) end diff --git a/src/kernel/00_preamble.lua b/src/kernel/00_preamble.lua index b5cc316..379f036 100644 --- a/src/kernel/00_preamble.lua +++ b/src/kernel/00_preamble.lua @@ -3,7 +3,6 @@ local bootfs = ... -- ...but [lua bios, openloader] don't, so fall back to the usual way bootfs = bootfs or computer.getBootAddress() - --store all functions we need in locals so userspace mischief can't replace them --prefix 'k' (for kernel) on any functions userspace will also have a version of to ensure kernelspace --function usage is intentional (ie prevent accidentally using the kernel's `load` in a userspace `loadfile`) @@ -12,9 +11,9 @@ local assert, checkArg, error, kload, ipairs, next, pairs, rawequal, rawset, set local invoke = component.invoke local kpullSignal, shutdown, uptime = computer.pullSignal, computer.shutdown, computer.uptime local co_create, co_status, kresume, kyield = coroutine.create, coroutine.status, coroutine.resume, coroutine.yield -local huge = math.huge -local format = string.format -local pack, unpack = table.pack, table.unpack +local huge, math_type = math.huge, math.type +local find, format, match, sub = string.find, string.format, string.match, string.sub +local concat, pack, unpack = table.concat, table.pack, table.unpack local big = math.maxinteger or huge --some large amount to read at once in kreadfile @@ -26,4 +25,44 @@ _ENV = setmetatable({}, { __newindex = function(self, key) return error(format('assigned global %q', key), 2) end, }) + +local isFile --function(obj:any):boolean, boolean -- returns (is obj a file?), (is obj an open file?) + +--[[function(index:number, val:any, types:string...):string -- asserts `types` contains the type of `val`. similar to `checkArg`. + recognizes all possible returns from `type()` as well as ['integer', 'float', 'file']. unlike io.type(), 'file' ignores whether the file is open or closed. + on success, returns the element of `types` that matched. + on failure, throws an error originating from the caller's caller that mimicks built-in type errors. `index` should be the 1-based index of the argument that was the wrong type. +]] +local checkArgEx +do + -- tail-recursive varargs part of checkArgEx + local function check(val, t, want, ...) + if t == want then + return want + elseif want == 'integer' or want == 'float' then + if math_type(val) == want then + return want + end + elseif want == 'file' then + if isFile(val) then + return want + end + elseif not want then + return nil + else + return check(val, t, ...) + end + end + + function checkArgEx(i, val, ...) + local t = type(val) + local ok = check(val, t, ...) + if ok then + return ok + else + error(format('bad argument #%d (%s expected, got %s)', i, concat({...}, ' or '), t), 3) + end + end +end + -- vi: set ts=2: diff --git a/src/kernel/02_io.lua b/src/kernel/02_io.lua new file mode 100644 index 0000000..771dc03 --- /dev/null +++ b/src/kernel/02_io.lua @@ -0,0 +1,307 @@ +local openNull, openPath +do + local filedata = setmetatable({}, { __mode = 'k' }) --weak-keyed mapping of file object to data/inner (fh of wrapFile) + local filemt = { __metatable = '' } + filemt.__index = filemt + + function isFile(obj) + if filedata[obj] then + -- if this is true, it's an object we control, so it's safe to trust attribute access + return true, not obj._closed + else + return false + end + end + + local function wrapFile(mode, fh) + local rwa, bin, plus, b2 = match(mode or 'r', '^([rwa])(b?)(%+?)(b?)$') --match all of x, xb, x+, xb+, x+b + if not rwa or (bin ~= '' and b2 ~= '') then return nil, 'bad mode' end + bin = bin .. b2 --when (bin == '') xor (b2 == ''), this is logical or + + local read = rwa == 'r' or plus + local write = rwa ~= 'r' or plus + local append = rwa == 'a' + local update = plus ~= '' + + local file = setmetatable({}, filemt) + filedata[file] = fh + if read then + fh._file_rbuf = '' + end + if write then + fh._file_wbuf, fh._file_wmode, fh._file_wcap = '', 'full', nil --nil capacity means default, which may vary with e.g. system memory usage + end + + return file + end + + local function null_close() return true end + local function null_read() return nil end --always eof + local function null_write() return true end --eats data + --null devices can't be seeked + + function openNull(mode) + return assert(wrapFile(mode, { close = null_close, read = null_read, write = null_write })) + end + + local function path_close(fh) return invoke(fh.address, 'close', fh.handle) end + local function path_read(fh, n) return invoke(fh.address, 'read', fh.handle, n) end + local function path_write(fh, data) return invoke(fh.address, 'write', fh.handle, data) end + local function path_seek(fh, ...) return invoke(fh.address, 'seek', fh.handle, ...) end + + function openPath(mode, addr, path) + local fd, err = invoke(addr, 'open', path, mode) --should we validate `mode` first? + if not fd then + return nil, err + end + return assert(wrapFile(mode, { address = addr, handle = fd, close = path_close, read = path_read, write = path_write, seek = path_seek })) + end + + + --file method helpers + local function assertOpen(file, how) + local fh = filedata[file] + if fh._file_closed then error('attempt to use a closed file', 3) end + if how == 'read' then + if not fh._file_rbuf then return nil, 'file not readable' end + elseif how == 'write' then + if not fh._file_wbuf then return nil, 'file not writable' end + elseif how == 'seek' then + if not fh.seek then return nil, 'file not seekable' end + elseif how then + error('bad how: should be read, write, seek, or nil', 2) + end + return fh + end + + local function flush(fh) + local ok, err = fh:write(fh._file_wbuf) + if ok then + fh._file_wbuf = '' + return true + else + return nil, err + end + end + + local function read1(fh, n) + if type(n) == 'number' then + while #fh._file_rbuf < n do + local blocksz = n - #fh._file_rbuf + if blocksz < 512 then blocksz = 512 end --TODO tune default capacity to e.g. free memory + local data, err = fh:read(blocksz) + if err then + return nil, err + elseif not data then --eof + break + end + fh._file_rbuf = fh._file_rbuf .. data + end + + local data = sub(fh._file_rbuf, 1, n) + fh._file_rbuf = sub(fh._file_rbuf, n + 1) + return data + elseif n == 'a' then + local parts = { fh._file_rbuf } + local chunk, err + repeat + chunk, err = fh:read(512) + parts[#parts+1] = chunk + until not chunk + + local cat = concat(parts) + + if err then + fh._file_rbuf = cat + return nil, err + else + fh._file_rbuf = '' + return cat + end + else + error('TODO read1 patterns') + end + end + + local function write1(fh, data) + local wbuf = fh._file_wbuf .. data + fh._file_wbuf = wbuf + + local mode = fh._file_wmode + if mode == 'no' then + if wbuf ~= '' then return flush(fh) end + elseif mode == 'full' then + local cap = fh._file_wcap or 512 --TODO tune default capacity to e.g. free memory + if #fh._file_wbuf >= cap then + return flush(fh) + end + else --for line buffering, only flush complete lines + local i1, i2 = find(fh._file_wbuf, '.+[\r\n]') --`i2` is the last cr or lf in the buffer + if i1 then --if there are any newlines + local rest = sub(fh._file_wbuf, i2 + 1) + fh._file_wbuf = sub(fh._file_wbuf, 1, i2) + local ok, err = flush(fh) + if ok then fh._file_wbuf = rest end + return ok, err + else --no newlines, nothing to flush + return true + end + end + end + + + --file methods + function filemt:close() + local fh = assertOpen(self) + if fh._file_wbuf then + local ok, err = flush(fh) + if not ok then return nil, err end --XXX should close ever fail? what do we expect the caller to do? + end + fh:close() + fh._file_closed = true + end + + filemt.__close = filemt.close --for future lua 5.4 compat + + function filemt:flush() + local fh, err = assertOpen(self, 'write') + if not fh then return nil, err end + + local ok, err = flush(fh) + if not ok then return nil, err end + + return self + end + + function filemt:lines(...) + local pats = pack(...) + for i = 1, pats.n do + local pat = pats[i] + local t = checkArgEx(i, pat, 'string', 'integer') + if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then + error(format('bad argument #%d to \'lines\' (invalid format)', i), 2) + end + end + if pats.n == 0 then + pats[1], pats.n = 'l', 1 + end + + local ok, err = assertOpen(self, 'read') + if not ok then error(err, 2) end --more informative than the error from `for line in nil, err do end` + + return function() + local ret = {} + for i = 1, pats.n do + local ok, err = read1(fh, pats[i]) + if not ok then + if err then error(err, 2) end + break + end + ret[i] = ok + end + return unpack(ret, 1, pats.n) + end + end + + function filemt:read(...) + local args = pack(...) + for i = 1, args.n do + local pat = args[i] + local t = checkArgEx(i, pat, 'string', 'integer') + if t == 'string' and pat ~= 'n' and pat ~= 'a' and pat ~= 'l' and pat ~= 'L' then + error(format('bad argument #%d to \'read\' (invalid format)', i), 2) + end + end + if args.n == 0 then + args[1], args.n = 'l', 1 + end + + local fh, err = assertOpen(self, 'read') + if not fh then return nil, err end + + local args = pack(...) + local iend = args.n + for i = 1, args.n do + local ok, err = read1(fh, args[i]) + if ok then + args[i] = ok + else + iend = i + 1 + args[i] = nil + args[i + 1] = err + break + end + end + return unpack(args, 1, iend) + end + + function filemt:seek(whence, offset) + if type(whence) == 'number' and rawequal(offset, nil) then + checkArgEx(1, whence, 'integer') --produce a better error message citing arg 1 + whence, offset = 'cur', whence + end + checkArgEx(1, whence, 'string', 'nil') + checkArgEx(2, offset, 'integer', 'nil') + + local fh, err = assertOpen(self, 'seek') + if not fh then return nil, err end + + --before seeking, flush any buffered writes + if fh._file_wbuf then + local ok, err = flush(fh) + if not ok then return nil, err end + end + + local ok, err = fh:seek(whence or 'cur', offset or 0) + if not ok then return nil, err end + + + local tell = (whence == 'cur' or not whence) and (offset == 0 or not offset) --true if we didn't move ie ftell() + --after seeking, if we moved, invalidate the read buffer + if (whence and whence ~= cur) or (offset and offset ~= 0) then + fh._file_rbuf = fh._file_rbuf and '' + end + + return ok + end + + function filemt:setvbuf(mode, size) + checkArg(1, mode, 'string') + if mode ~= 'no' and mode ~= 'line' and mode ~= 'full' then + error(format('bad argument #1 to \'setvbuf\' (invalid option %q)', mode), 2) + end + checkArgEx(2, size, 'integer', 'nil') + + local fh, err = assertOpen(self) + if not fh then return nil, err end + + if not fh._file_wbuf then + return true --on readonly files, setvbuf is a noop + end + + fh._file_wmode, fh._file_wcap = mode, size + --don't flush now; we don't have a way to propagate errors. the next write (or close) will take care of it. + return true + end + + function filemt:write(...) + local args = pack(...) + for i = 1, args.n do + local arg = args[i] + local t = checkArgEx(i, arg, 'string', 'number') + if t ~= 'string' then args[i] = tostring(arg) end + end + + local fh, err = assertOpen(self, 'write') + if not fh then return nil, err end + + for i = 1, args.n do + local ok, err = write1(fh, args[i]) + if not ok then return nil, err end + end + + return self + end +end + +-- vi: set ts=2: diff --git a/src/kernel/02_processes.lua b/src/kernel/03_processes.lua similarity index 100% rename from src/kernel/02_processes.lua rename to src/kernel/03_processes.lua diff --git a/src/kernel/03_environment.lua b/src/kernel/04_environment.lua similarity index 70% rename from src/kernel/03_environment.lua rename to src/kernel/04_environment.lua index 22dc5af..0b9956e 100644 --- a/src/kernel/03_environment.lua +++ b/src/kernel/04_environment.lua @@ -9,7 +9,7 @@ given : ]] -envBase._OSVERSION = 'kitn 0.1.2' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver" +envBase._OSVERSION = 'kitn 0.2.0-beta.1' --prefer [openloader, openos] "$name $ver" over plan9k "$name/$ver" envBase.kitn = {} envBase._G, envBase.load = nil @@ -84,6 +84,50 @@ function fillEnv(proc, uenv) uenv.load, uenv.loadfile, uenv.dofile = uload, uloadfile, udofile + local stdin, stdout = openNull('r'), openNull('w') + local uio; uio = { + stdin = stdin, stdout = stdout, stderr = stdout, + close = function(file) return (file or uio.stdout):close() end, + flush = function() return uio.stdout:flush() end, + input = function(file) + local t = checkArgEx(1, file, 'string', 'table', 'nil') + if t ~= 'nil' then + uio.stdin = (t == 'string' and assert(uio.open(file, 'r')) or file) + end + return uio.stdin + end, + lines = function(filename, ...) + checkArg(1, filename, 'string', 'nil') + error('TODO io.lines') --iff we open a file, we need to close it + end, + open = function(filename, mode) return openPath(mode, bootfs, filename) end, + output = function(file) + local t = checkArgEx(1, file, 'string', 'table', 'nil') + if t ~= nil then + uio.stdout = (t == 'string' and assert(uio.open(file, 'w')) or file) + end + return uio.stdout + end, + --popen is unimplemented because we don't have a notion of a shell + read = function(...) return uio.stdin:read(...) end, + tmpfile = function() + --use math.random() to pick a filename on computer.tmpAddress() that doesn't exist yet and create it + error('TODO io.tmpfile') --file is deleted on process exit, requires atExit mechanism + end, + type = function(obj) + local file, opened = isFile(obj) + if opened then + return 'file' + elseif file then + return 'closed file' + else + return nil + end + end, + write = function(...) return uio.stdout:write(...) end, + } + uenv.io = uio + --env will automatically mirror envBase.kitn here local ukitn = uenv.kitn diff --git a/src/kernel/04_scheduler.lua b/src/kernel/05_scheduler.lua similarity index 100% rename from src/kernel/04_scheduler.lua rename to src/kernel/05_scheduler.lua